Ir al contenido
Esta documentación describe la versión 4.0.0, todavía sin publicar. La versión actual en PyPI es la 3.3.0 y no incluye todo lo que se describe aquí.

underwater.propagation.numerical

La referencia de la API se publica en inglés en los dos idiomas: se genera a partir de los docstrings del código, que son su texto original.

Numerical models of underwater sound propagation (range-independent water, with an optionally sloping bottom for the ray-based solvers).

Four complementary numerical solvers for the acoustic field in a horizontally-stratified ocean waveguide, complementing the closed-form propagation loss of phonometry.underwater.propagation.closed_form:

  • normal_modes — the normal-mode expansion. Solves the depth-separated Sturm-Liouville eigenvalue problem by finite differences and assembles the propagation loss from the propagating modes.
  • ray_trace — ray tracing. Integrates the ray-trajectory equations through a sound-speed profile (Runge-Kutta), returning the ray paths and the travel time accumulated along each of them, and no amplitude.
  • gaussian_beams — Gaussian beam tracing. Hangs a beam on each of those rays and sums them into a propagation-loss field, which is finite at a caustic and decays smoothly into a shadow zone where ray theory has nothing to say.
  • eigenrays — the arrival structure. Takes a traced fan and a receiver and refines, by bisection on fresh traces, the rays that actually connect the source to that receiver, each with its travel time, launch and arrival angles, boundary-touch counts and classical complex amplitude: the list the sonar equation, a channel impulse response and communications work consume.
  • parabolic_equation — the standard (Tappert) parabolic equation, solved with the split-step Fourier algorithm, returning the propagation-loss field.

All four are implemented clean-room from Jensen, Kuperman, Porter & Schmidt, Computational Ocean Acoustics (2nd ed., Springer 2011): the modal derivation (Ch. 5, Eqs. 5.3-5.17), the ray equations (Ch. 3, Eqs. 3.23-3.24), the Gaussian beams of Sect. 3.5 (Eqs. 3.88-3.92) and the split-step Fourier PE (Ch. 6). They are validated against analytic oracles: the ideal (pressure-release) waveguide’s exact modes and its image-source sum, that same image sum over a lossy fluid seabed with the Rayleigh coefficient of each image’s own grazing angle raised to its count of bottom touches (Jensen Eq. 2.138 with Eq. 3.126 at every touch), the circular-arc ray paths of a linear sound-speed gradient together with the closed-form travel time along them (Medwin & Clay, Fundamentals of Acoustical Oceanography, Academic Press 1998, Eq. (3.3.20)), free-field spherical spreading, mutual agreement of the PE and normal-mode propagation loss for a range-independent waveguide, and, for the sloping bottom the two ray-based solvers accept, the ideal wedge’s exact image fan (the folded geometry to eleven digits, the beam field in dB).

The three field solvers report the same quantity on the same terms, so their propagation losses can be laid side by side: normal_modes on a range slice at one receiver depth, gaussian_beams and parabolic_equation on a (depth, range) grid. Which of them to reach for is a question of frequency and of what is being asked; the guide’s solver table sets it out.

Densities are in kg/m3, sound speeds in m/s, depths and ranges in metres, frequencies in Hz. The water column has a pressure-release surface at z = 0.

Auto-generated from the source docstrings by scripts/generate_api_docs.py (make api-docs). Do not edit by hand.

BeamFan(
max_angle_deg: float = 80.0,
n_beams: int | None = None,
beam_width: float | None = None,
)

The launch fan gaussian_beams hangs its beams on.

Aperture, density and initial width travel together because they are one bargain: the overlap condition that sizes the default fan reads the width, and the width’s own default is per launch angle. Every field has a default, so BeamFan() is the stock fan and one field is named to move one lever.

Attributes

NameDescription
max_angle_degHalf-angle of the fan, in degrees from the horizontal. Beams are spread symmetrically over [-max_angle_deg, +max_angle_deg].
n_beamsNumber of beams in the fan. Default (None): from the overlap condition. Adjacent beams are apart at arc length while each has spread to , so the condition that they still overlap, , is range-independent; the default takes four times that margin. Too coarse a fan shows as a periodic ripple in range at the beam spacing, which is easy to mistake for physical interference.
beam_widthThe of Eq. (3.91), in metres: the beam’s initial half-width, at the folding distance in intensity, applied to every beam of the fan when passed. Default (None): one width per launch angle, the free-space optimum of each beam’s own flight; see _default_beam_widths.
EigenrayResult(
launch_angles: NDArray[np.float64],
arrival_angles: NDArray[np.float64],
travel_times: NDArray[np.float64],
amplitudes: NDArray[np.complex128],
surface_reflections: NDArray[np.int_],
bottom_reflections: NDArray[np.int_],
caustic_crossings: NDArray[np.int_],
receiver_range: float,
receiver_depth: float,
source_depth: float,
water_depth: float,
)

The eigenrays connecting one source to one receiver, earliest first.

Every per-arrival array has one entry per eigenray, sorted by travel time. The frequency-independent pieces of each arrival are recorded separately (delay, complex amplitude, angles, boundary counts) so that one search serves every frequency: in the module’s convention the pressure a tone of angular frequency produces at the receiver is

with the amplitudes and the travel_times; the band-limited channel impulse response is the inverse transform of that sum, a spike of complex weight at each .

Attributes

