easypyram 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- easypyram/PyRAM.py +1178 -0
- easypyram/PyRAMmp.py +149 -0
- easypyram/Tests/TestPyRAMmp_Config.xml +6 -0
- easypyram/Tests/__init__.py +1 -0
- easypyram/Tests/test_pyram.py +57 -0
- easypyram/Tests/test_pyrammp.py +154 -0
- easypyram/Tests/tl_ref.line +100 -0
- easypyram/__init__.py +20 -0
- easypyram/examples.py +176 -0
- easypyram/matrc.py +143 -0
- easypyram/outpt.py +65 -0
- easypyram/solve.py +62 -0
- easypyram-2.0.0.dist-info/METADATA +250 -0
- easypyram-2.0.0.dist-info/RECORD +17 -0
- easypyram-2.0.0.dist-info/WHEEL +4 -0
- easypyram-2.0.0.dist-info/entry_points.txt +4 -0
- easypyram-2.0.0.dist-info/licenses/LICENSE.md +31 -0
easypyram/PyRAM.py
ADDED
|
@@ -0,0 +1,1178 @@
|
|
|
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 warnings
|
|
29
|
+
from time import process_time
|
|
30
|
+
from typing import Any, NamedTuple
|
|
31
|
+
|
|
32
|
+
import numpy as np
|
|
33
|
+
from numpy.typing import ArrayLike, NDArray
|
|
34
|
+
|
|
35
|
+
from easypyram.matrc import matrc
|
|
36
|
+
from easypyram.outpt import outpt
|
|
37
|
+
from easypyram.solve import solve
|
|
38
|
+
|
|
39
|
+
__all__ = (
|
|
40
|
+
"arctic_profile",
|
|
41
|
+
"munk_profile",
|
|
42
|
+
"PyRAM",
|
|
43
|
+
"PyRAMResults",
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class PyRAMResults(NamedTuple):
|
|
48
|
+
"""
|
|
49
|
+
Results returned by PyRAM.run().
|
|
50
|
+
|
|
51
|
+
Fields
|
|
52
|
+
------
|
|
53
|
+
ranges : ndarray
|
|
54
|
+
Calculation ranges [m].
|
|
55
|
+
depths : ndarray
|
|
56
|
+
Calculation depths [m].
|
|
57
|
+
loss_grid : ndarray
|
|
58
|
+
Transmission loss [dB] over depth and range.
|
|
59
|
+
loss_line : ndarray
|
|
60
|
+
Transmission loss [dB] at receiver depth.
|
|
61
|
+
pressure_grid : ndarray
|
|
62
|
+
Complex pressure field.
|
|
63
|
+
pressure_line : ndarray
|
|
64
|
+
Complex pressure at receiver depth.
|
|
65
|
+
c0 : float
|
|
66
|
+
Reference sound speed [m/s].
|
|
67
|
+
proc_time : float
|
|
68
|
+
Processing time [s].
|
|
69
|
+
id : int
|
|
70
|
+
Run identifier.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
ranges: NDArray[np.float64]
|
|
74
|
+
depths: NDArray[np.float64]
|
|
75
|
+
loss_grid: NDArray[np.float64]
|
|
76
|
+
loss_line: NDArray[np.float64]
|
|
77
|
+
pressure_grid: NDArray[np.complex128]
|
|
78
|
+
pressure_line: NDArray[np.complex128]
|
|
79
|
+
c0: float
|
|
80
|
+
proc_time: float
|
|
81
|
+
id: int
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def arctic_profile(
|
|
85
|
+
z: ArrayLike,
|
|
86
|
+
c0: float = 1440.0,
|
|
87
|
+
gradient: float = 0.02,
|
|
88
|
+
) -> NDArray[np.float64]:
|
|
89
|
+
"""
|
|
90
|
+
Simple Arctic sound-speed profile.
|
|
91
|
+
|
|
92
|
+
Parameters
|
|
93
|
+
----------
|
|
94
|
+
z : array_like
|
|
95
|
+
Depth [m], positive downward.
|
|
96
|
+
c0 : float
|
|
97
|
+
Surface sound speed [m/s].
|
|
98
|
+
gradient : float
|
|
99
|
+
Vertical sound-speed gradient [m/s/m].
|
|
100
|
+
gradient = 0.02 corresponds to an increase of
|
|
101
|
+
approximately 20 m/s per km depth.
|
|
102
|
+
|
|
103
|
+
Returns
|
|
104
|
+
-------
|
|
105
|
+
ndarray
|
|
106
|
+
Sound speed [m/s].
|
|
107
|
+
|
|
108
|
+
Notes
|
|
109
|
+
-----
|
|
110
|
+
This is not a standard published sound-speed profile.
|
|
111
|
+
It is a simple first-order approximation motivated by the
|
|
112
|
+
approximately monotonic increase of sound speed with depth
|
|
113
|
+
observed in many Arctic and Antarctic water columns.
|
|
114
|
+
|
|
115
|
+
References
|
|
116
|
+
----------
|
|
117
|
+
Munk, W., Worcester, P., and Wunsch, C.
|
|
118
|
+
Ocean Acoustic Tomography.
|
|
119
|
+
Cambridge University Press, 1995.
|
|
120
|
+
"""
|
|
121
|
+
if c0 <= 0:
|
|
122
|
+
raise ValueError("c0 must be positive")
|
|
123
|
+
if gradient < 0:
|
|
124
|
+
warnings.warn(
|
|
125
|
+
"Negative gradient produces decreasing sound speed with depth.",
|
|
126
|
+
stacklevel=2,
|
|
127
|
+
)
|
|
128
|
+
z = np.asarray(z, dtype=float)
|
|
129
|
+
|
|
130
|
+
return np.asarray(c0 + gradient * z, dtype=np.float64)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def munk_profile(
|
|
134
|
+
z: ArrayLike,
|
|
135
|
+
c0: float = 1500.0,
|
|
136
|
+
epsilon: float = 0.00737,
|
|
137
|
+
z_axis: float = 1300.0,
|
|
138
|
+
scale_depth: float = 1300.0,
|
|
139
|
+
) -> NDArray[np.float64]:
|
|
140
|
+
"""
|
|
141
|
+
Canonical Munk sound-speed profile.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
z : array_like
|
|
146
|
+
Depth [m], positive downward.
|
|
147
|
+
c0 : float, optional
|
|
148
|
+
Reference sound speed [m/s].
|
|
149
|
+
epsilon : float, optional
|
|
150
|
+
Dimensionless profile-strength parameter.
|
|
151
|
+
z_axis : float, optional
|
|
152
|
+
Sound-channel axis depth [m].
|
|
153
|
+
scale_depth : float, optional
|
|
154
|
+
Scale depth [m].
|
|
155
|
+
|
|
156
|
+
Returns
|
|
157
|
+
-------
|
|
158
|
+
ndarray
|
|
159
|
+
Sound speed [m/s].
|
|
160
|
+
|
|
161
|
+
Notes
|
|
162
|
+
-----
|
|
163
|
+
The Munk profile is defined as
|
|
164
|
+
|
|
165
|
+
c(z) = c0 * [1 + ε * (η + exp(-η) - 1)]
|
|
166
|
+
|
|
167
|
+
where
|
|
168
|
+
|
|
169
|
+
η = 2 * (z - z_axis) / scale_depth.
|
|
170
|
+
|
|
171
|
+
References
|
|
172
|
+
----------
|
|
173
|
+
Munk, W.
|
|
174
|
+
"Sound channel in an exponentially stratified ocean,
|
|
175
|
+
with application to SOFAR."
|
|
176
|
+
Journal of the Acoustical Society of America,
|
|
177
|
+
55(2), 1974.
|
|
178
|
+
|
|
179
|
+
Munk, W., Worcester, P., and Wunsch, C.
|
|
180
|
+
Ocean Acoustic Tomography.
|
|
181
|
+
Cambridge University Press, 1995.
|
|
182
|
+
"""
|
|
183
|
+
if scale_depth <= 0:
|
|
184
|
+
raise ValueError("scale_depth must be positive")
|
|
185
|
+
if c0 <= 0:
|
|
186
|
+
raise ValueError("c0 must be positive")
|
|
187
|
+
if z_axis < 0:
|
|
188
|
+
raise ValueError("z_axis must be non-negative")
|
|
189
|
+
if epsilon < 0:
|
|
190
|
+
warnings.warn(
|
|
191
|
+
"Negative epsilon produces an inverted Munk profile.",
|
|
192
|
+
stacklevel=2,
|
|
193
|
+
)
|
|
194
|
+
|
|
195
|
+
z = np.asarray(z, dtype=float)
|
|
196
|
+
|
|
197
|
+
eta = 2.0 * (z - z_axis) / scale_depth
|
|
198
|
+
|
|
199
|
+
return np.asarray(c0 * (1.0 + epsilon * (eta + np.expm1(-eta))), dtype=np.float64)
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
class PyRAM:
|
|
203
|
+
"""
|
|
204
|
+
Range-dependent Acoustic Model (RAM)
|
|
205
|
+
|
|
206
|
+
Parameters
|
|
207
|
+
----------
|
|
208
|
+
freq : float
|
|
209
|
+
Acoustic frequency [Hz].
|
|
210
|
+
zs : float
|
|
211
|
+
Source depth [m].
|
|
212
|
+
zr : float
|
|
213
|
+
Receiver depth [m].
|
|
214
|
+
z_ss : ndarray
|
|
215
|
+
Depths [m] corresponding to water sound-speed values.
|
|
216
|
+
rp_ss : ndarray
|
|
217
|
+
Ranges [m] corresponding to water sound-speed values.
|
|
218
|
+
cw : ndarray
|
|
219
|
+
Water sound-speed values [m/s], with shape
|
|
220
|
+
``(z_ss.size, rp_ss.size)``.
|
|
221
|
+
z_sb : ndarray
|
|
222
|
+
Depths [m] corresponding to seabed property values.
|
|
223
|
+
rp_sb : ndarray
|
|
224
|
+
Ranges [m] corresponding to seabed property values.
|
|
225
|
+
cb : ndarray
|
|
226
|
+
Seabed sound-speed values [m/s], with shape
|
|
227
|
+
``(z_sb.size, rp_sb.size)``.
|
|
228
|
+
rhob : ndarray
|
|
229
|
+
Seabed density values [g/cm³], with the same shape as
|
|
230
|
+
``cb``.
|
|
231
|
+
attn : ndarray
|
|
232
|
+
Seabed attenuation values [dB/wavelength], with the same
|
|
233
|
+
shape as ``cb``.
|
|
234
|
+
rbzb : ndarray
|
|
235
|
+
Bathymetry array [m], with columns containing range and
|
|
236
|
+
depth pairs.
|
|
237
|
+
|
|
238
|
+
Other Parameters
|
|
239
|
+
----------------
|
|
240
|
+
np : int, optional
|
|
241
|
+
Number of Padé approximation terms. Defaults to ``_np_default`` (8).
|
|
242
|
+
c0 : float, optional
|
|
243
|
+
Reference sound speed [m/s]. Defaults to the mean of the
|
|
244
|
+
first water sound-speed profile.
|
|
245
|
+
dr : float, optional
|
|
246
|
+
Calculation range step [m]. Defaults to ``0.5`` times the
|
|
247
|
+
acoustic wavelength.
|
|
248
|
+
dz : float, optional
|
|
249
|
+
Calculation depth step [m]. Defaults to
|
|
250
|
+
``0.05`` times the acoustic wavelength.
|
|
251
|
+
ndr : int, optional
|
|
252
|
+
Number of range steps between outputs. Defaults to
|
|
253
|
+
``_ndr_default`` (1).
|
|
254
|
+
ndz : int, optional
|
|
255
|
+
Number of depth steps between outputs. Defaults to
|
|
256
|
+
``_ndz_default`` (1).
|
|
257
|
+
zmplt : float, optional
|
|
258
|
+
Maximum output depth [m]. Defaults to the maximum depth in
|
|
259
|
+
``rbzb``.
|
|
260
|
+
rmax : float, optional
|
|
261
|
+
Maximum calculation range [m]. Defaults to the maximum
|
|
262
|
+
range in ``rp_ss``, ``rp_sb``, or ``rbzb``.
|
|
263
|
+
ns : int, optional
|
|
264
|
+
Number of stability constraints. Defaults to
|
|
265
|
+
``_ns_default`` (1).
|
|
266
|
+
rs : float, optional
|
|
267
|
+
Maximum range [m] over which stability constraints are
|
|
268
|
+
applied. Defaults to ``rmax``.
|
|
269
|
+
lyrw : float, optional
|
|
270
|
+
Width of the absorbing layer [wavelengths]. Defaults to
|
|
271
|
+
``_lyrw_default`` (20).
|
|
272
|
+
id : int, optional
|
|
273
|
+
Integer identifier for the model instance. Defaults to
|
|
274
|
+
``_id_default`` (0).
|
|
275
|
+
|
|
276
|
+
Notes
|
|
277
|
+
-----
|
|
278
|
+
RAM is intended primarily for low-frequency acoustic propagation
|
|
279
|
+
(typically below approximately 500 Hz) in range-dependent environments
|
|
280
|
+
consisting of fluid layers and neglecting seabed shear waves.
|
|
281
|
+
|
|
282
|
+
The numerical accuracy is primarily controlled by ``np``, ``dr``, and ``dz``.
|
|
283
|
+
Increasing the number of Padé terms generally improves accuracy at
|
|
284
|
+
the expense of increased computational cost. The number of stability
|
|
285
|
+
constraints (``ns``) can be increased if the solution exhibits
|
|
286
|
+
numerical instability for any combination of ``np``, ``dr`` and ``dz``.
|
|
287
|
+
|
|
288
|
+
General rules for using RAM:
|
|
289
|
+
|
|
290
|
+
As a rule of thumb use:
|
|
291
|
+
|
|
292
|
+
``dr`` <= 0.66 * wavelength (1)
|
|
293
|
+
``dz`` <= 0.066 * wavelength (2)
|
|
294
|
+
|
|
295
|
+
The default values of ``dr`` and ``dz`` satisfy these recommendations.
|
|
296
|
+
The allowable range-step size is limited by the degree of range
|
|
297
|
+
dependence in the environment. The RAM PE solver permits arbitrarily
|
|
298
|
+
large range steps of many wavelengths for range-independent regions
|
|
299
|
+
and dense range sampling. For an environment with small range variations,
|
|
300
|
+
a solution computed using the range-step specifications defined by
|
|
301
|
+
Eq. (1) and sampled on a fine scale, is barely distinguishable from a
|
|
302
|
+
solution computed with large range steps (``dr = 50 * wavelength``),
|
|
303
|
+
although sampled more sparsely. Calculations with a range step size that
|
|
304
|
+
satisfies Eq. (1) are computationally inefficient for environments with
|
|
305
|
+
small variations, but will handle a greater range of environments without
|
|
306
|
+
the hassle of having to evaluate the rate of range dependency and can
|
|
307
|
+
provide a solution that is sampled on a fine scale.
|
|
308
|
+
|
|
309
|
+
Occasionally you will see output that looks like the computation stopped
|
|
310
|
+
halfway or the data will be blank. This likely means there was an
|
|
311
|
+
instability in the calculation. The usual remedy is to vary ``dr`` until
|
|
312
|
+
a stable solution is obtained. If varying ``dr`` does not eliminate
|
|
313
|
+
the instability, try increasing ``ns`` above 1.
|
|
314
|
+
|
|
315
|
+
It is good to refine your grid (make ``dr`` and ``dz`` smaller) and increase
|
|
316
|
+
the number of Padé terms to ensure that the solution has converged.
|
|
317
|
+
Experience will guide you in deciding numerical parameters given a frequency
|
|
318
|
+
and geoacoustic environment.
|
|
319
|
+
|
|
320
|
+
The RAM model approximates a semi-infinite bottom half-space by
|
|
321
|
+
appending an artificial high-attenuation layer, also called a sponge layer,
|
|
322
|
+
at the lower boundary of the physical domain in order to prevent spurious
|
|
323
|
+
reflections from the lower computational boundary. The thickness of this
|
|
324
|
+
layer is controlled by ``lyrw``, which defaults to 20 wavelengths.
|
|
325
|
+
|
|
326
|
+
References
|
|
327
|
+
----------
|
|
328
|
+
[1] M. D. Collins (1993), "A split-step Padé solution for the parabolic equation method",
|
|
329
|
+
J. Acoust. Soc. Am., vol. 93, pp. 1736-1742
|
|
330
|
+
[2] M. D. Collins (2015), "User's Guide for RAM Version 1.0 and 1.0p",
|
|
331
|
+
Naval Research Laboratory.
|
|
332
|
+
[Online]. Available:
|
|
333
|
+
http://staff.washington.edu/dushaw/AcousticsCode/ram.pdf
|
|
334
|
+
[3] D. Calvo (2006), "Quick introduction to using the Naval Research Laboratory RAM parabolic
|
|
335
|
+
equation (PE) code that includes bottom loss",
|
|
336
|
+
Naval Research Laboratory.
|
|
337
|
+
[Online]. Available:
|
|
338
|
+
http://oalib.hlsresearch.com/PE/ramsurf/readme.orig
|
|
339
|
+
[4] M. D. Collins (1994), "Generalization of the split-step Padé solution",
|
|
340
|
+
J. Acoust. Soc. Am., vol. 96, pp. 382-385
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
_np_default = 8
|
|
344
|
+
_ndr_default = 1
|
|
345
|
+
_ndz_default = 1
|
|
346
|
+
_ns_default = 1
|
|
347
|
+
_lyrw_default = 20.0
|
|
348
|
+
_id_default = 0
|
|
349
|
+
|
|
350
|
+
def __init__(
|
|
351
|
+
self,
|
|
352
|
+
freq: float | int,
|
|
353
|
+
zs: float | int,
|
|
354
|
+
zr: float | int,
|
|
355
|
+
z_ss: ArrayLike,
|
|
356
|
+
rp_ss: ArrayLike,
|
|
357
|
+
cw: ArrayLike,
|
|
358
|
+
z_sb: ArrayLike,
|
|
359
|
+
rp_sb: ArrayLike,
|
|
360
|
+
cb: ArrayLike,
|
|
361
|
+
rhob: ArrayLike,
|
|
362
|
+
attn: ArrayLike,
|
|
363
|
+
rbzb: ArrayLike,
|
|
364
|
+
**kwargs: Any,
|
|
365
|
+
) -> None:
|
|
366
|
+
self._freq: float = float(freq)
|
|
367
|
+
self._zs: float = float(zs)
|
|
368
|
+
self._zr: float = float(zr)
|
|
369
|
+
|
|
370
|
+
# Store copies to avoid modifying caller-owned arrays
|
|
371
|
+
self._z_ss = np.array(z_ss, dtype=float, copy=True)
|
|
372
|
+
self._rp_ss = np.array(rp_ss, dtype=float, copy=True)
|
|
373
|
+
self._cw = np.array(cw, dtype=float, copy=True)
|
|
374
|
+
|
|
375
|
+
self._z_sb = np.array(z_sb, dtype=float, copy=True)
|
|
376
|
+
self._rp_sb = np.array(rp_sb, dtype=float, copy=True)
|
|
377
|
+
self._cb = np.array(cb, dtype=float, copy=True)
|
|
378
|
+
self._rhob = np.array(rhob, dtype=float, copy=True)
|
|
379
|
+
self._attn = np.array(attn, dtype=float, copy=True)
|
|
380
|
+
|
|
381
|
+
self._rbzb = np.array(rbzb, dtype=float, copy=True)
|
|
382
|
+
self._validate_inputs()
|
|
383
|
+
|
|
384
|
+
# Range-dependence flags
|
|
385
|
+
self.rd_ss = self._rp_ss.size > 1
|
|
386
|
+
self.rd_sb = self._rp_sb.size > 1
|
|
387
|
+
self.rd_bt = self._rbzb.shape[0] > 1
|
|
388
|
+
|
|
389
|
+
self.proc_time: float | None = None
|
|
390
|
+
|
|
391
|
+
# Work variables defined in setup
|
|
392
|
+
self.u: NDArray[np.complex128]
|
|
393
|
+
self.v: NDArray[np.complex128]
|
|
394
|
+
|
|
395
|
+
self.tll: NDArray[np.float64]
|
|
396
|
+
self.tlg: NDArray[np.float64]
|
|
397
|
+
|
|
398
|
+
self.cpl: NDArray[np.complex128]
|
|
399
|
+
self.cpg: NDArray[np.complex128]
|
|
400
|
+
|
|
401
|
+
self.s1: NDArray[np.complex128]
|
|
402
|
+
self.s2: NDArray[np.complex128]
|
|
403
|
+
self.s3: NDArray[np.complex128]
|
|
404
|
+
|
|
405
|
+
self.r1: NDArray[np.complex128]
|
|
406
|
+
self.r2: NDArray[np.complex128]
|
|
407
|
+
self.r3: NDArray[np.complex128]
|
|
408
|
+
|
|
409
|
+
self.vr: NDArray[np.float64]
|
|
410
|
+
self.vz: NDArray[np.float64]
|
|
411
|
+
|
|
412
|
+
self.iz: int
|
|
413
|
+
self.nz: int
|
|
414
|
+
self.ir: int
|
|
415
|
+
self.mdr: int
|
|
416
|
+
self.tlc: int
|
|
417
|
+
|
|
418
|
+
self.alpw: NDArray[np.float64]
|
|
419
|
+
self.alpb: NDArray[np.float64]
|
|
420
|
+
|
|
421
|
+
self.f1: NDArray[np.float64]
|
|
422
|
+
self.f2: NDArray[np.float64]
|
|
423
|
+
self.f3: NDArray[np.float64]
|
|
424
|
+
|
|
425
|
+
self.ksqw: NDArray[np.float64]
|
|
426
|
+
|
|
427
|
+
self.pd1: NDArray[np.complex128]
|
|
428
|
+
self.pd2: NDArray[np.complex128]
|
|
429
|
+
|
|
430
|
+
self.ksq: NDArray[np.complex128]
|
|
431
|
+
self.ksqb: NDArray[np.complex128]
|
|
432
|
+
|
|
433
|
+
self._initialize_params(**kwargs)
|
|
434
|
+
|
|
435
|
+
def run(self) -> PyRAMResults:
|
|
436
|
+
"""
|
|
437
|
+
Run the acoustic propagation model.
|
|
438
|
+
|
|
439
|
+
Returns
|
|
440
|
+
-------
|
|
441
|
+
PyRAMResults
|
|
442
|
+
Model output containing ranges, depths, transmission loss,
|
|
443
|
+
complex pressure, reference sound speed, processing time,
|
|
444
|
+
and run identifier.
|
|
445
|
+
"""
|
|
446
|
+
|
|
447
|
+
t0 = process_time()
|
|
448
|
+
|
|
449
|
+
self.setup()
|
|
450
|
+
|
|
451
|
+
nr = int(np.round(self._rmax / self._dr)) - 1
|
|
452
|
+
|
|
453
|
+
for rn in range(nr):
|
|
454
|
+
self.updat()
|
|
455
|
+
|
|
456
|
+
solve(
|
|
457
|
+
self.u,
|
|
458
|
+
self.v,
|
|
459
|
+
self.s1,
|
|
460
|
+
self.s2,
|
|
461
|
+
self.s3,
|
|
462
|
+
self.r1,
|
|
463
|
+
self.r2,
|
|
464
|
+
self.r3,
|
|
465
|
+
self.iz,
|
|
466
|
+
self.nz,
|
|
467
|
+
self._np,
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
self.r = (rn + 2) * self._dr
|
|
471
|
+
|
|
472
|
+
self.mdr, self.tlc = outpt(
|
|
473
|
+
self.r,
|
|
474
|
+
self.mdr,
|
|
475
|
+
self._ndr,
|
|
476
|
+
self._ndz,
|
|
477
|
+
self.tlc,
|
|
478
|
+
self.f3,
|
|
479
|
+
self.u,
|
|
480
|
+
self.dir,
|
|
481
|
+
self.ir,
|
|
482
|
+
self.tll,
|
|
483
|
+
self.tlg,
|
|
484
|
+
self.cpl,
|
|
485
|
+
self.cpg,
|
|
486
|
+
)[:]
|
|
487
|
+
|
|
488
|
+
proc_time = process_time() - t0
|
|
489
|
+
self.proc_time = proc_time
|
|
490
|
+
|
|
491
|
+
return PyRAMResults(
|
|
492
|
+
ranges=self.vr,
|
|
493
|
+
depths=self.vz,
|
|
494
|
+
loss_grid=self.tlg,
|
|
495
|
+
loss_line=self.tll,
|
|
496
|
+
pressure_grid=self.cpg,
|
|
497
|
+
pressure_line=self.cpl,
|
|
498
|
+
c0=self._c0,
|
|
499
|
+
proc_time=proc_time,
|
|
500
|
+
id=self._id,
|
|
501
|
+
)
|
|
502
|
+
|
|
503
|
+
def _validate_inputs(self) -> None:
|
|
504
|
+
"""Validate inputs."""
|
|
505
|
+
|
|
506
|
+
# Source and receiver depths
|
|
507
|
+
if not self._z_ss[0] <= self._zs <= self._z_ss[-1]:
|
|
508
|
+
raise ValueError("Source depth outside sound speed depths")
|
|
509
|
+
|
|
510
|
+
if not self._z_ss[0] <= self._zr <= self._z_ss[-1]:
|
|
511
|
+
raise ValueError("Receiver depth outside sound speed depths")
|
|
512
|
+
|
|
513
|
+
# Water sound-speed profiles
|
|
514
|
+
if self._cw.shape != (self._z_ss.size, self._rp_ss.size):
|
|
515
|
+
raise ValueError("Dimensions of z_ss, rp_ss, and cw must be consistent.")
|
|
516
|
+
|
|
517
|
+
# Seabed profiles
|
|
518
|
+
expected_shape = (self._z_sb.size, self._rp_sb.size)
|
|
519
|
+
|
|
520
|
+
for _name, profile in (
|
|
521
|
+
("cb", self._cb),
|
|
522
|
+
("rhob", self._rhob),
|
|
523
|
+
("attn", self._attn),
|
|
524
|
+
):
|
|
525
|
+
if profile.shape != expected_shape:
|
|
526
|
+
raise ValueError("Dimensions of z_sb, rp_sb, cb, rhob, and attn must be consistent.")
|
|
527
|
+
|
|
528
|
+
# Bathymetry
|
|
529
|
+
if self._rbzb[:, 1].max() > self._z_ss[-1]:
|
|
530
|
+
raise ValueError("Deepest sound speed point must be at or below deepest bathymetry point.")
|
|
531
|
+
|
|
532
|
+
def _initialize_params(self, **kwargs: Any) -> None:
|
|
533
|
+
"""Initialize model parameters from inputs and keyword arguments."""
|
|
534
|
+
|
|
535
|
+
self._np: int = int(kwargs.get("np", PyRAM._np_default))
|
|
536
|
+
|
|
537
|
+
c0 = np.mean(self._cw[:, 0]) if len(self._cw.shape) > 1 else np.mean(self._cw)
|
|
538
|
+
self._c0: float = float(kwargs.get("c0", c0))
|
|
539
|
+
lambda0: float = self._c0 / self._freq
|
|
540
|
+
self._lambda = lambda0
|
|
541
|
+
|
|
542
|
+
# dr and dz are based on c0 to get sensible output steps
|
|
543
|
+
self._dr: float = float(kwargs.get("dr", 0.5 * lambda0))
|
|
544
|
+
self._dz: float = float(kwargs.get("dz", 0.05 * lambda0))
|
|
545
|
+
|
|
546
|
+
self._ndr: int = int(kwargs.get("ndr", PyRAM._ndr_default))
|
|
547
|
+
self._ndz: int = int(kwargs.get("ndz", PyRAM._ndz_default))
|
|
548
|
+
|
|
549
|
+
self._zmplt: float = float(kwargs.get("zmplt", self._rbzb[:, 1].max()))
|
|
550
|
+
|
|
551
|
+
self._rmax: float = float(
|
|
552
|
+
kwargs.get("rmax", np.max([self._rp_ss.max(), self._rp_sb.max(), self._rbzb[:, 0].max()]))
|
|
553
|
+
)
|
|
554
|
+
|
|
555
|
+
self._ns: int = int(kwargs.get("ns", PyRAM._ns_default))
|
|
556
|
+
self._rs: float = float(kwargs.get("rs", self._rmax + self._dr))
|
|
557
|
+
|
|
558
|
+
self._lyrw: float = float(kwargs.get("lyrw", PyRAM._lyrw_default))
|
|
559
|
+
|
|
560
|
+
self._id: int = int(kwargs.get("id", PyRAM._id_default))
|
|
561
|
+
|
|
562
|
+
def setup(self) -> None:
|
|
563
|
+
"""Initialise the parameters, acoustic field, and matrices"""
|
|
564
|
+
|
|
565
|
+
if self._rbzb[-1, 0] < self._rmax:
|
|
566
|
+
self._rbzb = np.append(self._rbzb, np.array([[self._rmax, self._rbzb[-1, 1]]]), axis=0)
|
|
567
|
+
|
|
568
|
+
self.eta = 1 / (40 * np.pi * np.log10(np.exp(1)))
|
|
569
|
+
self.ib = 0 # Bathymetry pair index
|
|
570
|
+
self.mdr = 0 # Output range counter
|
|
571
|
+
self.r = self._dr
|
|
572
|
+
self.omega = 2 * np.pi * self._freq
|
|
573
|
+
ri = self._zr / self._dz
|
|
574
|
+
self.ir = int(np.floor(ri)) # Receiver depth index
|
|
575
|
+
self.dir = ri - self.ir # Offset
|
|
576
|
+
self.k0 = self.omega / self._c0
|
|
577
|
+
self._z_sb += self._z_ss[-1] # Make seabed profiles relative to deepest water profile point
|
|
578
|
+
self._zmax = self._z_sb.max() + self._lyrw * self._lambda
|
|
579
|
+
self.nz = int(np.floor(self._zmax / self._dz)) - 1 # Number of depth grid points - 2
|
|
580
|
+
self.nzplt = int(np.floor(self._zmplt / self._dz)) # Deepest output grid point
|
|
581
|
+
self.iz = int(np.floor(self._rbzb[0, 1] / self._dz)) # First index below seabed
|
|
582
|
+
self.iz = max(1, self.iz)
|
|
583
|
+
self.iz = min(self.nz - 1, self.iz)
|
|
584
|
+
|
|
585
|
+
self.u = np.zeros(self.nz + 2, dtype=np.complex128)
|
|
586
|
+
self.v = np.zeros(self.nz + 2, dtype=np.complex128)
|
|
587
|
+
self.ksq = np.zeros(self.nz + 2, dtype=np.complex128)
|
|
588
|
+
self.ksqb = np.zeros(self.nz + 2, dtype=np.complex128)
|
|
589
|
+
self.r1 = np.zeros([self.nz + 2, self._np], dtype=np.complex128)
|
|
590
|
+
self.r2 = np.zeros([self.nz + 2, self._np], dtype=np.complex128)
|
|
591
|
+
self.r3 = np.zeros([self.nz + 2, self._np], dtype=np.complex128)
|
|
592
|
+
self.s1 = np.zeros([self.nz + 2, self._np], dtype=np.complex128)
|
|
593
|
+
self.s2 = np.zeros([self.nz + 2, self._np], dtype=np.complex128)
|
|
594
|
+
self.s3 = np.zeros([self.nz + 2, self._np], dtype=np.complex128)
|
|
595
|
+
self.pd1 = np.zeros(self._np, dtype=np.complex128)
|
|
596
|
+
self.pd2 = np.zeros(self._np, dtype=np.complex128)
|
|
597
|
+
|
|
598
|
+
self.alpw = np.zeros(self.nz + 2, dtype=np.float64)
|
|
599
|
+
self.alpb = np.zeros(self.nz + 2, dtype=np.float64)
|
|
600
|
+
self.f1 = np.zeros(self.nz + 2, dtype=np.float64)
|
|
601
|
+
self.f2 = np.zeros(self.nz + 2, dtype=np.float64)
|
|
602
|
+
self.f3 = np.zeros(self.nz + 2, dtype=np.float64)
|
|
603
|
+
self.ksqw = np.zeros(self.nz + 2, dtype=np.float64)
|
|
604
|
+
nvr = int(np.floor(self._rmax / (self._dr * self._ndr)))
|
|
605
|
+
self._rmax = nvr * self._dr * self._ndr
|
|
606
|
+
nvz = int(np.floor(self.nzplt / self._ndz))
|
|
607
|
+
self.vr = np.asarray(np.arange(1, nvr + 1) * self._dr * self._ndr, dtype=np.float64)
|
|
608
|
+
self.vz = np.array(np.arange(1, nvz + 1) * self._dz * self._ndz, dtype=np.float64)
|
|
609
|
+
self.tll = np.zeros(nvr, dtype=np.float64)
|
|
610
|
+
self.tlg = np.zeros([nvz, nvr], dtype=np.float64)
|
|
611
|
+
self.cpl = np.zeros(nvr, dtype=np.complex128)
|
|
612
|
+
self.cpg = np.zeros((nvz, nvr), dtype=np.complex128)
|
|
613
|
+
self.tlc = -1 # TL output range counter
|
|
614
|
+
|
|
615
|
+
self.ss_ind = 0 # Sound speed profile range index
|
|
616
|
+
self.sb_ind = 0 # Seabed parameters range index
|
|
617
|
+
self.bt_ind = 0 # Bathymetry range index
|
|
618
|
+
|
|
619
|
+
# The initial profiles and starting field
|
|
620
|
+
self.profl()
|
|
621
|
+
self.selfs()
|
|
622
|
+
self.mdr, self.tlc = outpt(
|
|
623
|
+
self.r,
|
|
624
|
+
self.mdr,
|
|
625
|
+
self._ndr,
|
|
626
|
+
self._ndz,
|
|
627
|
+
self.tlc,
|
|
628
|
+
self.f3,
|
|
629
|
+
self.u,
|
|
630
|
+
self.dir,
|
|
631
|
+
self.ir,
|
|
632
|
+
self.tll,
|
|
633
|
+
self.tlg,
|
|
634
|
+
self.cpl,
|
|
635
|
+
self.cpg,
|
|
636
|
+
)[:]
|
|
637
|
+
|
|
638
|
+
# The propagation matrices
|
|
639
|
+
self.epade()
|
|
640
|
+
matrc(
|
|
641
|
+
self.k0,
|
|
642
|
+
self._dz,
|
|
643
|
+
self.iz,
|
|
644
|
+
self.iz,
|
|
645
|
+
self.nz,
|
|
646
|
+
self._np,
|
|
647
|
+
self.f1,
|
|
648
|
+
self.f2,
|
|
649
|
+
self.f3,
|
|
650
|
+
self.ksq,
|
|
651
|
+
self.alpw,
|
|
652
|
+
self.alpb,
|
|
653
|
+
self.ksqw,
|
|
654
|
+
self.ksqb,
|
|
655
|
+
self.rhob,
|
|
656
|
+
self.r1,
|
|
657
|
+
self.r2,
|
|
658
|
+
self.r3,
|
|
659
|
+
self.s1,
|
|
660
|
+
self.s2,
|
|
661
|
+
self.s3,
|
|
662
|
+
self.pd1,
|
|
663
|
+
self.pd2,
|
|
664
|
+
)
|
|
665
|
+
|
|
666
|
+
def profl(self) -> None:
|
|
667
|
+
"""Set up the profiles"""
|
|
668
|
+
|
|
669
|
+
attnf = 10 # 10dB/wavelength at floor
|
|
670
|
+
|
|
671
|
+
z = np.linspace(0, self._zmax, self.nz + 2)
|
|
672
|
+
self.cw = np.interp(
|
|
673
|
+
z,
|
|
674
|
+
self._z_ss,
|
|
675
|
+
self._cw[:, self.ss_ind],
|
|
676
|
+
left=self._cw[0, self.ss_ind],
|
|
677
|
+
right=self._cw[-1, self.ss_ind],
|
|
678
|
+
)
|
|
679
|
+
self.cb = np.interp(
|
|
680
|
+
z,
|
|
681
|
+
self._z_sb,
|
|
682
|
+
self._cb[:, self.sb_ind],
|
|
683
|
+
left=self._cb[0, self.sb_ind],
|
|
684
|
+
right=self._cb[-1, self.sb_ind],
|
|
685
|
+
)
|
|
686
|
+
self.rhob = np.interp(
|
|
687
|
+
z,
|
|
688
|
+
self._z_sb,
|
|
689
|
+
self._rhob[:, self.sb_ind],
|
|
690
|
+
left=self._rhob[0, self.sb_ind],
|
|
691
|
+
right=self._rhob[-1, self.sb_ind],
|
|
692
|
+
)
|
|
693
|
+
attnlyr = np.concatenate((self._attn[:, self.sb_ind], [self._attn[-1, self.sb_ind], attnf]))
|
|
694
|
+
zlyr = np.concatenate(
|
|
695
|
+
(
|
|
696
|
+
self._z_sb,
|
|
697
|
+
[
|
|
698
|
+
self._z_sb[-1] + 0.75 * self._lyrw * self._lambda,
|
|
699
|
+
self._z_sb[-1] + self._lyrw * self._lambda,
|
|
700
|
+
],
|
|
701
|
+
)
|
|
702
|
+
)
|
|
703
|
+
self.attn = np.interp(z, zlyr, attnlyr, left=self._attn[0, self.sb_ind], right=attnf)
|
|
704
|
+
|
|
705
|
+
self.ksqw = (self.omega / self.cw) ** 2 - self.k0**2
|
|
706
|
+
self.ksqb = ((self.omega / self.cb) * (1 + 1j * self.eta * self.attn)) ** 2 - self.k0**2
|
|
707
|
+
self.alpw = np.asarray(
|
|
708
|
+
np.sqrt(self.cw / self._c0),
|
|
709
|
+
dtype=np.float64,
|
|
710
|
+
)
|
|
711
|
+
|
|
712
|
+
self.alpb = np.asarray(
|
|
713
|
+
np.sqrt(self.rhob * self.cb / self._c0),
|
|
714
|
+
dtype=np.float64,
|
|
715
|
+
)
|
|
716
|
+
|
|
717
|
+
def updat(self) -> None:
|
|
718
|
+
"""Matrix updates"""
|
|
719
|
+
|
|
720
|
+
# Varying bathymetry
|
|
721
|
+
if self.rd_bt:
|
|
722
|
+
npt = self._rbzb.shape[0]
|
|
723
|
+
while (self.bt_ind < npt - 1) and (self.r >= self._rbzb[self.bt_ind + 1, 0]):
|
|
724
|
+
self.bt_ind += 1
|
|
725
|
+
jz = self.iz
|
|
726
|
+
z = self._rbzb[self.bt_ind, 1] + (self.r + 0.5 * self._dr - self._rbzb[self.bt_ind, 0]) * (
|
|
727
|
+
self._rbzb[self.bt_ind + 1, 1] - self._rbzb[self.bt_ind, 1]
|
|
728
|
+
) / (self._rbzb[self.bt_ind + 1, 0] - self._rbzb[self.bt_ind, 0])
|
|
729
|
+
self.iz = int(np.floor(z / self._dz)) # First index below seabed
|
|
730
|
+
self.iz = max(1, self.iz)
|
|
731
|
+
self.iz = min(self.nz - 1, self.iz)
|
|
732
|
+
if self.iz != jz:
|
|
733
|
+
matrc(
|
|
734
|
+
self.k0,
|
|
735
|
+
self._dz,
|
|
736
|
+
self.iz,
|
|
737
|
+
jz,
|
|
738
|
+
self.nz,
|
|
739
|
+
self._np,
|
|
740
|
+
self.f1,
|
|
741
|
+
self.f2,
|
|
742
|
+
self.f3,
|
|
743
|
+
self.ksq,
|
|
744
|
+
self.alpw,
|
|
745
|
+
self.alpb,
|
|
746
|
+
self.ksqw,
|
|
747
|
+
self.ksqb,
|
|
748
|
+
self.rhob,
|
|
749
|
+
self.r1,
|
|
750
|
+
self.r2,
|
|
751
|
+
self.r3,
|
|
752
|
+
self.s1,
|
|
753
|
+
self.s2,
|
|
754
|
+
self.s3,
|
|
755
|
+
self.pd1,
|
|
756
|
+
self.pd2,
|
|
757
|
+
)
|
|
758
|
+
|
|
759
|
+
# Varying sound speed profile
|
|
760
|
+
if self.rd_ss:
|
|
761
|
+
npt = self._rp_ss.size
|
|
762
|
+
ss_ind_o = self.ss_ind
|
|
763
|
+
while (self.ss_ind < npt - 1) and (self.r >= self._rp_ss[self.ss_ind + 1]):
|
|
764
|
+
self.ss_ind += 1
|
|
765
|
+
if self.ss_ind != ss_ind_o:
|
|
766
|
+
self.profl()
|
|
767
|
+
matrc(
|
|
768
|
+
self.k0,
|
|
769
|
+
self._dz,
|
|
770
|
+
self.iz,
|
|
771
|
+
self.iz,
|
|
772
|
+
self.nz,
|
|
773
|
+
self._np,
|
|
774
|
+
self.f1,
|
|
775
|
+
self.f2,
|
|
776
|
+
self.f3,
|
|
777
|
+
self.ksq,
|
|
778
|
+
self.alpw,
|
|
779
|
+
self.alpb,
|
|
780
|
+
self.ksqw,
|
|
781
|
+
self.ksqb,
|
|
782
|
+
self.rhob,
|
|
783
|
+
self.r1,
|
|
784
|
+
self.r2,
|
|
785
|
+
self.r3,
|
|
786
|
+
self.s1,
|
|
787
|
+
self.s2,
|
|
788
|
+
self.s3,
|
|
789
|
+
self.pd1,
|
|
790
|
+
self.pd2,
|
|
791
|
+
)
|
|
792
|
+
|
|
793
|
+
# Varying seabed profile
|
|
794
|
+
if self.rd_sb:
|
|
795
|
+
npt = self._rp_sb.size
|
|
796
|
+
sb_ind_o = self.sb_ind
|
|
797
|
+
while (self.sb_ind < npt - 1) and (self.r >= self._rp_sb[self.sb_ind + 1]):
|
|
798
|
+
self.sb_ind += 1
|
|
799
|
+
if self.sb_ind != sb_ind_o:
|
|
800
|
+
self.profl()
|
|
801
|
+
matrc(
|
|
802
|
+
self.k0,
|
|
803
|
+
self._dz,
|
|
804
|
+
self.iz,
|
|
805
|
+
self.iz,
|
|
806
|
+
self.nz,
|
|
807
|
+
self._np,
|
|
808
|
+
self.f1,
|
|
809
|
+
self.f2,
|
|
810
|
+
self.f3,
|
|
811
|
+
self.ksq,
|
|
812
|
+
self.alpw,
|
|
813
|
+
self.alpb,
|
|
814
|
+
self.ksqw,
|
|
815
|
+
self.ksqb,
|
|
816
|
+
self.rhob,
|
|
817
|
+
self.r1,
|
|
818
|
+
self.r2,
|
|
819
|
+
self.r3,
|
|
820
|
+
self.s1,
|
|
821
|
+
self.s2,
|
|
822
|
+
self.s3,
|
|
823
|
+
self.pd1,
|
|
824
|
+
self.pd2,
|
|
825
|
+
)
|
|
826
|
+
|
|
827
|
+
# Turn off the stability constraints
|
|
828
|
+
if self.r >= self._rs:
|
|
829
|
+
self._ns = 0
|
|
830
|
+
self._rs = self._rmax + self._dr
|
|
831
|
+
self.epade()
|
|
832
|
+
matrc(
|
|
833
|
+
self.k0,
|
|
834
|
+
self._dz,
|
|
835
|
+
self.iz,
|
|
836
|
+
self.iz,
|
|
837
|
+
self.nz,
|
|
838
|
+
self._np,
|
|
839
|
+
self.f1,
|
|
840
|
+
self.f2,
|
|
841
|
+
self.f3,
|
|
842
|
+
self.ksq,
|
|
843
|
+
self.alpw,
|
|
844
|
+
self.alpb,
|
|
845
|
+
self.ksqw,
|
|
846
|
+
self.ksqb,
|
|
847
|
+
self.rhob,
|
|
848
|
+
self.r1,
|
|
849
|
+
self.r2,
|
|
850
|
+
self.r3,
|
|
851
|
+
self.s1,
|
|
852
|
+
self.s2,
|
|
853
|
+
self.s3,
|
|
854
|
+
self.pd1,
|
|
855
|
+
self.pd2,
|
|
856
|
+
)
|
|
857
|
+
|
|
858
|
+
def selfs(self) -> None:
|
|
859
|
+
"""Set up the initial field. The self-starter"""
|
|
860
|
+
|
|
861
|
+
# Conditions for the delta function
|
|
862
|
+
|
|
863
|
+
si = self._zs / self._dz
|
|
864
|
+
_is = int(np.floor(si)) # Source depth index
|
|
865
|
+
dis = si - _is # Offset
|
|
866
|
+
|
|
867
|
+
self.u[_is] = (1 - dis) * np.sqrt(2 * np.pi / self.k0) / (self._dz * self.alpw[_is])
|
|
868
|
+
self.u[_is + 1] = dis * np.sqrt(2 * np.pi / self.k0) / (self._dz * self.alpw[_is])
|
|
869
|
+
|
|
870
|
+
# Divide the delta function by (1-X)**2 to get a smooth rhs
|
|
871
|
+
|
|
872
|
+
self.pd1[0] = 0
|
|
873
|
+
self.pd2[0] = -1
|
|
874
|
+
|
|
875
|
+
matrc(
|
|
876
|
+
self.k0,
|
|
877
|
+
self._dz,
|
|
878
|
+
self.iz,
|
|
879
|
+
self.iz,
|
|
880
|
+
self.nz,
|
|
881
|
+
1,
|
|
882
|
+
self.f1,
|
|
883
|
+
self.f2,
|
|
884
|
+
self.f3,
|
|
885
|
+
self.ksq,
|
|
886
|
+
self.alpw,
|
|
887
|
+
self.alpb,
|
|
888
|
+
self.ksqw,
|
|
889
|
+
self.ksqb,
|
|
890
|
+
self.rhob,
|
|
891
|
+
self.r1,
|
|
892
|
+
self.r2,
|
|
893
|
+
self.r3,
|
|
894
|
+
self.s1,
|
|
895
|
+
self.s2,
|
|
896
|
+
self.s3,
|
|
897
|
+
self.pd1,
|
|
898
|
+
self.pd2,
|
|
899
|
+
)
|
|
900
|
+
for _ in range(2):
|
|
901
|
+
solve(self.u, self.v, self.s1, self.s2, self.s3, self.r1, self.r2, self.r3, self.iz, self.nz, 1)
|
|
902
|
+
|
|
903
|
+
# Apply the operator (1-X)**2*(1+X)**(-1/4)*exp(ci*k0*r*sqrt(1+X))
|
|
904
|
+
|
|
905
|
+
self.epade(ip=2)
|
|
906
|
+
matrc(
|
|
907
|
+
self.k0,
|
|
908
|
+
self._dz,
|
|
909
|
+
self.iz,
|
|
910
|
+
self.iz,
|
|
911
|
+
self.nz,
|
|
912
|
+
self._np,
|
|
913
|
+
self.f1,
|
|
914
|
+
self.f2,
|
|
915
|
+
self.f3,
|
|
916
|
+
self.ksq,
|
|
917
|
+
self.alpw,
|
|
918
|
+
self.alpb,
|
|
919
|
+
self.ksqw,
|
|
920
|
+
self.ksqb,
|
|
921
|
+
self.rhob,
|
|
922
|
+
self.r1,
|
|
923
|
+
self.r2,
|
|
924
|
+
self.r3,
|
|
925
|
+
self.s1,
|
|
926
|
+
self.s2,
|
|
927
|
+
self.s3,
|
|
928
|
+
self.pd1,
|
|
929
|
+
self.pd2,
|
|
930
|
+
)
|
|
931
|
+
solve(
|
|
932
|
+
self.u, self.v, self.s1, self.s2, self.s3, self.r1, self.r2, self.r3, self.iz, self.nz, self._np
|
|
933
|
+
)
|
|
934
|
+
|
|
935
|
+
def epade(self, ip: int = 1) -> None:
|
|
936
|
+
"""Set the coefficients of the rational approximation"""
|
|
937
|
+
|
|
938
|
+
n = 2 * self._np
|
|
939
|
+
_bin = np.zeros([n + 1, n + 1])
|
|
940
|
+
a = np.zeros([n + 1, n + 1], dtype=np.complex128)
|
|
941
|
+
b = np.zeros(n, dtype=np.complex128)
|
|
942
|
+
dg = np.zeros(n + 1, dtype=np.complex128)
|
|
943
|
+
dh1 = np.zeros(n, dtype=np.complex128)
|
|
944
|
+
dh2 = np.zeros(n, dtype=np.complex128)
|
|
945
|
+
dh3 = np.zeros(n, dtype=np.complex128)
|
|
946
|
+
fact = np.zeros(n + 1)
|
|
947
|
+
sig = self.k0 * self._dr
|
|
948
|
+
|
|
949
|
+
nu: int
|
|
950
|
+
alp: float
|
|
951
|
+
|
|
952
|
+
if ip == 1:
|
|
953
|
+
nu, alp = 0, 0.0
|
|
954
|
+
else:
|
|
955
|
+
nu, alp = 1, -0.25
|
|
956
|
+
|
|
957
|
+
# The factorials
|
|
958
|
+
fact[0] = 1
|
|
959
|
+
for i in range(1, n):
|
|
960
|
+
fact[i] = (i + 1) * fact[i - 1]
|
|
961
|
+
|
|
962
|
+
# The binomial coefficients
|
|
963
|
+
for i in range(n + 1):
|
|
964
|
+
_bin[i, 0] = 1
|
|
965
|
+
_bin[i, i] = 1
|
|
966
|
+
for i in range(2, n + 1):
|
|
967
|
+
for j in range(1, i):
|
|
968
|
+
_bin[i, j] = _bin[i - 1, j - 1] + _bin[i - 1, j]
|
|
969
|
+
|
|
970
|
+
# The accuracy constraints
|
|
971
|
+
dg, dh1, dh2, dh3 = self.deriv(n, sig, alp, dg, dh1, dh2, dh3, _bin, nu)
|
|
972
|
+
for i in range(n):
|
|
973
|
+
b[i] = dg[i + 1]
|
|
974
|
+
for i in range(n):
|
|
975
|
+
if 2 * i <= n - 1:
|
|
976
|
+
a[i, 2 * i] = fact[i]
|
|
977
|
+
for j in range(i + 1):
|
|
978
|
+
if 2 * j + 1 <= n - 1:
|
|
979
|
+
a[i, 2 * j + 1] = -_bin[i + 1, j + 1] * fact[j] * dg[i - j]
|
|
980
|
+
|
|
981
|
+
# The stability constraints
|
|
982
|
+
|
|
983
|
+
if self._ns >= 1:
|
|
984
|
+
z1 = -3 + 0j
|
|
985
|
+
b[n - 1] = -1
|
|
986
|
+
for j in range(self._np):
|
|
987
|
+
a[n - 1, 2 * j] = z1 ** (j + 1)
|
|
988
|
+
a[n - 1, 2 * j + 1] = 0
|
|
989
|
+
|
|
990
|
+
if self._ns >= 2:
|
|
991
|
+
z1 = -1.5 + 0j
|
|
992
|
+
b[n - 2] = -1
|
|
993
|
+
for j in range(self._np):
|
|
994
|
+
a[n - 2, 2 * j] = z1 ** (j + 1)
|
|
995
|
+
a[n - 2, 2 * j + 1] = 0
|
|
996
|
+
|
|
997
|
+
a, b = self.gauss(n, a, b, self.pivot)
|
|
998
|
+
|
|
999
|
+
dh1[0] = 1
|
|
1000
|
+
for j in range(self._np):
|
|
1001
|
+
dh1[j + 1] = b[2 * j]
|
|
1002
|
+
dh1, dh2 = self.fndrt(dh1, self._np, dh2, self.guerre)
|
|
1003
|
+
for j in range(self._np):
|
|
1004
|
+
self.pd1[j] = -1 / dh2[j]
|
|
1005
|
+
|
|
1006
|
+
dh1[0] = 1
|
|
1007
|
+
for j in range(self._np):
|
|
1008
|
+
dh1[j + 1] = b[2 * j + 1]
|
|
1009
|
+
dh1, dh2 = self.fndrt(dh1, self._np, dh2, self.guerre)
|
|
1010
|
+
for j in range(self._np):
|
|
1011
|
+
self.pd2[j] = -1 / dh2[j]
|
|
1012
|
+
|
|
1013
|
+
@staticmethod
|
|
1014
|
+
def deriv(
|
|
1015
|
+
n: int, sig: Any, alp: Any, dg: Any, dh1: Any, dh2: Any, dh3: Any, _bin: Any, nu: Any
|
|
1016
|
+
) -> tuple[Any, Any, Any, Any]:
|
|
1017
|
+
"""Return the derivatives of the operator function at x=0"""
|
|
1018
|
+
|
|
1019
|
+
dh1[0] = 0.5 * 1j * sig
|
|
1020
|
+
exp1 = -0.5
|
|
1021
|
+
dh2[0] = alp
|
|
1022
|
+
exp2 = -1
|
|
1023
|
+
dh3[0] = -2 * nu
|
|
1024
|
+
exp3 = -1
|
|
1025
|
+
for i in range(1, n):
|
|
1026
|
+
dh1[i] = dh1[i - 1] * exp1
|
|
1027
|
+
exp1 -= 1
|
|
1028
|
+
dh2[i] = dh2[i - 1] * exp2
|
|
1029
|
+
exp2 -= 1
|
|
1030
|
+
dh3[i] = -nu * dh3[i - 1] * exp3
|
|
1031
|
+
exp3 -= 1
|
|
1032
|
+
|
|
1033
|
+
dg[0] = 1
|
|
1034
|
+
dg[1] = dh1[0] + dh2[0] + dh3[0]
|
|
1035
|
+
for i in range(1, n):
|
|
1036
|
+
dg[i + 1] = dh1[i] + dh2[i] + dh3[i]
|
|
1037
|
+
for j in range(i):
|
|
1038
|
+
dg[i + 1] += _bin[i, j] * (dh1[j] + dh2[j] + dh3[j]) * dg[i - j]
|
|
1039
|
+
|
|
1040
|
+
return dg, dh1, dh2, dh3
|
|
1041
|
+
|
|
1042
|
+
@staticmethod
|
|
1043
|
+
def gauss(n: int, a: Any, b: Any, pivot: Any) -> tuple[Any, Any]:
|
|
1044
|
+
"""
|
|
1045
|
+
Gaussian elimination
|
|
1046
|
+
"""
|
|
1047
|
+
|
|
1048
|
+
# Downward elimination
|
|
1049
|
+
for i in range(n):
|
|
1050
|
+
if i < n - 1:
|
|
1051
|
+
a, b = pivot(n, i, a, b)
|
|
1052
|
+
a[i, i] = 1 / a[i, i]
|
|
1053
|
+
b[i] *= a[i, i]
|
|
1054
|
+
if i < n - 1:
|
|
1055
|
+
for j in range(i + 1, n + 1):
|
|
1056
|
+
a[i, j] *= a[i, i]
|
|
1057
|
+
for k in range(i + 1, n):
|
|
1058
|
+
b[k] -= a[k, i] * b[i]
|
|
1059
|
+
for j in range(i + 1, n):
|
|
1060
|
+
a[k, j] -= a[k, i] * a[i, j]
|
|
1061
|
+
|
|
1062
|
+
# Back substitution
|
|
1063
|
+
for i in range(n - 2, -1, -1):
|
|
1064
|
+
for j in range(i + 1, n):
|
|
1065
|
+
b[i] -= a[i, j] * b[j]
|
|
1066
|
+
|
|
1067
|
+
return a, b
|
|
1068
|
+
|
|
1069
|
+
@staticmethod
|
|
1070
|
+
def pivot(n: int, i: int, a: Any, b: Any) -> tuple[Any, Any]:
|
|
1071
|
+
"""
|
|
1072
|
+
Rows are interchanged for stability
|
|
1073
|
+
"""
|
|
1074
|
+
|
|
1075
|
+
i0 = i
|
|
1076
|
+
amp0 = np.abs(a[i, i])
|
|
1077
|
+
for j in range(i + 1, n):
|
|
1078
|
+
amp = np.abs(a[j, i])
|
|
1079
|
+
if amp > amp0:
|
|
1080
|
+
i0 = j
|
|
1081
|
+
amp0 = amp
|
|
1082
|
+
|
|
1083
|
+
if i0 != i:
|
|
1084
|
+
b[i0], b[i] = b[i], b[i0]
|
|
1085
|
+
for j in range(i, n + 1):
|
|
1086
|
+
a[i0, j], a[i, j] = a[i, j], a[i0, j]
|
|
1087
|
+
|
|
1088
|
+
return a, b
|
|
1089
|
+
|
|
1090
|
+
@staticmethod
|
|
1091
|
+
def fndrt(a: Any, n: int, z: Any, guerre: Any) -> tuple[Any, Any]:
|
|
1092
|
+
"""Find the roots of polynomial a"""
|
|
1093
|
+
|
|
1094
|
+
if n == 1:
|
|
1095
|
+
z[0] = -a[0] / a[1]
|
|
1096
|
+
return a, z
|
|
1097
|
+
|
|
1098
|
+
if n != 2:
|
|
1099
|
+
for k in range(n - 1, 1, -1):
|
|
1100
|
+
# Obtain an approximate root
|
|
1101
|
+
root = 0
|
|
1102
|
+
err = 1e-12
|
|
1103
|
+
a, root, err = guerre(a, k + 1, root, err, 1000)
|
|
1104
|
+
# Refine the root by iterating five more times
|
|
1105
|
+
err = 0
|
|
1106
|
+
a, root, err = guerre(a, k + 1, root, err, 5)
|
|
1107
|
+
z[k] = root
|
|
1108
|
+
# Divide out the factor (z-root).
|
|
1109
|
+
for i in range(k, -1, -1):
|
|
1110
|
+
a[i] += root * a[i + 1]
|
|
1111
|
+
for i in range(k + 1):
|
|
1112
|
+
a[i] = a[i + 1]
|
|
1113
|
+
|
|
1114
|
+
z[1] = 0.5 * (-a[1] + np.sqrt(a[1] ** 2 - 4 * a[0] * a[2])) / a[2]
|
|
1115
|
+
z[0] = 0.5 * (-a[1] - np.sqrt(a[1] ** 2 - 4 * a[0] * a[2])) / a[2]
|
|
1116
|
+
|
|
1117
|
+
return a, z
|
|
1118
|
+
|
|
1119
|
+
@staticmethod
|
|
1120
|
+
def guerre(a: Any, n: int, z: Any, err: Any, nter: int) -> tuple[Any, Any, Any]:
|
|
1121
|
+
"""
|
|
1122
|
+
Return the root of a polynomial of degree n > 2 by Laguerre's method
|
|
1123
|
+
"""
|
|
1124
|
+
|
|
1125
|
+
az = np.zeros(n, dtype=np.complex128)
|
|
1126
|
+
azz = np.zeros(n - 1, dtype=np.complex128)
|
|
1127
|
+
|
|
1128
|
+
eps = 1e-20
|
|
1129
|
+
# The coefficients of p'(z) and p''(z)
|
|
1130
|
+
for i in range(n):
|
|
1131
|
+
az[i] = (i + 1) * a[i + 1]
|
|
1132
|
+
for i in range(n - 1):
|
|
1133
|
+
azz[i] = (i + 1) * az[i + 1]
|
|
1134
|
+
|
|
1135
|
+
_iter = 0
|
|
1136
|
+
jter = 0 # Missing from original code - assume this is correct
|
|
1137
|
+
dz = np.inf
|
|
1138
|
+
|
|
1139
|
+
while (np.abs(dz) > err) and (_iter < nter - 1):
|
|
1140
|
+
p = a[n - 1] + a[n] * z
|
|
1141
|
+
for i in range(n - 2, -1, -1):
|
|
1142
|
+
p = a[i] + z * p
|
|
1143
|
+
if np.abs(p) < eps:
|
|
1144
|
+
return a, z, err
|
|
1145
|
+
|
|
1146
|
+
pz = az[n - 2] + az[n - 1] * z
|
|
1147
|
+
for i in range(n - 3, -1, -1):
|
|
1148
|
+
pz = az[i] + z * pz
|
|
1149
|
+
|
|
1150
|
+
pzz = azz[n - 3] + azz[n - 2] * z
|
|
1151
|
+
for i in range(n - 4, -1, -1):
|
|
1152
|
+
pzz = azz[i] + z * pzz
|
|
1153
|
+
|
|
1154
|
+
# The Laguerre perturbation
|
|
1155
|
+
f = pz / p
|
|
1156
|
+
g = f**2 - pzz / p
|
|
1157
|
+
h = np.sqrt((n - 1) * (n * g - f**2))
|
|
1158
|
+
amp1 = np.abs(f + h)
|
|
1159
|
+
amp2 = np.abs(f - h)
|
|
1160
|
+
if amp1 > amp2:
|
|
1161
|
+
dz = -n / (f + h)
|
|
1162
|
+
else:
|
|
1163
|
+
dz = -n / (f - h)
|
|
1164
|
+
|
|
1165
|
+
_iter += 1
|
|
1166
|
+
|
|
1167
|
+
# Rotate by 90 degrees to avoid limit cycles
|
|
1168
|
+
|
|
1169
|
+
jter += 1
|
|
1170
|
+
if jter == 9:
|
|
1171
|
+
jter = 0
|
|
1172
|
+
dz *= 1j
|
|
1173
|
+
z += dz
|
|
1174
|
+
|
|
1175
|
+
if _iter == 100:
|
|
1176
|
+
raise ValueError("Laguerre method not converging. Try a different combination of DR and NP.")
|
|
1177
|
+
|
|
1178
|
+
return a, z, err
|