├── .gitignore ├── LICENSE.md ├── MANIFEST.in ├── README.md ├── pyram ├── PyRAM.py ├── PyRAMmp.py ├── Tests │ ├── TestPyRAM.py │ ├── TestPyRAMmp.py │ ├── TestPyRAMmp_Config.xml │ ├── __init__.py │ └── tl_ref.line ├── __init__.py ├── matrc.py ├── outpt.py └── solve.py └── setup.py /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | /build 3 | /dist 4 | /pyram.egg-info 5 | __pycache__/ 6 | /pyram/Tests/tl.line 7 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright © 2000 Dr. Michael D. Collins, Copyright © 2017 Marcus Donnelly 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | 12 | - Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, 16 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR 17 | CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, 18 | PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF 19 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, 20 | EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 21 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include LICENSE.md 2 | include pyram/Tests/TestPyRAMmp_Config.xml 3 | include pyram/Tests/tl_ref.line 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## PyRAM 2 | 3 | ----- 4 | 5 | Python adaptation of the Range-dependent Acoustic Model (RAM). 6 | 7 | RAM was created by Michael D Collins at the US Naval Research Laboratory. 8 | This adaptation is of RAM v1.5, available from the Ocean Acoustics Library at 9 | https://oalib-acoustics.org/models-and-software/parabolic-equation 10 | 11 | The purpose of PyRAM is to provide a version of RAM which can be used within a 12 | Python interpreter environment (e.g. Spyder or the Jupyter notebook) and is 13 | easier to understand, extend and integrate into other applications than the 14 | Fortran version. It is written in pure Python and achieves speeds comparable to 15 | native code by using the Numba library for JIT compilation. 16 | 17 | The PyRAM class contains methods which largely correspond to the original 18 | Fortran subroutines and functions (including retaining the same names). The 19 | variable names are also mostly the same. However some of the original code, 20 | e.g. subroutine zread, is unnecessary when the same purpose can be achieved 21 | using widely available Python library functions, e.g. from NumPy or SciPy, 22 | and has therefore been replaced. 23 | 24 | A difference in functionality is that sound speed profile updates with range 25 | are decoupled from seabed parameter updates, which provides more flexibility 26 | in specifying the environment, e.g. if the data comes from different sources. 27 | 28 | PyRAM also provides various conveniences, e.g. automatic calculation of range 29 | and depth steps, though these can be overridden using keyword arguments. 30 | -------------------------------------------------------------------------------- /pyram/PyRAM.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyRAM: Python adaptation of the Range-dependent Acoustic Model (RAM). 3 | RAM was created by Michael D Collins at the US Naval Research Laboratory. 4 | This adaptation is of RAM v1.5, available from the Ocean Acoustics Library at 5 | https://oalib-acoustics.org/models-and-software/parabolic-equation 6 | 7 | The purpose of PyRAM is to provide a version of RAM which can be used within a 8 | Python interpreter environment (e.g. Spyder or the Jupyter notebook) and is 9 | easier to understand, extend and integrate into other applications than the 10 | Fortran version. It is written in pure Python and achieves speeds comparable to 11 | native code by using the Numba library for JIT compilation. 12 | 13 | The PyRAM class contains methods which largely correspond to the original 14 | Fortran subroutines and functions (including retaining the same names). The 15 | variable names are also mostly the same. However some of the original code 16 | (e.g. subroutine zread) is unnecessary when the same purpose can be achieved 17 | using available Python library functions (e.g. from NumPy or SciPy) and has 18 | therefore been replaced. 19 | 20 | A difference in functionality is that sound speed profile updates with range 21 | are decoupled from seabed parameter updates, which provides more flexibility 22 | in specifying the environment (e.g. if the data comes from different sources). 23 | 24 | PyRAM also provides various conveniences, e.g. automatic calculation of range 25 | and depth steps (though these can be overridden using keyword arguments). 26 | """ 27 | 28 | import numpy 29 | from time import process_time 30 | from pyram.matrc import matrc 31 | from pyram.solve import solve 32 | from pyram.outpt import outpt 33 | 34 | 35 | class PyRAM: 36 | 37 | _np_default = 8 38 | _dzf = 0.1 39 | _ndr_default = 1 40 | _ndz_default = 1 41 | _ns_default = 1 42 | _lyrw_default = 20 43 | _id_default = 0 44 | 45 | def __init__(self, freq, zs, zr, z_ss, rp_ss, cw, z_sb, rp_sb, cb, rhob, 46 | attn, rbzb, **kwargs): 47 | 48 | """ 49 | ------- 50 | args... 51 | ------- 52 | freq: Frequency (Hz). 53 | zs: Source depth (m). 54 | zr: Receiver depth (m). 55 | z_ss: Water sound speed profile depths (m), NumPy 1D array. 56 | rp_ss: Water sound speed profile update ranges (m), NumPy 1D array. 57 | cw: Water sound speed values (m/s), 58 | Numpy 2D array, dimensions z_ss.size by rp_ss.size. 59 | z_sb: Seabed parameter profile depths (m), NumPy 1D array. 60 | rp_sb: Seabed parameter update ranges (m), NumPy 1D array. 61 | cb: Seabed sound speed values (m/s), 62 | NumPy 2D array, dimensions z_sb.size by rp_sb.size. 63 | rhob: Seabed density values (g/cm3), same dimensions as cb 64 | attn: Seabed attenuation values (dB/wavelength), same dimensions as cb 65 | rbzb: Bathymetry (m), Numpy 2D array with columns of ranges and depths 66 | --------- 67 | kwargs... 68 | --------- 69 | np: Number of Pade terms. Defaults to _np_default. 70 | c0: Reference sound speed (m/s). Defaults to mean of 1st profile. 71 | dr: Calculation range step (m). Defaults to np times the wavelength. 72 | dz: Calculation depth step (m). Defaults to _dzf*wavelength. 73 | ndr: Number of range steps between outputs. Defaults to _ndr_default. 74 | ndz: Number of depth steps between outputs. Defaults to _ndz_default. 75 | zmplt: Maximum output depth (m). Defaults to maximum depth in rbzb. 76 | rmax: Maximum calculation range (m). Defaults to max in rp_ss or rp_sb. 77 | ns: Number of stability constraints. Defaults to _ns_default. 78 | rs: Maximum range of the stability constraints (m). Defaults to rmax. 79 | lyrw: Absorbing layer width (wavelengths). Defaults to _lyrw_default. 80 | NB: original zmax input not needed due to lyrw. 81 | id: Integer identifier for this instance. 82 | """ 83 | 84 | self._freq, self._zs, self._zr = freq, zs, zr 85 | self.check_inputs(z_ss, rp_ss, cw, z_sb, rp_sb, cb, rhob, attn, rbzb) 86 | self.get_params(**kwargs) 87 | 88 | def run(self): 89 | 90 | """ 91 | Run the model. Sets the following instance variables: 92 | vr: Calculation ranges (m), NumPy 1D array. 93 | vz: Calculation depths (m), NumPy 1D array. 94 | tll: Transmission loss (dB) at receiver depth (zr), 95 | NumPy 1D array, length vr.size. 96 | tlg: Transmission loss (dB) grid, 97 | NumPy 2D array, dimensions vz.size by vr.size. 98 | proc_time: Processing time (s). 99 | """ 100 | 101 | t0 = process_time() 102 | 103 | self.setup() 104 | 105 | nr = int(numpy.round(self._rmax / self._dr)) - 1 106 | 107 | for rn in range(nr): 108 | 109 | self.updat() 110 | 111 | solve(self.u, self.v, self.s1, self.s2, self.s3, 112 | self.r1, self.r2, self.r3, self.iz, self.nz, self._np) 113 | 114 | self.r = (rn + 2) * self._dr 115 | 116 | self.mdr, self.tlc = \ 117 | (outpt(self.r, self.mdr, self._ndr, self._ndz, self.tlc, self.f3, 118 | self.u, self.dir, self.ir, self.tll, self.tlg, self.cpl, self.cpg)[:]) 119 | 120 | self.proc_time = process_time() - t0 121 | 122 | results = {'ID': self._id, 123 | 'Proc Time': self.proc_time, 124 | 'Ranges': self.vr, 125 | 'Depths': self.vz, 126 | 'TL Grid': self.tlg, 127 | 'TL Line': self.tll, 128 | 'CP Grid': self.cpg, 129 | 'CP Line': self.cpl, 130 | 'c0': self._c0} 131 | 132 | return results 133 | 134 | def check_inputs(self, z_ss, rp_ss, cw, z_sb, rp_sb, cb, rhob, attn, rbzb): 135 | 136 | """ 137 | Basic checks on dimensions of inputs 138 | """ 139 | 140 | self._status_ok = True 141 | 142 | # Source and receiver depths 143 | if not z_ss[0] <= self._zs <= z_ss[-1]: 144 | self._status_ok = False 145 | raise ValueError('Source depth outside sound speed depths') 146 | if not z_ss[0] <= self._zr <= z_ss[-1]: 147 | self._status_ok = False 148 | raise ValueError('Receiver depth outside sound speed depths') 149 | if self._status_ok: 150 | self._z_ss = z_ss 151 | 152 | # Water sound speed profiles 153 | num_depths = self._z_ss.size 154 | num_ranges = rp_ss.size 155 | cw_dims = cw.shape 156 | if (cw_dims[0] == num_depths) and (cw_dims[1] == num_ranges): 157 | self._rp_ss, self._cw = rp_ss, cw 158 | else: 159 | raise ValueError('Dimensions of z_ss, rp_ss and cw must be consistent.') 160 | 161 | # Seabed profiles 162 | self._z_sb = z_sb 163 | num_depths = self._z_sb.size 164 | num_ranges = rp_sb.size 165 | for prof in [cb, rhob, attn]: 166 | prof_dims = prof.shape 167 | if (prof_dims[0] != num_depths) or (prof_dims[1] != num_ranges): 168 | self._status_ok = False 169 | if self._status_ok: 170 | self._rp_sb, self._cb, self._rhob, self._attn = \ 171 | rp_sb, cb, rhob, attn 172 | else: 173 | raise ValueError('Dimensions of z_sb, rp_sb, cb, rhob and attn must be consistent.') 174 | 175 | if rbzb[:, 1].max() <= self._z_ss[-1]: 176 | self._rbzb = rbzb 177 | else: 178 | self._status_ok = False 179 | raise ValueError('Deepest sound speed point must be at or below deepest bathymetry point.') 180 | 181 | # Set flags for range-dependence (water SSP, seabed profile, bathymetry) 182 | self.rd_ss = True if self._rp_ss.size > 1 else False 183 | self.rd_sb = True if self._rp_sb.size > 1 else False 184 | self.rd_bt = True if self._rbzb.shape[0] > 1 else False 185 | 186 | def get_params(self, **kwargs): 187 | 188 | """ 189 | Get the parameters from the keyword arguments 190 | """ 191 | 192 | self._np = kwargs.get('np', PyRAM._np_default) 193 | 194 | self._c0 = kwargs.get('c0', numpy.mean(self._cw[:, 0]) 195 | if len(self._cw.shape) > 1 else 196 | numpy.mean(self._cw)) 197 | 198 | self._lambda = self._c0 / self._freq 199 | 200 | # dr and dz are based on 1500m/s to get sensible output steps 201 | self._dr = kwargs.get('dr', self._np * 1500 / self._freq) 202 | self._dz = kwargs.get('dz', PyRAM._dzf * 1500 / self._freq) 203 | 204 | self._ndr = kwargs.get('ndr', PyRAM._ndr_default) 205 | self._ndz = kwargs.get('ndz', PyRAM._ndz_default) 206 | 207 | self._zmplt = kwargs.get('zmplt', self._rbzb[:, 1].max()) 208 | 209 | self._rmax = kwargs.get('rmax', numpy.max([self._rp_ss.max(), 210 | self._rp_sb.max(), 211 | self._rbzb[:, 0].max()])) 212 | 213 | self._ns = kwargs.get('ns', PyRAM._ns_default) 214 | self._rs = kwargs.get('rs', self._rmax + self._dr) 215 | 216 | self._lyrw = kwargs.get('lyrw', PyRAM._lyrw_default) 217 | 218 | self._id = kwargs.get('id', PyRAM._id_default) 219 | 220 | self.proc_time = None 221 | 222 | def setup(self): 223 | 224 | """ 225 | Initialise the parameters, acoustic field, and matrices 226 | """ 227 | 228 | if self._rbzb[-1, 0] < self._rmax: 229 | self._rbzb = numpy.append(self._rbzb, 230 | numpy.array([[self._rmax, self._rbzb[-1, 1]]]), 231 | axis=0) 232 | 233 | self.eta = 1 / (40 * numpy.pi * numpy.log10(numpy.exp(1))) 234 | self.ib = 0 # Bathymetry pair index 235 | self.mdr = 0 # Output range counter 236 | self.r = self._dr 237 | self.omega = 2 * numpy.pi * self._freq 238 | ri = self._zr / self._dz 239 | self.ir = int(numpy.floor(ri)) # Receiver depth index 240 | self.dir = ri - self.ir # Offset 241 | self.k0 = self.omega / self._c0 242 | self._z_sb += self._z_ss[-1] # Make seabed profiles relative to deepest water profile point 243 | self._zmax = self._z_sb.max() + self._lyrw * self._lambda 244 | self.nz = int(numpy.floor(self._zmax / self._dz)) - 1 # Number of depth grid points - 2 245 | self.nzplt = int(numpy.floor(self._zmplt / self._dz)) # Deepest output grid point 246 | self.iz = int(numpy.floor(self._rbzb[0, 1] / self._dz)) # First index below seabed 247 | self.iz = max(1, self.iz) 248 | self.iz = min(self.nz - 1, self.iz) 249 | 250 | self.u = numpy.zeros(self.nz + 2, dtype=numpy.complex128) 251 | self.v = numpy.zeros(self.nz + 2, dtype=numpy.complex128) 252 | self.ksq = numpy.zeros(self.nz + 2, dtype=numpy.complex128) 253 | self.ksqb = numpy.zeros(self.nz + 2, dtype=numpy.complex128) 254 | self.r1 = numpy.zeros([self.nz + 2, self._np], dtype=numpy.complex128) 255 | self.r2 = numpy.zeros([self.nz + 2, self._np], dtype=numpy.complex128) 256 | self.r3 = numpy.zeros([self.nz + 2, self._np], dtype=numpy.complex128) 257 | self.s1 = numpy.zeros([self.nz + 2, self._np], dtype=numpy.complex128) 258 | self.s2 = numpy.zeros([self.nz + 2, self._np], dtype=numpy.complex128) 259 | self.s3 = numpy.zeros([self.nz + 2, self._np], dtype=numpy.complex128) 260 | self.pd1 = numpy.zeros(self._np, dtype=numpy.complex128) 261 | self.pd2 = numpy.zeros(self._np, dtype=numpy.complex128) 262 | 263 | self.alpw = numpy.zeros(self.nz + 2) 264 | self.alpb = numpy.zeros(self.nz + 2) 265 | self.f1 = numpy.zeros(self.nz + 2) 266 | self.f2 = numpy.zeros(self.nz + 2) 267 | self.f3 = numpy.zeros(self.nz + 2) 268 | self.ksqw = numpy.zeros(self.nz + 2) 269 | nvr = int(numpy.floor(self._rmax / (self._dr * self._ndr))) 270 | self._rmax = nvr * self._dr * self._ndr 271 | nvz = int(numpy.floor(self.nzplt / self._ndz)) 272 | self.vr = numpy.arange(1, nvr + 1) * self._dr * self._ndr 273 | self.vz = numpy.arange(1, nvz + 1) * self._dz * self._ndz 274 | self.tll = numpy.zeros(nvr) 275 | self.tlg = numpy.zeros([nvz, nvr]) 276 | self.cpl = numpy.zeros(nvr) * 1j 277 | self.cpg = numpy.zeros([nvz, nvr]) * 1j 278 | self.tlc = -1 # TL output range counter 279 | 280 | self.ss_ind = 0 # Sound speed profile range index 281 | self.sb_ind = 0 # Seabed parameters range index 282 | self.bt_ind = 0 # Bathymetry range index 283 | 284 | # The initial profiles and starting field 285 | self.profl() 286 | self.selfs() 287 | self.mdr, self.tlc = \ 288 | (outpt(self.r, self.mdr, self._ndr, self._ndz, self.tlc, self.f3, 289 | self.u, self.dir, self.ir, self.tll, self.tlg, self.cpl, self.cpg)[:]) 290 | 291 | # The propagation matrices 292 | self.epade() 293 | matrc(self.k0, self._dz, self.iz, self.iz, self.nz, self._np, 294 | self.f1, self.f2, self.f3, self.ksq, self.alpw, self.alpb, 295 | self.ksqw, self.ksqb, self.rhob, self.r1, self.r2, self.r3, 296 | self.s1, self.s2, self.s3, self.pd1, self.pd2) 297 | 298 | def profl(self): 299 | 300 | """ 301 | Set up the profiles 302 | """ 303 | 304 | attnf = 10 # 10dB/wavelength at floor 305 | 306 | z = numpy.linspace(0, self._zmax, self.nz + 2) 307 | self.cw = numpy.interp(z, self._z_ss, self._cw[:, self.ss_ind], 308 | left=self._cw[0, self.ss_ind], 309 | right=self._cw[-1, self.ss_ind]) 310 | self.cb = numpy.interp(z, self._z_sb, self._cb[:, self.sb_ind], 311 | left=self._cb[0, self.sb_ind], 312 | right=self._cb[-1, self.sb_ind]) 313 | self.rhob = numpy.interp(z, self._z_sb, self._rhob[:, self.sb_ind], 314 | left=self._rhob[0, self.sb_ind], 315 | right=self._rhob[-1, self.sb_ind]) 316 | attnlyr = numpy.concatenate((self._attn[:, self.sb_ind], 317 | [self._attn[-1, self.sb_ind], attnf])) 318 | zlyr = numpy.concatenate((self._z_sb, 319 | [self._z_sb[-1] + 0.75 * self._lyrw * self._lambda, 320 | self._z_sb[-1] + self._lyrw * self._lambda])) 321 | self.attn = numpy.interp(z, zlyr, attnlyr, 322 | left=self._attn[0, self.sb_ind], 323 | right=attnf) 324 | 325 | for i in range(self.nz + 2): 326 | self.ksqw[i] = (self.omega / self.cw[i])**2 - self.k0**2 327 | self.ksqb[i] = ((self.omega / self.cb[i]) * 328 | (1 + 1j * self.eta * self.attn[i]))**2 - self.k0**2 329 | self.alpw[i] = numpy.sqrt(self.cw[i] / self._c0) 330 | self.alpb[i] = numpy.sqrt(self.rhob[i] * self.cb[i] / self._c0) 331 | 332 | def updat(self): 333 | 334 | """ 335 | Matrix updates 336 | """ 337 | 338 | # Varying bathymetry 339 | if self.rd_bt: 340 | npt = self._rbzb.shape[0] 341 | while (self.bt_ind < npt - 1) and (self.r >= self._rbzb[self.bt_ind + 1, 0]): 342 | self.bt_ind += 1 343 | jz = self.iz 344 | z = self._rbzb[self.bt_ind, 1] + \ 345 | (self.r + 0.5 * self._dr - self._rbzb[self.bt_ind, 0]) * \ 346 | (self._rbzb[self.bt_ind + 1, 1] - self._rbzb[self.bt_ind, 1]) / \ 347 | (self._rbzb[self.bt_ind + 1, 0] - self._rbzb[self.bt_ind, 0]) 348 | self.iz = int(numpy.floor(z / self._dz)) # First index below seabed 349 | self.iz = max(1, self.iz) 350 | self.iz = min(self.nz - 1, self.iz) 351 | if self.iz != jz: 352 | matrc(self.k0, self._dz, self.iz, jz, self.nz, self._np, 353 | self.f1, self.f2, self.f3, self.ksq, self.alpw, 354 | self.alpb, self.ksqw, self.ksqb, self.rhob, self.r1, 355 | self.r2, self.r3, self.s1, self.s2, self.s3, self.pd1, 356 | self.pd2) 357 | 358 | # Varying sound speed profile 359 | if self.rd_ss: 360 | npt = self._rp_ss.size 361 | ss_ind_o = self.ss_ind 362 | while (self.ss_ind < npt - 1) and (self.r >= self._rp_ss[self.ss_ind + 1]): 363 | self.ss_ind += 1 364 | if self.ss_ind != ss_ind_o: 365 | self.profl() 366 | matrc(self.k0, self._dz, self.iz, self.iz, self.nz, self._np, 367 | self.f1, self.f2, self.f3, self.ksq, self.alpw, 368 | self.alpb, self.ksqw, self.ksqb, self.rhob, self.r1, 369 | self.r2, self.r3, self.s1, self.s2, self.s3, self.pd1, 370 | self.pd2) 371 | 372 | # Varying seabed profile 373 | if self.rd_sb: 374 | npt = self._rp_sb.size 375 | sb_ind_o = self.sb_ind 376 | while (self.sb_ind < npt - 1) and (self.r >= self._rp_sb[self.sb_ind + 1]): 377 | self.sb_ind += 1 378 | if self.sb_ind != sb_ind_o: 379 | self.profl() 380 | matrc(self.k0, self._dz, self.iz, self.iz, self.nz, self._np, 381 | self.f1, self.f2, self.f3, self.ksq, self.alpw, 382 | self.alpb, self.ksqw, self.ksqb, self.rhob, self.r1, 383 | self.r2, self.r3, self.s1, self.s2, self.s3, self.pd1, 384 | self.pd2) 385 | 386 | # Turn off the stability constraints 387 | if self.r >= self._rs: 388 | self._ns = 0 389 | self._rs = self._rmax + self._dr 390 | self.epade() 391 | matrc(self.k0, self._dz, self.iz, self.iz, self.nz, self._np, 392 | self.f1, self.f2, self.f3, self.ksq, self.alpw, self.alpb, 393 | self.ksqw, self.ksqb, self.rhob, self.r1, self.r2, self.r3, 394 | self.s1, self.s2, self.s3, self.pd1, self.pd2) 395 | 396 | def selfs(self): 397 | 398 | """ 399 | The self-starter 400 | """ 401 | 402 | # Conditions for the delta function 403 | 404 | si = self._zs / self._dz 405 | _is = int(numpy.floor(si)) # Source depth index 406 | dis = si - _is # Offset 407 | 408 | self.u[_is] = (1 - dis) * numpy.sqrt(2 * numpy.pi / self.k0) / \ 409 | (self._dz * self.alpw[_is]) 410 | self.u[_is + 1] = dis * numpy.sqrt(2 * numpy.pi / self.k0) / \ 411 | (self._dz * self.alpw[_is]) 412 | 413 | # Divide the delta function by (1-X)**2 to get a smooth rhs 414 | 415 | self.pd1[0] = 0 416 | self.pd2[0] = -1 417 | 418 | matrc(self.k0, self._dz, self.iz, self.iz, self.nz, 1, 419 | self.f1, self.f2, self.f3, self.ksq, self.alpw, self.alpb, 420 | self.ksqw, self.ksqb, self.rhob, self.r1, self.r2, self.r3, 421 | self.s1, self.s2, self.s3, self.pd1, self.pd2) 422 | for _ in range(2): 423 | solve(self.u, self.v, self.s1, self.s2, self.s3, 424 | self.r1, self.r2, self.r3, self.iz, self.nz, 1) 425 | 426 | # Apply the operator (1-X)**2*(1+X)**(-1/4)*exp(ci*k0*r*sqrt(1+X)) 427 | 428 | self.epade(ip=2) 429 | matrc(self.k0, self._dz, self.iz, self.iz, self.nz, self._np, 430 | self.f1, self.f2, self.f3, self.ksq, self.alpw, self.alpb, 431 | self.ksqw, self.ksqb, self.rhob, self.r1, self.r2, self.r3, 432 | self.s1, self.s2, self.s3, self.pd1, self.pd2) 433 | solve(self.u, self.v, self.s1, self.s2, self.s3, 434 | self.r1, self.r2, self.r3, self.iz, self.nz, self._np) 435 | 436 | def epade(self, ip=1): 437 | 438 | """ 439 | The coefficients of the rational approximation 440 | """ 441 | 442 | n = 2 * self._np 443 | _bin = numpy.zeros([n + 1, n + 1]) 444 | a = numpy.zeros([n + 1, n + 1], dtype=numpy.complex128) 445 | b = numpy.zeros(n, dtype=numpy.complex128) 446 | dg = numpy.zeros(n + 1, dtype=numpy.complex128) 447 | dh1 = numpy.zeros(n, dtype=numpy.complex128) 448 | dh2 = numpy.zeros(n, dtype=numpy.complex128) 449 | dh3 = numpy.zeros(n, dtype=numpy.complex128) 450 | fact = numpy.zeros(n + 1) 451 | sig = self.k0 * self._dr 452 | 453 | if ip == 1: 454 | nu, alp = 0, 0 455 | else: 456 | nu, alp = 1, -0.25 457 | 458 | # The factorials 459 | fact[0] = 1 460 | for i in range(1, n): 461 | fact[i] = (i + 1) * fact[i - 1] 462 | 463 | # The binomial coefficients 464 | for i in range(n + 1): 465 | _bin[i, 0] = 1 466 | _bin[i, i] = 1 467 | for i in range(2, n + 1): 468 | for j in range(1, i): 469 | _bin[i, j] = _bin[i - 1, j - 1] + _bin[i - 1, j] 470 | 471 | # The accuracy constraints 472 | dg, dh1, dh2, dh3 = \ 473 | self.deriv(n, sig, alp, dg, dh1, dh2, dh3, _bin, nu) 474 | for i in range(n): 475 | b[i] = dg[i + 1] 476 | for i in range(n): 477 | if 2 * i <= n - 1: 478 | a[i, 2 * i] = fact[i] 479 | for j in range(i + 1): 480 | if 2 * j + 1 <= n - 1: 481 | a[i, 2 * j + 1] = -_bin[i + 1, j + 1] * fact[j] * dg[i - j] 482 | 483 | # The stability constraints 484 | 485 | if self._ns >= 1: 486 | z1 = -3 + 0j 487 | b[n - 1] = -1 488 | for j in range(self._np): 489 | a[n - 1, 2 * j] = z1**(j + 1) 490 | a[n - 1, 2 * j + 1] = 0 491 | 492 | if self._ns >= 2: 493 | z1 = -1.5 + 0j 494 | b[n - 2] = -1 495 | for j in range(self._np): 496 | a[n - 2, 2 * j] = z1**(j + 1) 497 | a[n - 2, 2 * j + 1] = 0 498 | 499 | a, b = self.gauss(n, a, b, self.pivot) 500 | 501 | dh1[0] = 1 502 | for j in range(self._np): 503 | dh1[j + 1] = b[2 * j] 504 | dh1, dh2 = self.fndrt(dh1, self._np, dh2, self.guerre) 505 | for j in range(self._np): 506 | self.pd1[j] = -1 / dh2[j] 507 | 508 | dh1[0] = 1 509 | for j in range(self._np): 510 | dh1[j + 1] = b[2 * j + 1] 511 | dh1, dh2 = self.fndrt(dh1, self._np, dh2, self.guerre) 512 | for j in range(self._np): 513 | self.pd2[j] = -1 / dh2[j] 514 | 515 | @staticmethod 516 | def deriv(n, sig, alp, dg, dh1, dh2, dh3, _bin, nu): 517 | 518 | """ 519 | The derivatives of the operator function at x=0 520 | """ 521 | 522 | dh1[0] = 0.5 * 1j * sig 523 | exp1 = -0.5 524 | dh2[0] = alp 525 | exp2 = -1 526 | dh3[0] = -2 * nu 527 | exp3 = -1 528 | for i in range(1, n): 529 | dh1[i] = dh1[i - 1] * exp1 530 | exp1 -= 1 531 | dh2[i] = dh2[i - 1] * exp2 532 | exp2 -= 1 533 | dh3[i] = -nu * dh3[i - 1] * exp3 534 | exp3 -= 1 535 | 536 | dg[0] = 1 537 | dg[1] = dh1[0] + dh2[0] + dh3[0] 538 | for i in range(1, n): 539 | dg[i + 1] = dh1[i] + dh2[i] + dh3[i] 540 | for j in range(i): 541 | dg[i + 1] += _bin[i, j] * (dh1[j] + dh2[j] + dh3[j]) * dg[i - j] 542 | 543 | return dg, dh1, dh2, dh3 544 | 545 | @staticmethod 546 | def gauss(n, a, b, pivot): 547 | 548 | """ 549 | Gaussian elimination 550 | """ 551 | 552 | # Downward elimination 553 | for i in range(n): 554 | if i < n - 1: 555 | a, b = pivot(n, i, a, b) 556 | a[i, i] = 1 / a[i, i] 557 | b[i] *= a[i, i] 558 | if i < n - 1: 559 | for j in range(i + 1, n + 1): 560 | a[i, j] *= a[i, i] 561 | for k in range(i + 1, n): 562 | b[k] -= a[k, i] * b[i] 563 | for j in range(i + 1, n): 564 | a[k, j] -= a[k, i] * a[i, j] 565 | 566 | # Back substitution 567 | for i in range(n - 2, -1, -1): 568 | for j in range(i + 1, n): 569 | b[i] -= a[i, j] * b[j] 570 | 571 | return a, b 572 | 573 | @staticmethod 574 | def pivot(n, i, a, b): 575 | 576 | """ 577 | Rows are interchanged for stability 578 | """ 579 | 580 | i0 = i 581 | amp0 = numpy.abs(a[i, i]) 582 | for j in range(i + 1, n): 583 | amp = numpy.abs(a[j, i]) 584 | if amp > amp0: 585 | i0 = j 586 | amp0 = amp 587 | 588 | if i0 != i: 589 | b[i0], b[i] = b[i], b[i0] 590 | for j in range(i, n + 1): 591 | a[i0, j], a[i, j] = a[i, j], a[i0, j] 592 | 593 | return a, b 594 | 595 | @staticmethod 596 | def fndrt(a, n, z, guerre): 597 | 598 | """ 599 | The root finding subroutine 600 | """ 601 | 602 | if n == 1: 603 | z[0] = -a[0] / a[1] 604 | return a, z 605 | 606 | if n != 2: 607 | for k in range(n - 1, 1, -1): 608 | # Obtain an approximate root 609 | root = 0 610 | err = 1e-12 611 | a, root, err = guerre(a, k + 1, root, err, 1000) 612 | # Refine the root by iterating five more times 613 | err = 0 614 | a, root, err = guerre(a, k + 1, root, err, 5) 615 | z[k] = root 616 | # Divide out the factor (z-root). 617 | for i in range(k, -1, -1): 618 | a[i] += root * a[i + 1] 619 | for i in range(k + 1): 620 | a[i] = a[i + 1] 621 | 622 | z[1] = 0.5 * (-a[1] + numpy.sqrt(a[1]**2 - 4 * a[0] * a[2])) / a[2] 623 | z[0] = 0.5 * (-a[1] - numpy.sqrt(a[1]**2 - 4 * a[0] * a[2])) / a[2] 624 | 625 | return a, z 626 | 627 | @staticmethod 628 | def guerre(a, n, z, err, nter): 629 | 630 | """ 631 | This subroutine finds a root of a polynomial of degree n > 2 by Laguerre's method 632 | """ 633 | 634 | az = numpy.zeros(n, dtype=numpy.complex128) 635 | azz = numpy.zeros(n - 1, dtype=numpy.complex128) 636 | 637 | eps = 1e-20 638 | # The coefficients of p'(z) and p''(z) 639 | for i in range(n): 640 | az[i] = (i + 1) * a[i + 1] 641 | for i in range(n - 1): 642 | azz[i] = (i + 1) * az[i + 1] 643 | 644 | _iter = 0 645 | jter = 0 # Missing from original code - assume this is correct 646 | dz = numpy.inf 647 | 648 | while (numpy.abs(dz) > err) and (_iter < nter - 1): 649 | p = a[n - 1] + a[n] * z 650 | for i in range(n - 2, -1, -1): 651 | p = a[i] + z * p 652 | if numpy.abs(p) < eps: 653 | return a, z, err 654 | 655 | pz = az[n - 2] + az[n - 1] * z 656 | for i in range(n - 3, -1, -1): 657 | pz = az[i] + z * pz 658 | 659 | pzz = azz[n - 3] + azz[n - 2] * z 660 | for i in range(n - 4, -1, -1): 661 | pzz = azz[i] + z * pzz 662 | 663 | # The Laguerre perturbation 664 | f = pz / p 665 | g = f**2 - pzz / p 666 | h = numpy.sqrt((n - 1) * (n * g - f**2)) 667 | amp1 = numpy.abs(f + h) 668 | amp2 = numpy.abs(f - h) 669 | if amp1 > amp2: 670 | dz = -n / (f + h) 671 | else: 672 | dz = -n / (f - h) 673 | 674 | _iter += 1 675 | 676 | # Rotate by 90 degrees to avoid limit cycles 677 | 678 | jter += 1 679 | if jter == 9: 680 | jter = 0 681 | dz *= 1j 682 | z += dz 683 | 684 | if _iter == 100: 685 | raise ValueError('Laguerre method not converging. Try a different combination of DR and NP.') 686 | 687 | return a, z, err 688 | -------------------------------------------------------------------------------- /pyram/PyRAMmp.py: -------------------------------------------------------------------------------- 1 | """ 2 | PyRAMmp class definition 3 | """ 4 | 5 | from multiprocessing.pool import Pool 6 | from pyram.PyRAM import PyRAM 7 | from time import sleep 8 | 9 | 10 | def run_pyram(run): 11 | 12 | """ 13 | Add a new PyRAM run (needs to be a function rather than a class method) 14 | """ 15 | 16 | args, kwargs = run[0], run[1] 17 | 18 | freq = args['freq'] 19 | zs = args['zs'] 20 | zr = args['zr'] 21 | z_ss = args['z_ss'] 22 | rp_ss = args['rp_ss'] 23 | cw = args['cw'] 24 | z_sb = args['z_sb'] 25 | rp_sb = args['rp_sb'] 26 | cb = args['cb'] 27 | rhob = args['rhob'] 28 | attn = args['attn'] 29 | rbzb = args['rbzb'] 30 | 31 | pyram = PyRAM(freq, zs, zr, z_ss, rp_ss, cw, z_sb, rp_sb, cb, rhob, 32 | attn, rbzb, **kwargs) 33 | results = pyram.run() 34 | 35 | return results 36 | 37 | 38 | class PyRAMmp: 39 | 40 | """ 41 | The PyRAMmp class sets up and runs a multiprocessing pool to enable 42 | parallel PyRAM model runs 43 | """ 44 | 45 | def __init__(self, processes=None, maxtasksperchild=None): 46 | 47 | """ 48 | Initialise the pool and variable lists 49 | processes and maxtasksperchild are passed to the pool 50 | """ 51 | 52 | self.pool = Pool(processes=processes, maxtasksperchild=maxtasksperchild) 53 | self.results = [] # Results from PyRAM.run() 54 | self._outputs = [] # New outputs from PyRAM.run() for transfer to self.results 55 | self._waiting = [] # Waiting runs 56 | self._num_processes = len(self.pool.__dict__['_pool']) 57 | self._num_waiting = 0 # Number of waiting runs 58 | self._num_active = 0 # Number of active runs 59 | self._sleep_time = 1e-2 # Minimum sleep time between adding runs to pool 60 | self._new = True # Flag to indicate ready for new set of runs 61 | 62 | def submit_runs(self, runs): 63 | 64 | """ 65 | Submit new runs to the pool as resources become available 66 | runs is a list of PyRAM input tuples (args, kwargs) 67 | """ 68 | 69 | # Add to waiting list 70 | for run in runs: 71 | self._waiting.append(run) 72 | self._num_waiting = len(self._waiting) 73 | 74 | # Check how many active runs have finished 75 | for _ in range(len(self._outputs)): 76 | run = self._outputs.pop(0) 77 | self.results.append(run) 78 | self._num_active -= 1 79 | 80 | num_start = self._num_processes - self._num_active 81 | num_start = min(num_start, self._num_waiting) 82 | 83 | # Start new runs if processes are free 84 | for _ in range(num_start): 85 | run = self._waiting.pop(0) 86 | self.pool.apply_async(run_pyram, args=(run,), callback=self._get_output) 87 | self._num_active += 1 88 | 89 | if self._new: 90 | self._new = False 91 | self._wait() 92 | 93 | def _wait(self): 94 | 95 | """ 96 | Wait for all submitted runs to complete 97 | """ 98 | 99 | while self._num_active > 0: 100 | self.submit_runs([]) 101 | sleep(self._sleep_time) 102 | 103 | self._new = True 104 | 105 | def close(self): 106 | 107 | """ 108 | Close the pool and wait for all processes to finish 109 | """ 110 | 111 | self.pool.close() 112 | self.pool.join() 113 | 114 | def _get_output(self, output): 115 | 116 | """ 117 | Get a PyRAM output 118 | """ 119 | 120 | self._outputs.append(output) 121 | 122 | def __del__(self): 123 | 124 | self.close() 125 | -------------------------------------------------------------------------------- /pyram/Tests/TestPyRAM.py: -------------------------------------------------------------------------------- 1 | '''TestPyRAM class definition''' 2 | 3 | import unittest 4 | import numpy 5 | from pyram.PyRAM import PyRAM 6 | 7 | 8 | class TestPyRAM(unittest.TestCase): 9 | 10 | ''' 11 | Test PyRAM using the test case supplied with RAM. 12 | ''' 13 | 14 | def setUp(self): 15 | 16 | self.inputs = dict(freq=50, 17 | zs=50, 18 | zr=50, 19 | z_ss=numpy.array([0, 100, 400]), 20 | rp_ss=numpy.array([0, 25000]), 21 | cw=numpy.array([[1480, 1530], 22 | [1520, 1530], 23 | [1530, 1530]]), 24 | z_sb=numpy.array([0]), 25 | rp_sb=numpy.array([0]), 26 | cb=numpy.array([[1700]]), 27 | rhob=numpy.array([[1.5]]), 28 | attn=numpy.array([[0.5]]), 29 | rmax=50000, 30 | dr=500, 31 | dz=2, 32 | zmplt=500, 33 | c0=1600, 34 | rbzb=numpy.array([[0, 200], 35 | [40000, 400]])) 36 | 37 | ref_tl_file = 'tl_ref.line' 38 | dat = numpy.fromfile(ref_tl_file, sep='\t').reshape([100, 2]) 39 | self.ref_r, self.ref_tl = dat[:, 0], dat[:, 1] 40 | 41 | self.tl_tol = 1e-2 # Tolerable mean difference in TL (dB) with reference result 42 | 43 | def tearDown(self): 44 | pass 45 | 46 | def test_PyRAM(self): 47 | 48 | pyram = PyRAM(self.inputs['freq'], self.inputs['zs'], self.inputs['zr'], 49 | self.inputs['z_ss'], self.inputs['rp_ss'], self.inputs['cw'], 50 | self.inputs['z_sb'], self.inputs['rp_sb'], self.inputs['cb'], 51 | self.inputs['rhob'], self.inputs['attn'], self.inputs['rbzb'], 52 | rmax=self.inputs['rmax'], dr=self.inputs['dr'], 53 | dz=self.inputs['dz'], zmplt=self.inputs['zmplt'], 54 | c0=self.inputs['c0']) 55 | pyram.run() 56 | 57 | with open('tl.line', 'w') as fid: 58 | for ran in range(len(pyram.vr)): 59 | fid.write(str(pyram.vr[ran]) + '\t' + str(pyram.tll[ran]) + '\n') 60 | 61 | self.assertTrue(numpy.array_equal(self.ref_r, pyram.vr), 62 | 'Ranges are not equal') 63 | 64 | mean_diff = numpy.mean(numpy.abs(pyram.tll - self.ref_tl)) 65 | self.assertTrue(mean_diff <= self.tl_tol, 66 | 'Mean TL difference with reference result not within tolerance') 67 | 68 | if __name__ == "__main__": 69 | unittest.main() 70 | -------------------------------------------------------------------------------- /pyram/Tests/TestPyRAMmp.py: -------------------------------------------------------------------------------- 1 | ''' 2 | TestPyRAMmp unit test class. 3 | Uses configuration file TestPyRAMmp_Config.xml. 4 | Computational range and depth steps and number of repetitions are configurable. 5 | Number of PyRAM runs = number of frequencies * number of repetitions. 6 | Tests should always pass but speedup will depend upon computing environment. 7 | ''' 8 | 9 | import unittest 10 | import xml.etree.ElementTree as et 11 | from time import time 12 | from copy import deepcopy 13 | import numpy 14 | from pyram.PyRAMmp import PyRAMmp 15 | from pyram.PyRAM import PyRAM 16 | 17 | 18 | class TestPyRAMmp(unittest.TestCase): 19 | 20 | ''' 21 | Test PyRAMmp using the test case supplied with RAM and different frequencies. 22 | ''' 23 | 24 | def setUp(self): 25 | 26 | config_file = 'TestPyRAMmp_Config.xml' 27 | root = et.parse(config_file).getroot() 28 | 29 | for child in root: 30 | 31 | if child.tag == 'RangeStep': 32 | dr = float(child.text) 33 | if child.tag == 'DepthStep': 34 | dz = float(child.text) 35 | if child.tag == 'NumberOfRepetitions': 36 | self.nrep = int(child.text) 37 | 38 | self.pyram_args = dict(zs=50., 39 | zr=50., 40 | z_ss=numpy.array([0., 100, 400]), 41 | rp_ss=numpy.array([0., 25000]), 42 | cw=numpy.array([[1480., 1530], 43 | [1520, 1530], 44 | [1530, 1530]]), 45 | z_sb=numpy.array([0.]), 46 | rp_sb=numpy.array([0.]), 47 | cb=numpy.array([[1700.]]), 48 | rhob=numpy.array([[1.5]]), 49 | attn=numpy.array([[0.5]]), 50 | rbzb=numpy.array([[0., 200], 51 | [40000, 400]])) 52 | 53 | self.pyram_kwargs = dict(rmax=50000., 54 | dr=dr, 55 | dz=dz, 56 | zmplt=500., 57 | c0=1600.) 58 | 59 | self.freqs = [30., 40, 50, 60, 70] 60 | 61 | self.ref_r = [] 62 | self.ref_z = [] 63 | self.ref_tl = [] 64 | 65 | for fn in range(len(self.freqs)): 66 | 67 | pyram_args = deepcopy(self.pyram_args) 68 | pyram_kwargs = deepcopy(self.pyram_kwargs) 69 | pyram = PyRAM(self.freqs[fn], pyram_args['zs'], 70 | pyram_args['zr'], pyram_args['z_ss'], 71 | pyram_args['rp_ss'], pyram_args['cw'], 72 | pyram_args['z_sb'], pyram_args['rp_sb'], 73 | pyram_args['cb'], pyram_args['rhob'], 74 | pyram_args['attn'], pyram_args['rbzb'], 75 | **pyram_kwargs) 76 | 77 | results = pyram.run() 78 | 79 | self.ref_r.append(results['Ranges']) 80 | self.ref_z.append(results['Depths']) 81 | self.ref_tl.append(results['TL Grid']) 82 | 83 | def tearDown(self): 84 | pass 85 | 86 | def test_PyRAMmp(self): 87 | 88 | ''' 89 | Test that the results from PyRAMmp are the same as from PyRAM. Also measure the speedup. 90 | ''' 91 | 92 | freqs_rep = numpy.tile(self.freqs, self.nrep) 93 | num_runs = len(freqs_rep) 94 | 95 | print(num_runs, 'PyRAM runs set up, running...', ) 96 | 97 | runs = [] 98 | for n in range(num_runs): 99 | pyram_args = deepcopy(self.pyram_args) 100 | pyram_args['freq'] = freqs_rep[n] 101 | pyram_kwargs = deepcopy(self.pyram_kwargs) 102 | pyram_kwargs['id'] = n 103 | runs.append((pyram_args, pyram_kwargs)) 104 | 105 | pyram_mp = PyRAMmp() 106 | nproc = pyram_mp.pool._processes 107 | t0 = time() 108 | pyram_mp.submit_runs(runs[:int(num_runs / 2)]) # Submit in 2 batches 109 | pyram_mp.submit_runs(runs[int(num_runs / 2):]) 110 | self.elap_time = time() - t0 # Approximate value as process_time can't be used 111 | 112 | results = [None] * num_runs 113 | self.proc_time = 0 114 | for result in pyram_mp.results: 115 | rid = result['ID'] 116 | results[rid] = result 117 | self.proc_time += result['Proc Time'] 118 | 119 | pyram_mp.close() 120 | 121 | for n in range(num_runs): 122 | 123 | freq = runs[n][0]['freq'] 124 | ind = self.freqs.index(freq) 125 | 126 | self.assertTrue(numpy.array_equal(self.ref_r[ind], results[n]['Ranges']), 127 | 'Ranges are not equal') 128 | self.assertTrue(numpy.array_equal(self.ref_z[ind], results[n]['Depths']), 129 | 'Depths are not equal') 130 | self.assertTrue(numpy.array_equal(self.ref_tl[ind], results[n]['TL Grid']), 131 | 'Transmission Loss values are not equal') 132 | 133 | print('Finished.\n') 134 | speed_fact = 100 * (self.proc_time / nproc) / self.elap_time 135 | print('{0:.1f} % of expected speed up achieved'.format(speed_fact)) 136 | 137 | if __name__ == "__main__": 138 | unittest.main() 139 | -------------------------------------------------------------------------------- /pyram/Tests/TestPyRAMmp_Config.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 100 4 | 1 5 | 2 6 | 7 | -------------------------------------------------------------------------------- /pyram/Tests/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | 'TestPyRAM', 3 | 'TestPyRAMmp' 4 | ) 5 | -------------------------------------------------------------------------------- /pyram/Tests/tl_ref.line: -------------------------------------------------------------------------------- 1 | 500.000000 47.8255768 2 | 1000.00000 46.0639763 3 | 1500.00000 54.5050240 4 | 2000.00000 50.1185265 5 | 2500.00000 52.1906815 6 | 3000.00000 53.9146423 7 | 3500.00000 53.4301987 8 | 4000.00000 58.8855820 9 | 4500.00000 62.4989471 10 | 5000.00000 57.6978989 11 | 5500.00000 57.0024033 12 | 6000.00000 56.6590271 13 | 6500.00000 59.8674355 14 | 7000.00000 62.1349907 15 | 7500.00000 60.6837883 16 | 8000.00000 60.6316338 17 | 8500.00000 57.6343079 18 | 9000.00000 58.0431633 19 | 9500.00000 59.6980171 20 | 10000.0000 61.1298409 21 | 10500.0000 60.6316986 22 | 11000.0000 60.9820633 23 | 11500.0000 61.5458908 24 | 12000.0000 60.1396217 25 | 12500.0000 60.0928154 26 | 13000.0000 61.9511833 27 | 13500.0000 62.7051544 28 | 14000.0000 63.3505249 29 | 14500.0000 63.1355553 30 | 15000.0000 62.2680206 31 | 15500.0000 64.0360260 32 | 16000.0000 62.9261017 33 | 16500.0000 63.2995834 34 | 17000.0000 63.8443451 35 | 17500.0000 64.1903076 36 | 18000.0000 64.3844299 37 | 18500.0000 62.9718323 38 | 19000.0000 62.5730705 39 | 19500.0000 63.9531708 40 | 20000.0000 65.6262817 41 | 20500.0000 64.9994965 42 | 21000.0000 64.5905380 43 | 21500.0000 64.0982056 44 | 22000.0000 63.9384727 45 | 22500.0000 64.3860321 46 | 23000.0000 64.1728821 47 | 23500.0000 65.5992661 48 | 24000.0000 66.2025757 49 | 24500.0000 65.5044556 50 | 25000.0000 64.7319183 51 | 25500.0000 68.5664673 52 | 26000.0000 69.8527298 53 | 26500.0000 71.7956085 54 | 27000.0000 74.7734070 55 | 27500.0000 76.5996704 56 | 28000.0000 71.9443359 57 | 28500.0000 76.1911850 58 | 29000.0000 74.7039032 59 | 29500.0000 71.0120239 60 | 30000.0000 73.4981689 61 | 30500.0000 70.5856018 62 | 31000.0000 68.9404602 63 | 31500.0000 75.4808960 64 | 32000.0000 79.2427139 65 | 32500.0000 75.4186172 66 | 33000.0000 70.3052826 67 | 33500.0000 70.0831833 68 | 34000.0000 74.4449005 69 | 34500.0000 72.7634430 70 | 35000.0000 71.8493500 71 | 35500.0000 79.7465820 72 | 36000.0000 82.1018372 73 | 36500.0000 75.5283356 74 | 37000.0000 75.5387726 75 | 37500.0000 89.5793304 76 | 38000.0000 74.2580948 77 | 38500.0000 73.2210693 78 | 39000.0000 74.7040405 79 | 39500.0000 72.5313568 80 | 40000.0000 70.8003387 81 | 40500.0000 69.6179504 82 | 41000.0000 69.2131500 83 | 41500.0000 72.3162155 84 | 42000.0000 89.6843414 85 | 42500.0000 81.7134094 86 | 43000.0000 83.2498856 87 | 43500.0000 89.9458618 88 | 44000.0000 105.154518 89 | 44500.0000 89.4440689 90 | 45000.0000 86.8525314 91 | 45500.0000 79.5968323 92 | 46000.0000 75.1000443 93 | 46500.0000 74.0579834 94 | 47000.0000 74.0318909 95 | 47500.0000 75.4259109 96 | 48000.0000 74.6813507 97 | 48500.0000 77.4401321 98 | 49000.0000 74.1595764 99 | 49500.0000 72.6973877 100 | 50000.0000 76.9641800 101 | -------------------------------------------------------------------------------- /pyram/__init__.py: -------------------------------------------------------------------------------- 1 | __all__ = ( 2 | 'matrc', 3 | 'outpt', 4 | 'PyRAM', 5 | 'PyRAMmp', 6 | 'solve' 7 | ) 8 | -------------------------------------------------------------------------------- /pyram/matrc.py: -------------------------------------------------------------------------------- 1 | """ 2 | solve function definition 3 | """ 4 | 5 | from numba import jit, float64, int64, complex128 6 | 7 | 8 | @jit((float64, float64, int64, int64, int64, int64, float64[:], float64[:], 9 | float64[:], complex128[:], float64[:], float64[:], float64[:], 10 | complex128[:], float64[:], complex128[:, :], complex128[:, :], 11 | complex128[:, :], complex128[:, :], complex128[:, :], complex128[:, :], 12 | complex128[:], complex128[:]), nopython=True) 13 | def matrc(k0, dz, iz, jz, nz, np, f1, f2, f3, ksq, alpw, alpb, ksqw, ksqb, 14 | rhob, r1, r2, r3, s1, s2, s3, pd1, pd2): 15 | 16 | """ 17 | The tridiagonal matrices 18 | """ 19 | 20 | a1 = k0**2 / 6 21 | a2 = 2 * k0**2 / 3 22 | a3 = a1 23 | cfact = 0.5 / dz**2 24 | dfact = 1 / 12 25 | 26 | # New matrices when iz == jz 27 | if iz == jz: 28 | i1 = 1 29 | i2 = nz 30 | for i in range(iz + 1): 31 | f1[i] = 1 / alpw[i] 32 | f2[i] = 1 33 | f3[i] = alpw[i] 34 | ksq[i] = ksqw[i] 35 | for i in range(iz + 1, nz + 2): 36 | f1[i] = rhob[i] / alpb[i] 37 | f2[i] = 1 / rhob[i] 38 | f3[i] = alpb[i] 39 | ksq[i] = ksqb[i] 40 | # Updated matrices when iz != jz 41 | elif iz > jz: 42 | i1 = jz 43 | i2 = iz + 1 44 | for i in range(jz + 1, iz + 1): 45 | f1[i] = 1 / alpw[i] 46 | f2[i] = 1 47 | f3[i] = alpw[i] 48 | ksq[i] = ksqw[i] 49 | else: 50 | i1 = iz 51 | i2 = jz + 1 52 | for i in range(iz + 1, jz + 1): 53 | f1[i] = rhob[i] / alpb[i] 54 | f2[i] = 1 / rhob[i] 55 | f3[i] = alpb[i] 56 | ksq[i] = ksqb[i] 57 | 58 | # Discretization by Galerkin's method 59 | 60 | for i in range(i1, i2 + 1): 61 | 62 | c1 = cfact * f1[i] * (f2[i - 1] + f2[i]) * f3[i - 1] 63 | c2 = -cfact * f1[i] * (f2[i - 1] + 2 * f2[i] + f2[i + 1]) * f3[i] 64 | c3 = cfact * f1[i] * (f2[i] + f2[i + 1]) * f3[i + 1] 65 | d1 = c1 + dfact * (ksq[i - 1] + ksq[i]) 66 | d2 = c2 + dfact * (ksq[i - 1] + 6 * ksq[i] + ksq[i + 1]) 67 | d3 = c3 + dfact * (ksq[i] + ksq[i + 1]) 68 | 69 | for j in range(np): 70 | r1[i, j] = a1 + pd2[j] * d1 71 | r2[i, j] = a2 + pd2[j] * d2 72 | r3[i, j] = a3 + pd2[j] * d3 73 | s1[i, j] = a1 + pd1[j] * d1 74 | s2[i, j] = a2 + pd1[j] * d2 75 | s3[i, j] = a3 + pd1[j] * d3 76 | 77 | # The matrix decomposition 78 | for j in range(np): 79 | 80 | for i in range(i1, iz + 1): 81 | rfact = 1 / (r2[i, j] - r1[i, j] * r3[i - 1, j]) 82 | r1[i, j] *= rfact 83 | r3[i, j] *= rfact 84 | s1[i, j] *= rfact 85 | s2[i, j] *= rfact 86 | s3[i, j] *= rfact 87 | 88 | for i in range(i2, iz + 1, -1): 89 | rfact = 1 / (r2[i, j] - r3[i, j] * r1[i + 1, j]) 90 | r1[i, j] *= rfact 91 | r3[i, j] *= rfact 92 | s1[i, j] *= rfact 93 | s2[i, j] *= rfact 94 | s3[i, j] *= rfact 95 | 96 | r2[iz + 1, j] -= r1[iz + 1, j] * r3[iz, j] 97 | r2[iz + 1, j] -= r3[iz + 1, j] * r1[iz + 2, j] 98 | r2[iz + 1, j] = 1 / r2[iz + 1, j] 99 | -------------------------------------------------------------------------------- /pyram/outpt.py: -------------------------------------------------------------------------------- 1 | """ 2 | outpt function definition 3 | """ 4 | 5 | import numpy 6 | from numba import jit, int64, float64, complex128 7 | 8 | 9 | @jit(int64[:](float64, int64, int64, int64, int64, float64[:], 10 | complex128[:], float64, int64, float64[:], float64[:, :], 11 | complex128[:], complex128[:, :]), nopython=True) 12 | def outpt(r, mdr, ndr, ndz, tlc, f3, u, _dir, ir, tll, tlg, cpl, cpg): 13 | 14 | """ 15 | Output transmission loss and complex pressure. 16 | Complex pressure does not include cylindrical spreading term 1/sqrt(r) 17 | or phase term exp(-j*k0*r). 18 | """ 19 | 20 | eps = 1e-20 21 | 22 | mdr += 1 23 | if mdr == ndr: 24 | mdr = 0 25 | tlc += 1 26 | cpl[tlc] = (1 - _dir) * f3[ir] * u[ir] + \ 27 | _dir * f3[ir + 1] * u[ir + 1] 28 | temp = 10 * numpy.log10(r + eps) 29 | tll[tlc] = -20 * numpy.log10(numpy.abs(cpl[tlc]) + eps) + temp 30 | 31 | for i in range(tlg.shape[0]): 32 | j = (i + 1) * ndz 33 | cpg[i, tlc] = u[j] * f3[j] 34 | tlg[i, tlc] = \ 35 | -20 * numpy.log10(numpy.abs(cpg[i, tlc]) + eps) + temp 36 | 37 | return numpy.array([mdr, tlc], dtype=numpy.int64) 38 | -------------------------------------------------------------------------------- /pyram/solve.py: -------------------------------------------------------------------------------- 1 | """ 2 | solve function definition 3 | """ 4 | 5 | from numba import jit, int64, complex128 6 | 7 | 8 | @jit((complex128[:], complex128[:], complex128[:, :], complex128[:, :], 9 | complex128[:, :], complex128[:, :], complex128[:, :], complex128[:, :], 10 | int64, int64, int64), nopython=True) 11 | def solve(u, v, s1, s2, s3, r1, r2, r3, iz, nz, np): 12 | 13 | """ 14 | The tridiagonal solver 15 | """ 16 | 17 | eps = 1e-30 18 | 19 | for j in range(np): 20 | # The right side 21 | for i in range(1, nz + 1): 22 | v[i] = s1[i, j] * u[i - 1] + s2[i, j] * u[i] + s3[i, j] * u[i + 1] + eps 23 | 24 | # The elimination steps 25 | for i in range(2, iz + 1): 26 | v[i] -= r1[i, j] * v[i - 1] + eps 27 | for i in range(nz - 1, iz + 1, -1): 28 | v[i] -= r3[i, j] * v[i + 1] + eps 29 | 30 | u[iz + 1] = (v[iz + 1] - r1[iz + 1, j] * v[iz] - r3[iz + 1, j] * v[iz + 2]) * \ 31 | r2[iz + 1, j] + eps 32 | 33 | # The back substitution steps 34 | for i in range(iz, -1, -1): 35 | u[i] = v[i] - r3[i, j] * u[i + 1] + eps 36 | for i in range(iz + 2, nz + 1): 37 | u[i] = v[i] - r1[i, j] * u[i - 1] + eps 38 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup, find_packages 2 | 3 | setup(name='pyram', 4 | version='1.3.0', 5 | description='Python adaptation of the Range-dependent Acoustic Model (RAM)', 6 | author='Marcus Donnelly', 7 | author_email='marcus.k.donnelly@gmail.com', 8 | url='https://github.com/marcuskd/pyram', 9 | license='BSD', 10 | classifiers=['Development Status :: 5 - Production/Stable', 11 | 'Intended Audience :: Science/Research', 12 | 'Intended Audience :: Developers', 13 | 'License :: OSI Approved :: BSD License', 14 | 'Programming Language :: Python :: 3', 15 | 'Topic :: Scientific/Engineering' 16 | ], 17 | keywords=['RAM', 18 | 'Acoustics', 19 | 'Parabolic Equation' 20 | ], 21 | packages=find_packages(), 22 | install_requires=['numpy >= 1.25', 23 | 'numba >= 0.58' 24 | ], 25 | include_package_data=True 26 | ) 27 | --------------------------------------------------------------------------------