NameDescription
launch_anglesLaunch angle of each eigenray at the source, from the horizontal, in degrees, positive downward: the same convention the fan was launched with.
arrival_anglesThe angle each eigenray crosses the receiver with, same convention. In a range-independent medium its magnitude is fixed by Snell’s invariant at the receiver depth; its sign says whether the arrival is descending or climbing, which is what a vertical array steers on.
travel_timesTravel time of each eigenray, in seconds: the marcher’s third Runge-Kutta state read at the receiver, not a quadrature over the finished path.
amplitudesComplex amplitude of each arrival, dimensionless, normalised to unit pressure at 1 m from the source (the reference of Jensen Eqs. (3.67)-(3.68), the same one every field solver of this module reports its loss against). The magnitude is the classical ray amplitude of Eq. (3.65), with integrated from the point-source initial conditions of Eq. (3.63); the phase is the caustic factor of Eq. (3.79) times the boundary factors and (Eqs. 3.125-3.126). See eigenrays for why that convention and not another.
surface_reflectionsSea-surface touches of each eigenray.
bottom_reflectionsSeabed touches of each eigenray. The pair classifies the arrivals the way Fig. 3.7 colours them: refracted paths carry zeros, and every multipath family is named by its counts.
caustic_crossingsThe KMAH index of Eq. (3.79): how many times each eigenray’s ray-tube spreading vanished on the way, each crossing turning the amplitude by . Zero for every path in an isovelocity channel, where straight rays cannot form caustics.
receiver_rangeRange of the receiver the list connects to, in m.
receiver_depthIts depth, in metres.
source_depthSource depth, in metres.
water_depthWater-column depth, in metres.
EigenrayResult.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot the arrival structure (per-path loss stems against delay).

eigenrays(
trace: RayTraceResult,
*,
receiver_range: float,
receiver_depth: float,
bottom: str | FluidSeabed = 'pressure-release',
max_arrivals: int = 64,
n_steps: int | None = None,
) -> EigenrayResult

The eigenrays a traced fan brackets between a source and a receiver.

Finding an eigenray is finding a launch angle whose ray’s depth at the receiver range equals the receiver depth: a root of , which is continuous in because a specular reflection folds the trajectory continuously. The fan of trace supplies the brackets, one per adjacent pair of rays whose changes sign, and each bracket is then closed by bisection on fresh marches through the same profile, never by interpolating between the traced rays: interpolation across a fan is exactly the hazard Jensen Sect. 3.7.5.1 illustrates (two rays of one bracket taking different bounce histories have no path between them), and a root polished on real traces is a real ray, whose travel time, bounce counts and amplitude are its own rather than a blend’s. The marcher that traces the fan is the marcher that closes the brackets, with the dynamic pair of Eq. (3.58) riding along under the real point-source initial conditions of Eq. (3.63), so every arrival’s amplitude is the Jacobian of the very trajectory that hit the receiver.

The amplitude convention, stated once. Each arrival’s complex amplitude is

in the module’s convention, normalised to unit pressure at 1 m. Why each factor:

  • The magnitude is Eq. (3.65) with the cancelled against the free-field reference of Eqs. (3.67)-(3.68), which is how the book itself defines transmission loss from these amplitudes and how every solver of this module already normalises: the coherent sum over a complete arrival set is directly comparable to GaussianBeamResult.pressure, and to every propagation loss here.
  • is Eq. (3.79) exactly as printed. Sect. 3.3 writes the ray field as (Eq. 3.57), which is the convention, so the printed factor transfers unchanged; it is the same per caustic the beam solver’s tracked square-root branch spends continuously, taken here in the discrete form the classical amplitude needs, since with real initial conditions passes through zero instead of around it.
  • and are Eqs. (3.125)-(3.126) applied at every boundary touch, collapsed to powers because Snell’s invariant fixes one crossing angle per ray at a flat boundary. With a FluidSeabed as the bottom, is the Rayleigh coefficient of reflection_coefficient at that angle, not conjugated: that function returns the coefficient in the convention these amplitudes are declared in, so it enters as printed. (gaussian_beams conjugates the very same coefficient because its internal sum is assembled in the conjugate convention and conjugated once at the end; neither solver’s choice transfers to the other, which is why both spell it out.)

A receiver standing exactly on a caustic is the one place the list is honest rather than useful: there and the classical amplitude of Eq. (3.65) diverges, as Sect. 3.4.1 says it must. The infinity is ray theory’s own, not an artefact to clamp; gaussian_beams is the solver whose field stays finite there.

What the fan resolves is what the search can find. A bracket exists where changes sign between adjacent fan rays, so a pair of eigenrays standing between the same two rays (the two sides of a fold near a caustic do this first) merges into no bracket at all, and an eigenray steeper than the fan’s aperture does not exist to it. The fan’s density and half-angle are the completeness levers, and they belong to the caller’s ray_trace, where they are visible, rather than to a hidden retrace here. Tangential contact (an that touches zero without crossing) is likewise invisible, which is also why a receiver on a boundary is rejected: a folded trajectory only ever grazes the boundaries.

The cap. The multipath count grows without bound as paths steepen: in an ideal waveguide nothing but spreading attenuates the high-order bounces, and every extra of unfolded depth is two more arrivals. max_arrivals bounds what is returned: the earliest max_arrivals arrivals are kept, because the early paths are the flat, least-bounced, least-attenuated ones that carry the energy, and the tail being discarded is the part every physical seabed drains fastest. A PhonometryWarning says when the cap truncated; raise it to keep everything the fan bracketed.

Parameters

NameDescription
traceA ray_trace result: the fan to bracket on, the profile to retrace through, and the source the rays leave from.
receiver_rangeReceiver range, in metres, within the traced range.
receiver_depthReceiver depth, in metres, strictly inside the water column.
bottom"pressure-release" (default) or "rigid", the perfect reflector whose coefficient ( or ) each bottom touch multiplies into the amplitude; or a FluidSeabed, whose complex Rayleigh coefficient is charged instead. The sea surface is always pressure-release. The choice touches amplitudes only; the geometry, times and angles of the eigenrays are specular either way.
max_arrivalsMost arrivals to return (earliest kept, see above).
n_stepsRange samples per refinement march, receiver column included. Default (None): the step of trace itself carried over, so the search resolves what the fan resolved.

Returns: An EigenrayResult, possibly with zero arrivals: a receiver the traced fan never crosses (a shadow zone, or simply outside the aperture) has no eigenrays to list, which is an answer and not an error.

Raises

ExceptionWhen
ValueErrorIf the inputs are invalid.
FluidSeabed(
density: float,
sound_speed: float,
water_density: float = 1000.0,
)

A lossy fluid seabed, passed as the bottom of a solver.

The fluid half-space of reflection_coefficient under the water column: handing one to eigenrays or gaussian_beams as bottom= replaces the perfect reflector with the Rayleigh interface, and every bottom touch of a path is charged the complex coefficient at the one grazing angle Snell’s invariant fixes for it. The water’s own sound speed at the interface comes from the sound-speed profile, so it is not repeated here.

Attributes

NameDescription
densitySediment density , in the same unit as water_density (kg/m³ by convention; only the ratio enters).
sound_speedSediment sound speed , in m/s. A sediment faster than the water at the bottom has a critical grazing angle, below which the reflection is total in magnitude and lossy in phase alone.
water_densityWater density above the seabed (default 1000 kg/m³). It enters only through the impedance ratio, the fields themselves being density-normalised already.
gaussian_beams(
frequency_hz: float,
depths: NDArray[np.float64] | list[float],
sound_speeds: NDArray[np.float64] | list[float],
*,
source_depth: float,
max_range: float = 10000.0,
ranges_m: NDArray[np.float64] | list[float] | None = None,
receiver_depths_m: NDArray[np.float64] | list[float] | None = None,
n_depth_points: int = 200,
fan: BeamFan | None = None,
range_step: float = 25.0,
bottom: str | FluidSeabed = 'pressure-release',
absorption: str | VolumeAbsorption | None = None,
bathymetry: tuple[NDArray[np.float64] | list[float], NDArray[np.float64] | list[float]] | None = None,
) -> GaussianBeamResult

Propagation-loss field from Gaussian beam tracing.

Hangs a Gaussian beam on each ray of a launch fan (Jensen Eq. 3.88) and sums them over the fan with the weight of Eq. (3.92). The rays are the ones ray_trace draws, integrated by the same marcher through the same profile; what is added is the dynamic pair of Eq. (3.58), started from the complex conditions of Eq. (3.91) that make each ray the axis of a beam of initial half-width fan.beam_width and flat wavefront.

The point of the beams is that the answer stays finite. Ray theory’s amplitude, Eq. (3.65), divides by the ray-tube spreading, which vanishes on a caustic and gives an infinity there (Sect. 3.4.1) and nothing at all in a shadow zone. Complex cannot vanish, so this field needs no KMAH index and no minimum-width floor, is finite wherever a beam reaches, and falls into a shadow zone gradually rather than off a cliff, which is what the exact solution does (Figs. 3.11, 3.17). See GaussianBeamResult for the one place it still reports an infinity, which is the wedge no beam of the fan illuminates at all.

The limits are worth knowing before the numbers are believed.

  • Ray theory’s own regime (Sect. 3.4.2): “the wavelength should be substantially smaller than any physical scale in the problem”. This is the limit that bites hardest and the one a plausible-looking answer hides best — and one earlier version of this paragraph blamed it for an error that was really the beam width’s. At 20 Hz in 100 m of water the depth is 1.3 wavelengths and two modes propagate; the quarter-depth cap this module used to put on left a beam a third of a wavelength across, and the loss came out decibels high against the image-source sum. With the cap retired the same guide (source 36 m, receiver 64 m, energy-averaged 0.2 to 5 km) measures -0.001 dB in the mean and 0.03 dB at worst, at the ten-wavelength floor’s 750 m width. That clean bill is narrower than it looks: an isovelocity column over perfect reflectors is pure geometry, which the folded receiver images reproduce exactly at any frequency, so it says nothing about a channel the low-frequency field actually refracts through, and there normal_modes remains the solver to trust, exact in that regime for the cost of two modes.
  • There is no near field, and this is the largest error the function makes. Eq. (3.92) weights the fan by matching it to a point source in the far field, and Eq. (3.88) divides by a cylindrical range that goes to zero on the axis every ray leaves from, so close in the sum has nothing to converge to. The cylindrical range is floored at one wavelength, which is what keeps the answer bounded rather than what makes it right. The scale it recovers on is the initial beam width, not a fixed distance. Worst error against over a +-500 m depth cut in an unbounded medium at 100 Hz, at three settings whose spans 150 to 437 m: 17, 13 and 4.1 dB at a quarter of , 1.2, 0.64 and 0.36 dB at , 0.012, 0.005 and 0.002 dB at 2.5 , and a thousandth of a decibel or better from 3 out. Read nothing inside about three beam widths of the source; since the default’s free-space term grows as , a longer run pushes that boundary out rather than in. parabolic_equation is the solver to reach for close to the source.
  • The fan is truncated at fan.max_angle_deg, and a waveguide with two perfectly reflecting boundaries is the worst case for that, because nothing but attenuates the steep multiple bounces. Measured on the ideal 1000 m guide at 300 Hz, source at 300 m and receiver at 600 m, against the image-source sum at 2, 5 and 10 km: a fan to 80 degrees is 0.27, 4.06 and 2.52 dB out, a fan to 85 degrees 0.21, 1.32 and 1.91 dB, and a fan to 88 degrees 0.0002, 0.0003 and 0.0004 dB. Cutting the oracle to the same half-angle moves it by 0.25, 3.95 and 2.31 dB, so what is left at 80 degrees is the fan and not the method. A real seabed (the FluidSeabed below) absorbs those bounces and the default is then ample; a perfect reflector needs the fan opened and range_step cut with it, since a step has to resolve depth units of climb per unit range. The warning below says when that pairing is wrong.
  • A shallow channel sets its own width, and the default now pays it per launch angle rather than clamping against it. An earlier version of this module capped at a quarter of the water depth, reading Sect. 3.5’s caution that a beam “large compared to the channel … causes a variety of problems” as a ceiling; measured against the closed-form Airy modes of an -linear 200 m guide at 200 Hz (source 30.5 m, receiver 120.5 m, energy-averaged over 0.5 to 4 km), that cap’s 50 m width came out +3.08 dB in the mean and +5.86 dB at worst, systematically too quiet, while the per-angle default measures +0.19 dB on the same cut. What a shallow guide actually demands is the opposite bound, a beam wide enough to resolve the channel’s modes in launch angle, and _default_beam_widths says why that is and what the folded receiver images do to make the width affordable. The same profile in 1000 m of water, where the modal criterion is out of the band’s reach and the free-space optimum stands, comes out at +0.72 dB with a 1.37 dB worst bin, closer to the exact field than normal_modes on the same cut. An explicit fan.beam_width is taken as given, whatever its size: the old quarter-depth warning went with the cap, since the measurements put the fault on the cap’s side.

Seawater absorption is off by default and the field is then optimistic beyond a few kilometres at sonar frequencies, exactly as ray theory without a volume loss must be. Passing absorption multiplies each beam by with the arc length along its central ray, which is Sect. 3.6.2 done as printed: perturbing the eikonal with the complex sound speed a volume loss implies leaves the real rays standing and attaches to each (Eq. 3.116), an integral along the path flown, not along the range axis. The distinction is not pedantry. The same section notes that adding to the loss “is used in many ray models”, and that shortcut under-charges every steep or multiply-reflected path by the obliquity of its climb: a path at 60 degrees is twice as long as the range it covers, and it is precisely the steep multiples of a waveguide that absorption is supposed to be killing. The marcher integrates with the very Runge-Kutta stages that place the ray (), so the length the loss is charged over is the length of the geometry actually summed. The coefficient itself comes from seawater_absorption, one per run, evaluated at the source frequency and at the source depth (the same point the reference sound speed is read at); over a water column the coefficient’s own depth terms move it by around a percent per hundred metres, which is far inside the method’s error budget. The default stays off so the validation figures quoted throughout, all measured without absorption, remain reproducible as printed.

The seabed is a perfect reflector by default, for the same reason, and real shallow-water propagation loss is dominated by what that default leaves out: the seabed absorbs part of every bottom bounce. Passing a FluidSeabed as the bottom replaces the perfect reflector with the lossy fluid half-space of reflection_coefficient (the Rayleigh interface, density standing for the water above it and the profile’s own bottom sound speed for its c1). This is Sect. 3.6.3 done as printed: “most ray codes treat the bottom simply as a reflector”, and each boundary touch multiplies the ray amplitude by and adds to its phase (Eqs. 3.125-3.126), while the dynamic pair crosses the reflection exactly as before, the curvature term of Eq. (3.122) vanishing at a flat bottom, so the beam keeps its width and only its complex amplitude is docked. The phase is not a refinement to skip: below the critical angle , only the phase distinguishes the lossy seabed from a perfect one, and it moves the interference fringes of every bottom-interacting path. The grazing angle each beam is charged at is the one Snell’s invariant fixes, along the whole ray, so at a flat seabed a given beam arrives at one and the same angle at every touch whatever the profile above did in between: its coefficient is evaluated once, exactly, and raised to the marcher’s count of bottom touches, in the running product and in the receiver-image ladder alike. The book is candid that a plane-wave coefficient applied to a field that is not a plane wave is an approximation (p. 189), and it is the approximation the whole method already breathes; sediment attenuation and elasticity are outside the fluid-fluid model here as they are outside seabed_reflection itself.

The bottom may slope. The bathymetry pair replaces the level bottom with the same piecewise-linear depth(r) polyline ray_trace takes, and it is the first range dependence in this module; the sound-speed profile stays range independent, deliberately — see ray_trace’s scope note for why full is excluded (no exact oracle exists to hold it to). Four things follow from the slope, each stated with its cost:

  • Every beam’s central ray reflects specularly off the local facet (Eq. 3.121), so an upslope bounce steepens it by twice the slope, and the dynamic pair takes the reflection impulse of Eqs. (3.122)-(3.123) evaluated on that facet (curvature zero, since the facets are straight; phonometry._internal.rays records the closed form and its flat-bottom limit).
  • A beam steepened past the vertical would run backward in range, which a range-marching solver cannot carry: it is terminated at that bounce and its weight is zero from there on, so the field keeps only what still travels forward, exactly as the one-way parabolic equation keeps no backscatter. Upslope propagation toward an apex therefore loses the energy the wedge sends back down the slope; the ideal-wedge oracle in the tests prices that truncation next to everything else.
  • The receiver-image ladder folds each receiver column about its own local facet: the vertical stack of mirrors becomes the dihedral fan about the local apex, exact for a single facet and local to each column’s facet on a general polyline; at slope zero it is the level ladder bit for bit, which a test pins. The fan is what lets the tails keep representing paths the marched axes cannot: an arrival that went up the slope, turned past the vertical and came back is a wrapped rung of the fan, analytic rather than marched, and the solver recovers most of it.
  • The lossy fluid seabed cannot be combined with a slope, and the rejection is of the wiring, not the physics: the one-coefficient-per- beam collapse rests on Snell’s invariant fixing a single grazing angle per ray at the bottom, and a slope rotates that invariant at every touch. A sloping run takes the perfect reflectors of bottom.

Validated against the ideal wedge, which has an exact solution by images: an isovelocity wedge under a pressure-release surface with a rigid sloping bottom of angle ( even) unfolds into a closed fan of image sources on a circle about the apex. The tests build that fan from pure geometry and quantify the agreement in dB, cross-sections in range and depth, upslope: a tenth of a decibel where a single facet bounce carries the field, and within about two decibels of the complete wedge field (mean two thirds of one) across a thin 2.8-degree wedge whose every cell is dense multipath, most of it arrivals near their own turning point, which is where a one-way marcher pays its way; the test module’s docstring records the measurements and their stability under step, width and aperture.

What it costs is fan.n_beams times the size of the receiver grid, and none of the three factors depends on the frequency: the ray core does not have to resolve a wavelength on a grid, and the fan only widens as , which the default width holds nearly fixed. On a 5000 m Munk column at 100 Hz over 10 km, everything left at its default (512 beams, a 200 by 401 field), this takes 14 s against 0.1 s for parabolic_equation and 177 s for normal_modes; raise the frequency and the first number stays where it is while the other two climb. Shrinking n_depth_points or handing in a coarser ranges_m is the direct way to trade resolution for time.

Parameters

NameDescription
frequency_hzSource frequency, in Hz.
depthsDepth samples of the profile, in metres, from z = 0.
sound_speedsSound speed at each depth, in m/s.
source_depthSource depth, in metres, inside the water column.
max_rangeMaximum range to march to, in metres.
ranges_mRanges at which to evaluate the field, in metres. Default (None): the marching grid itself, which puts every receiver on a column the rays were actually sampled at.
receiver_depths_mDepths at which to evaluate the field, in metres. Default (None): n_depth_points points spread over the water column, on the interior grid parabolic_equation uses, so the two fields land on the same depths.
n_depth_pointsSize of that default depth grid.
fanThe launch fan, as a BeamFan: its half-angle max_angle_deg, its density n_beams and its initial width beam_width, each with the default that class documents. Default (None): the stock BeamFan(), the fan every published validation number of this module was measured with.
range_stepMarching step in range, in metres, and the spacing of the default ranges_m.
bottom"pressure-release" (default) or "rigid", perfect reflectors; or a FluidSeabed, the lossy fluid half-space whose complex Rayleigh coefficient every bottom touch is charged. The sea surface is always pressure-release, and the perfect default is what every published validation number of this module returns.
absorptionSeawater volume absorption applied along each beam’s central ray: the bare model name ("francois-garrison", "ainslie-mccolm" or "thorp") for the standard water of VolumeAbsorption’s defaults, or a VolumeAbsorption naming its own temperature, salinity and pH. Default (None): no volume absorption, so the published validation numbers of this module are what the solver returns.
bathymetryA (ranges_m, depths_m) pair of arrays describing a piecewise-linear bottom profile, the same pair ray_trace takes: node ranges in metres, strictly increasing from r = 0 and level past the last node, with the bottom depth at each node in metres, strictly positive and never below the sound-speed profile’s last depth. Not alongside a FluidSeabed. Default (None): the level bottom at the profile’s last depth, which is every validation number of this module.

Returns: A GaussianBeamResult.

Raises

ExceptionWhen
ValueErrorIf the inputs are invalid.

Warns

WarningWhen
PhonometryWarningwhen the source sits on a kink of the profile (Sect. 3.7.4’s spurious horizontal jet), and when one marching step carries the steepest beam of the fan across more than a quarter of the water column, which is the pairing between fan.max_angle_deg and range_step that is easiest to get wrong.
GaussianBeamResult(
frequency: float,
ranges: NDArray[np.float64],
depths: NDArray[np.float64],
propagation_loss: NDArray[np.float64],
pressure: NDArray[np.complex128],
launch_angles: NDArray[np.float64],
ray_ranges: NDArray[np.float64],
ray_depths: NDArray[np.float64],
beam_widths: NDArray[np.float64],
wavefront_curvatures: NDArray[np.float64],
initial_beam_widths: NDArray[np.float64],
absorption_model: str | None,
absorption_coefficient: float,
seabed_density: float | None,
seabed_sound_speed: float | None,
source_depth: float,
water_depth: float,
bathymetry_ranges: NDArray[np.float64] | None = None,
bathymetry_depths: NDArray[np.float64] | None = None,
)

Gaussian beam solution of a range-independent waveguide.

The propagation-loss field is on the same footing as ParabolicEquationResult’s: same shape, same reference, so the two can be subtracted.

Attributes

NameDescription
frequencySource frequency, in Hz.
rangesRange grid of the field, in metres.
depthsDepth grid of the field, in metres.
propagation_lossPropagation-loss field PL(z, r), in dB, shape (n_depths, n_ranges). Infinite where the field is exactly zero, which happens in the wedge no beam of the fan reaches: each beam is summed out to four half-widths, 140 dB below its own axis, so a point that far from every one of them is outside the traced aperture rather than merely in shadow. The graded penumbra just past a limiting ray, which is the part of a shadow zone worth having, is finite and carries the beams’ tails. Many ordinary cases have no infinity at all: an isovelocity 1000 m guide at 300 Hz over 10 km, everything default, has none in 80200 cells. The source column is not one of the infinities, and is not to be read. parabolic_equation divides by and so genuinely diverges at r = 0; the beam sum does not, and hands back a finite number there instead, 13.6 dB in the case above. It means nothing, and neither does anything else within about three initial beam widths of the source: see gaussian_beams on why this method has no near field. The plausible size of these numbers is the point worth knowing about them.
pressureThe complex field the loss was taken from, same shape, in the module’s own convention (the conjugate of the one Jensen Eq. (3.88) is printed in) and normalised to unit pressure at 1 m, so propagation_loss = -20 lg|pressure|.
launch_anglesLaunch angle of each beam’s central ray, from the horizontal, in degrees.
ray_rangesRange of each central ray at each marching step, in metres, shape (n_beams, n_steps). This is the marching grid, which is finer than (and independent of) ranges.
ray_depthsDepth of each central ray on that grid, in metres.
beam_widthsBeam half-width on that grid, in metres: Jensen Eq. (3.89), the distance at which the beam’s own pressure has fallen by and its intensity by .
wavefront_curvaturesBeam wavefront curvature on that grid, in 1/m: Jensen Eq. (3.90) with the sign that belongs to the conjugated field this result exposes, so that a beam spreading in free space reproduces Eq. (3.85), , as a positive number.
initial_beam_widthsThe of Eq. (3.91) actually used by each beam of the fan, in metres, shape (n_beams,). An explicit fan.beam_width fills it with one value; the default is per launch angle (see _default_beam_widths), widest on the axis of the fan whenever a shallow channel’s modal-resolution term is in play and flat across it otherwise.
absorption_modelThe seawater absorption model applied along the beams, or None when the run propagated without volume absorption (the default).
absorption_coefficientThe absorption coefficient actually applied, in dB/km (0.0 when absorption_model is None), as seawater_absorption evaluated it at the source frequency and depth. Recorded so a run’s loss can be decomposed without re-deriving what was subtracted.
seabed_densitySediment density of the fluid seabed the bottom bounces were charged with, or None when the bottom was one of the perfect reflectors (the default).
seabed_sound_speedSediment sound speed of that seabed, in m/s, or None likewise. Together the pair names the Rayleigh interface of reflection_coefficient each beam’s bottom reflections multiplied it by.
source_depthSource depth, in metres.
water_depthWater-column depth, in metres: the sound-speed profile’s last depth, which over a sloping bottom is the deepest water the medium description reaches while the column itself is the bathymetry below.
bathymetry_rangesNode ranges of the bottom profile the run was marched over, in metres, or None for the level bottom (the default). When present, the per-beam histories (ray_depths, beam_widths, wavefront_curvatures) are NaN from the column at which a beam was terminated by a reflection past the vertical, the same convention RayTraceResult uses and for the same reason: from there on the beam no longer exists in the forward field, and its weight in the sum is zero.
bathymetry_depthsBottom depth at each of those nodes, in metres (None likewise).
GaussianBeamResult.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot the propagation-loss field (depth increasing downward).

normal_modes(
frequency_hz: float,
depths: NDArray[np.float64] | list[float],
sound_speeds: NDArray[np.float64] | list[float],
*,
source_depth: float,
receiver_depth: float,
ranges_m: NDArray[np.float64] | list[float] | None = None,
density: float = 1000.0,
bottom: str = 'pressure-release',
n_depth_points: int | None = None,
) -> NormalModeResult

Normal-mode propagation loss for a range-independent waveguide.

Solves the depth-separated Sturm-Liouville problem (Jensen Eq. 5.3) on a uniform finite-difference grid, then assembles the coherent propagation loss from the propagating modes (Eq. 5.17).

The finite-difference eigenvalues carry an error that grows with the mode’s vertical wavenumber, so near-cutoff modes need a fine grid. Two guards apply: eigenvalues inside the scheme’s error band () are discarded as numerically indistinguishable from cutoff, and a PhonometryWarning is emitted when a retained mode sits within ten times that band (increase n_depth_points to resolve it).

Parameters

NameDescription
frequency_hzSource frequency, in Hz.
depthsDepth samples of the sound-speed profile, in metres, starting at the surface z = 0 and strictly increasing to the bottom.
sound_speedsSound speed at each depth, in m/s.
source_depthSource depth zs, in metres.
receiver_depthReceiver depth for the propagation-loss slice, in m.
ranges_mRanges at which to evaluate the loss, in metres; defaults to 100 m to 10 km.
densityWater density (constant), in kg/m3.
bottom"pressure-release" (default) or "rigid".
n_depth_pointsNumber of finite-difference depth points. Default (None): derived from the physics as , which keeps the near-cutoff eigenvalue error small at any frequency/depth combination, capped at 20 000 points (very high products exceed the cap; the near-cutoff warning then indicates whether the capped grid suffices, and an explicit n_depth_points overrides the cap).

Returns: A NormalModeResult.

Raises

ExceptionWhen
ValueErrorIf the inputs are invalid.
NormalModeResult(
frequency: float,
wavenumbers: NDArray[np.float64],
mode_depths: NDArray[np.float64],
mode_functions: NDArray[np.float64],
ranges: NDArray[np.float64],
propagation_loss: NDArray[np.float64],
receiver_depth: float,
source_depth: float,
)

Normal-mode solution of a range-independent waveguide.

Attributes

NameDescription
frequencySource frequency, in Hz.
wavenumbersHorizontal wavenumbers krm of the propagating modes, in rad/m (descending order).
mode_depthsDepth grid of the mode functions, in metres.
mode_functionsOrthonormalised mode shapes Ψm(z), shape (n_modes, n_depths).
rangesRanges at which the propagation loss is evaluated, in metres.
propagation_lossCoherent propagation loss at receiver_depth per range, in dB.
receiver_depthReceiver depth of the propagation-loss slice, in m.
source_depthSource depth, in metres.
NormalModeResult.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot the propagation loss versus range (loss increasing downward).

parabolic_equation(
frequency_hz: float,
depths: NDArray[np.float64] | list[float],
sound_speeds: NDArray[np.float64] | list[float],
*,
source_depth: float,
max_range: float = 10000.0,
range_step: float = 10.0,
n_depth_points: int = 1024,
) -> ParabolicEquationResult

Propagation-loss field from the standard (Tappert) parabolic equation.

Marches the split-step Fourier solution (Jensen Ch. 6) in range with a discrete sine transform in depth, enforcing a pressure-release surface at z = 0 and bottom at z = water_depth. The envelope is related to pressure by and (Eqs. 6.70-6.71), using a Gaussian starter.

The standard PE is paraxial: it is accurate for propagation within roughly ±15-20° of the horizontal (Jensen §6.2). Steep modes therefore carry a phase error that shows at short and intermediate range in shallow-waveguide problems (a few dB against the exact field below a few water depths of range), converging at long range; the free-field calibration itself is exact to ~1e-4 dB at the default range_step.

Parameters

NameDescription
frequency_hzSource frequency, in Hz.
depthsDepth samples of the profile, in metres, from z = 0.
sound_speedsSound speed at each depth, in m/s.
source_depthSource depth, in metres.
max_rangeMaximum range, in metres.
range_stepRange marching step , in metres.
n_depth_pointsNumber of depth points (interior sine-transform grid).

Returns: A ParabolicEquationResult.

Raises

ExceptionWhen
ValueErrorIf the inputs are invalid.
ParabolicEquationResult(
frequency: float,
ranges: NDArray[np.float64],
depths: NDArray[np.float64],
propagation_loss: NDArray[np.float64],
source_depth: float,
)

Parabolic-equation propagation-loss field.

Attributes

NameDescription
frequencySource frequency, in Hz.
rangesRange grid, in metres.
depthsDepth grid, in metres.
propagation_lossPropagation-loss field PL(z, r), in dB, shape (n_depths, n_ranges).
source_depthSource depth, in metres.
ParabolicEquationResult.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot the propagation-loss field (depth increasing downward).

ray_trace(
depths: NDArray[np.float64] | list[float],
sound_speeds: NDArray[np.float64] | list[float],
*,
source_depth: float,
launch_angles_deg: NDArray[np.float64] | list[float],
max_range: float = 10000.0,
n_steps: int = 2000,
bathymetry: tuple[NDArray[np.float64] | list[float], NDArray[np.float64] | list[float]] | None = None,
) -> RayTraceResult

Trace acoustic rays through a range-independent sound-speed profile.

Integrates the ray-trajectory equations (Jensen Eqs. 3.23-3.24) with a fixed-step fourth-order Runge-Kutta scheme, reflecting at the pressure-release surface (z = 0) and the bottom (z = water_depth).

The bottom may slope. Passing the bathymetry pair replaces the level bottom with a piecewise-linear depth profile depth(r), the faceted boundary model of Jensen Fig. 3.20, and the first range dependence in this module; the sound-speed profile stays range independent (see the scope note below). The marcher then finds each boundary crossing against the interpolated polyline and reflects the ray specularly about the local facet (Eq. 3.121), so a bounce off a slope of angle changes the ray’s inclination by : upslope bounces steepen a ray, which is the whole one-line physics of wedge propagation, and downslope bounces flatten it. Snell’s invariant is therefore no longer a constant of each ray but a constant between its bottom bounces, and one consequence is drawn honestly rather than papered over: a ray steepened past the vertical runs backward in range, which a marcher whose independent variable is range cannot carry (the same one-way surgery the parabolic equation performs), so such a ray is terminated at that bounce and its samples are NaN from there on — see RayTraceResult.bathymetry_ranges. The polyline continues level past its last node exactly as numpy.interp clamps the sound-speed profile; a bathymetric feature narrower than one range step can hide between two samples of the crossing search, so n_steps must resolve the bathymetry as well as the rays.

Scope, stated plainly. Range dependence enters here through the boundary alone: full is not implemented, deliberately. Every solver in this module ships with an exact published oracle, and the sloping-bottom geometry has one (the ideal wedge unfolds into a closed fan of images, which is what the tests hold it to), while a range-dependent water column has none: there is no closed form to hold a marcher to, so it would ship on trust, and this module does not ship on trust.

The travel time is a third state of that same Runge-Kutta step rather than a quadrature run over the finished path: with the range-invariant Snell parameter it obeys , so it is integrated with the very stages that place the ray and cannot drift from the geometry actually returned. The arc length is a fourth state on the same footing, , because it is the measure volume absorption needs (see RayTraceResult) and reading it off the finished path would demote it to first order. This is the same ray core, and the same travel-time equation, as the atmospheric atmospheric_ray_paths (which reflects at the ground instead of at the sea surface). Reflections cost no time and no path, so both odometers stay continuous across them. They are counted, though, per boundary: see RayTraceResult on why the two cumulative counts, with the crossing angle Snell’s invariant fixes per ray, are the entire per-bounce record a downstream amplitude needs.

Parameters

NameDescription
depthsDepth samples of the profile, in metres, from z = 0.
sound_speedsSound speed at each depth, in m/s.
source_depthSource depth, in metres.
launch_angles_degLaunch angles from the horizontal, in degrees (positive downward).
max_rangeMaximum horizontal range to trace, in metres.
n_stepsNumber of integration steps per ray.
bathymetryA (ranges_m, depths_m) pair of arrays describing a piecewise-linear bottom profile: node ranges in metres, strictly increasing from r = 0 and level past the last node, with the bottom depth at each node in metres, strictly positive and never below the sound-speed profile’s last depth. Default (None): the level bottom at the profile’s last depth.

Returns: A RayTraceResult.

Raises

ExceptionWhen
ValueErrorIf the inputs are invalid.
RayTraceResult(
launch_angles: NDArray[np.float64],
ranges: NDArray[np.float64],
depths: NDArray[np.float64],
travel_times: NDArray[np.float64],
arc_lengths: NDArray[np.float64],
surface_reflections: NDArray[np.int_],
bottom_reflections: NDArray[np.int_],
source_depth: float,
water_depth: float,
profile_depths: NDArray[np.float64],
profile_speeds: NDArray[np.float64],
bathymetry_ranges: NDArray[np.float64] | None = None,
bathymetry_depths: NDArray[np.float64] | None = None,
)

Ray-tracing solution through a sound-speed profile.

Attributes

NameDescription
launch_anglesLaunch angles from the horizontal, in degrees.
rangesPer-ray horizontal ranges, in metres, shape (n_rays, n_steps).
depthsPer-ray depths, in metres, shape (n_rays, n_steps).
travel_timesPer-ray cumulative travel times, in seconds, shape (n_rays, n_steps) (zero at the source, increasing along the ray).
arc_lengthsPer-ray cumulative arc length along the ray, in metres, same shape (zero at the source). It is never less than the range column it stands in, exceeds it by the obliquity of the path, and a reflection leaves it continuous. This, and not the range, is the measure seawater absorption acts along: Jensen Sect. 3.6.2 carries a volume loss into the ray solution by perturbing the eikonal and lands on (Eq. 3.116), an integral over the path actually flown, so a caller hanging amplitudes on these rays multiplies by with the read off here.
surface_reflectionsPer-ray cumulative count of sea-surface reflections by each range sample, same shape (zero at the source).
bottom_reflectionsThe same count for the seabed. The two counts, and not the reflection coefficients themselves, are the whole of the per-bounce record an amplitude carrier needs from the geometry. Jensen Sect. 3.6.3 treats a boundary interaction as multiplying the ray amplitude by and adding to its phase (Eqs. 3.125-3.126), with the local angle of incidence; and in a range-independent medium that angle is the same at every touch of the same flat boundary, because the direction a ray crosses a depth with is fixed by Snell’s invariant, , not by how many times it has bounced. Any boundary coefficient therefore enters a path’s amplitude only as with the read off here, which is how gaussian_beams charges its lossy seabed and how eigenrays charges each arrival; ray_trace itself carries no amplitude, so the counts are what it can meaningfully expose.
source_depthSource depth, in metres.
water_depthWater-column depth, in metres.
profile_depthsDepth samples of the sound-speed profile the rays were traced through, in metres, exactly as cleaned on the way in. The pair below is the medium (both solvers interpolate it piecewise linearly and nothing else about the water enters the geometry), so recording it makes the result self-contained: eigenrays needs it to put fresh rays through the same water the fan flew.
profile_speedsSound speed at each of those depths, in m/s.
bathymetry_rangesNode ranges of the bottom profile the rays were traced over, in metres, or None for the level bottom at water_depth (the default). With a sloping bottom two of the flat record’s invariants fall, and the arrays here say so honestly rather than quietly keep their old meaning. A ray reflected past the vertical by the accumulating slope cannot be carried by a range march (see ray_trace); its depths, travel_times and arc_lengths are NaN from the sample of the terminating bounce on, so a plot simply ends where the ray turned and nothing downstream can mistake a frozen sample for a traced one (the reflection counts, being integers, instead hold their last value). And the crossing angle at the bottom is no longer one per ray: each slope bounce rotates Snell’s invariant, so the per-bounce record that sufficed for a flat guide (counts alone) does not price a sloping one, which is why eigenrays declines such a trace.
bathymetry_depthsBottom depth at each of those nodes, in metres (None likewise). Between nodes the bottom is the straight facet, beyond the last node it continues level: exactly the boundary the marcher reflected off.
RayTraceResult.plot(
ax: Axes | None = None,
*,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot the ray paths (depth increasing downward).

VolumeAbsorption(
model: str,
temperature: float = 10.0,
salinity: float = 35.0,
ph: float = 8.0,
)

Seawater volume absorption: a model and the water it is evaluated in.

The same models, spelled the same way, as seawater_absorption; gaussian_beams evaluates the coefficient once, at the source frequency and the source depth, and charges it along each beam’s own arc length (Jensen Eq. 3.116). Passing the bare model name to the solver is the same as passing this class with its defaults.

Attributes

NameDescription
model"francois-garrison", "ainslie-mccolm" or "thorp".
temperatureTemperature T, in degrees Celsius.
salinitySalinity S, in parts per thousand.
phAcidity (Thorp ignores it always).