Ir al contenido

simulation.fdtd

Esta página aún no está disponible en tu idioma.

2D acoustic finite-difference time-domain (FDTD) simulation.

A staggered-grid (Yee-style) pressure-velocity leapfrog solver for the linear acoustic equations in a non-moving medium, following the reference formulation of Attenborough & Van Renterghem, Predicting Outdoor Sound (2nd ed., CRC Press 2021), chapter 4:

  • the governing first-order system in p and v (Eqs. 4.3-4.4);
  • the staggered-in-place, staggered-in-time discretisation (Eqs. 4.11-4.12), with pressure at cell centres and velocity components on cell faces;
  • the Courant stability condition CN <= 1 with CN = c dt sqrt(1/dx^2 + 1/dy^2) (Eqs. 4.13-4.14);
  • rigid boundaries as zero normal face velocity (Eq. 4.32) and the frequency-independent real-impedance boundary update (Eqs. 4.33-4.35);
  • absorbing edges as a graded sponge layer, the simple precursor of the perfectly-matched-layer treatment discussed in section 4.2.3.

Two API levels are exposed. fdtd_simulation is the result-object entry point: it builds the grid, runs a deterministic simulation and returns a frozen FDTDResult with per-probe pressure histories, optional field snapshots and a .plot() method. FDTD2D is the underlying stepping engine (also used by the documentation animations) for callers that need frame-by-frame access to the field.

The solver is deliberately deterministic: float64 arithmetic throughout, no random numbers and single-threaded numpy execution, so identical inputs give bit-identical outputs on the same platform.

Validated against analytic oracles: the eigenfrequencies of a rigid rectangular box and of an effectively 1D tube, the numerical dispersion relation of the leapfrog scheme (the discrete counterpart of Eq. 4.15), free-field pulse arrival times and cylindrical 1/sqrt(r) amplitude decay, the image-source echo of a rigid wall, the normal-incidence reflection coefficient (Z - rho c)/(Z + rho c) of an impedance edge, and second-order convergence under grid refinement.

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

CWSource(
ix: int,
iy: int,
frequency: float,
amplitude: float = 1.0,
ramp_cycles: float = 3.0,
)

A continuous sine drive with a smooth cosine-ramped onset.

The first ramp_cycles periods fade the amplitude in with a raised cosine so the start does not splash a broadband transient over the field.

Attributes

NameDescription
ixSource column (x) index.
iySource row (y) index.
frequencyDrive frequency [Hz].
amplitudeSteady-state source amplitude [Pa].
ramp_cyclesOnset ramp length in periods of frequency.
CWSource.value(t: float) -> float

Source waveform at time t (seconds).

FDTD2D(
c: float | Field2D,
dx: float,
*,
rho: float | Field2D = 1.2,
cfl: float = 0.6,
sponge_width: int = 0,
sponge_sides: str | Iterable[str] | None = None,
sponge_reflection: float = 0.0001,
damping: float = 0.0,
shape: tuple[int, int] | None = None,
edge_impedance: Mapping[str, float | NDArray[np.float64]] | None = None,
obstacle_mask: NDArray[np.bool_] | None = None,
)

2D acoustic FDTD stepping engine on a staggered grid.

Pressure p lives at cell centres, shape (ny, nx) (row = y, column = x, the imshow convention); vx at interior x-faces, shape (ny, nx - 1); vy at interior y-faces, shape (ny - 1, nx). Because only interior faces are stored, the domain boundary is perfectly rigid (zero normal velocity, Eq. 4.32) by construction; sponge layers and per-cell real impedances turn selected sides into absorbing or locally reacting boundaries. Sources are soft (additive) pressure injections.

Parameters

NameDescription
cSound-speed map [m/s], shape (ny, nx). A scalar with an explicit shape is also accepted.
dxGrid spacing [m] (square cells).
rhoDensity map [kg/m3]; scalar or (ny, nx) array (default 1.2).
cflCourant number CN = c_max dt sqrt(2) / dx (Eq. 4.13); the explicit scheme is stable for CN <= 1 (Eq. 4.14) and values in (0, 1) are accepted. The default 0.6 keeps a wide stability margin with moderate numerical dispersion.
sponge_widthThickness of the absorbing layer in cells (0 = no absorbing sides).
sponge_sidesWhich sides absorb: a single side name or an iterable drawn from {"left", "right", "top", "bottom"} (default: all four when sponge_width > 0). left/right are the low/high column edges and top/bottom the low/high row edges (the default imshow origin).
sponge_reflectionTarget round-trip amplitude reflection of the sponge layer; sets the peak absorption rate.
dampingUniform bulk amplitude decay rate [1/s] applied to the whole field (a simple stand-in for air/wall absorption; 6.91 / T60 gives a T60 seconds reverberant decay).
shapeGrid shape (ny, nx), required only when c is a scalar.
edge_impedanceLocally reacting boundary sides: a mapping from side name to a real specific acoustic impedance [Pa s/m], either a scalar or a per-edge-cell 1D array (length ny for left/ right, nx for top/bottom). Implements Eqs. (4.33)-(4.35); Z = rho c is a normal-incidence matched (anechoic) edge. A side cannot be both a sponge and an impedance boundary.
obstacle_maskBoolean map, shape (ny, nx), of rigid cells: every face adjacent to a masked cell is closed (zero normal velocity, Eq. 4.32), rasterising arbitrary interior geometry.
FDTD2D.add_source(source: Source) -> None

Register a soft pressure source (additive injection at one cell).

FDTD2D.energy() -> float

Total acoustic field energy [J per metre of depth].

FDTD2D.run(
steps: int,
record_every: int | None = None,
decimate: int = 1,
) -> NDArray[np.float64]

Advance steps steps, optionally recording pressure frames.

With record_every = k a snapshot of p is stored after every k-th step (and one of the initial state), spatially subsampled by decimate; the stacked (n_frames, ny', nx') array plugs straight into a FuncAnimation imshow update. Without record_every an empty array is returned and only the final state is kept (read it from self.p).

FDTD2D.step() -> None

Advance the leapfrog scheme by one time step.

property

Elapsed simulated time [s].

fdtd_simulation(
c: float | Field2D,
dx: float,
duration: float,
*,
sources: Sequence[Source],
shape: tuple[int, int] | None = None,
rho: float | Field2D = 1.2,
cfl: float = 0.6,
probes: Sequence[tuple[int, int]] = (),
boundaries: str | Mapping[str, str | float | NDArray[np.float64]] = 'rigid',
absorbing_layer_cells: int = 20,
obstacle_mask: NDArray[np.bool_] | None = None,
damping: float = 0.0,
snapshot_every: int | None = None,
) -> FDTDResult

Run a deterministic 2D acoustic FDTD simulation.

Builds the staggered-grid domain (Attenborough & Van Renterghem 2021, Eqs. 4.11-4.12), applies the requested boundary conditions, injects the sources and integrates for duration seconds, recording the pressure at every probe each time step and, optionally, full-field snapshots.

The grid covers (nx * dx, ny * dx) metres; a cell index (ix, iy) maps to the physical cell centre ((ix + 0.5) * dx, (iy + 0.5) * dx). Resolve at least 10 cells per shortest wavelength using the smallest sound speed of the domain (dx <= c_min / (10 f)), the usual rule for this lowest-order scheme: the worst-case (on-axis) numerical dispersion error magnitude, (k dx)^2 / 24 from the discrete counterpart of Eq. 4.15 (the modelled frequency under-reads, so the signed error is negative), is then about 1.6 % (about 1.4 % at the default cfl; in a heterogeneous domain the slower cells run at a lower local Courant number and sit nearer the 1.6 % bound) and finer grids reduce it quadratically. The simulation is 2D, so a point source is physically a line source with cylindrical 1/sqrt(r) amplitude spreading rather than the 3D spherical 1/r.

Parameters

NameDescription
cSound-speed map [m/s], shape (ny, nx), or a scalar with an explicit shape.
dxGrid spacing [m] (square cells).
durationPhysical time to simulate [s].
sourcesOne or more of GaussianPulse, CWSource or SignalSource.
shapeGrid shape (ny, nx), required when c is a scalar.
rhoDensity map [kg/m3]; scalar or (ny, nx) array.
cflCourant number in (0, 1) (Eqs. 4.13-4.14); the time step is dt = cfl * dx / (c_max * sqrt(2)). Default 0.6.
probesPressure-probe cells as (ix, iy) index pairs.
boundaries"rigid" (default), "absorbing", or a mapping from side name (left/right/top/bottom) to "rigid", "absorbing", or a real specific impedance [Pa s/m] (scalar or per-edge-cell 1D array, Eqs. 4.33-4.35).
absorbing_layer_cellsSponge-layer thickness for absorbing sides, in cells.
obstacle_maskBoolean map, shape (ny, nx), of rigid cells (rasterised interior geometry).
dampingUniform bulk amplitude decay rate [1/s].
snapshot_everyRecord a full pressure-field snapshot every this many steps (and at t = 0); None records none.

Returns: A FDTDResult.

Raises

ExceptionWhen
ValueErrorIf the inputs are invalid.
FDTDResult(
times: NDArray[np.float64],
pressures: NDArray[np.float64],
probes: NDArray[np.int_],
probe_positions: NDArray[np.float64],
dx: float,
dt: float,
shape: tuple[int, int],
sources: tuple[Source, ...],
snapshots: NDArray[np.float64] | None,
snapshot_times: NDArray[np.float64] | None,
obstacle_mask: NDArray[np.bool_] | None,
)

Frozen result of a fdtd_simulation run.

Attributes

NameDescription
timesTime axis [s], length n_steps + 1 (includes t = 0).
pressuresPressure history at each probe [Pa], shape (n_probes, n_steps + 1).
probesProbe cell indices (ix, iy), shape (n_probes, 2).
probe_positionsProbe cell-centre positions (x, y) [m], shape (n_probes, 2).
dxGrid spacing [m].
dtTime step [s].
shapeGrid shape (ny, nx).
sourcesThe source definitions of the run.
snapshotsRecorded pressure fields, shape (n_frames, ny, nx), or None when no snapshots were requested.
snapshot_timesTime of each snapshot [s], or None.
obstacle_maskBoolean map of rigid cells, or None.
FDTDResult.plot(
ax: Axes | None = None,
*,
kind: str = 'probes',
frame: int = -1,
language: str = 'en',
**kwargs: Any,
) -> Axes

Plot the probe histories or one recorded field snapshot.

Parameters

NameDescription
axExisting axes, or None to create a figure.
kind"probes" (default) draws the per-probe pressure time histories; "snapshot" renders one recorded pressure field with the geometry overlaid (imshow raster).
frameSnapshot index for kind="snapshot" (default: the last recorded frame).
languageLabel language, "en" (default) or "es".
kwargsForwarded to the underlying plot/imshow.

Returns: The axes.

property

Domain size (lx, ly) [m].

GaussianPulse(
ix: int,
iy: int,
width: float,
t0: float | None = None,
amplitude: float = 1.0,
)

A soft Gaussian pressure pulse injected at one cell.

s(t) = amplitude * exp(-((t - t0) / width)**2) with t0 defaulting to 4 * width so the pulse starts from (numerically) zero.

Attributes

NameDescription
ixSource column (x) index; the cell centre is at x = (ix + 0.5) * dx.
iySource row (y) index.
widthGaussian half-width [s]; sets the pulse bandwidth.
t0Pulse centre time [s] (default 4 * width).
amplitudePeak source amplitude [Pa].
GaussianPulse.value(t: float) -> float

Source waveform at time t (seconds).

SignalSource(
ix: int,
iy: int,
samples: NDArray[np.float64],
sample_rate: float,
amplitude: float = 1.0,
)

An arbitrary sampled waveform injected at one cell.

The samples are interpreted as the source signal at sample_rate and linearly interpolated onto the simulation time steps; outside the sampled span the source is zero. sample_rate therefore does not need to match the simulation rate 1/dt, although a rate well above the highest frequency of interest avoids interpolation roll-off.

Attributes

NameDescription
ixSource column (x) index.
iySource row (y) index.
samplesSource signal samples [Pa] (stored as a read-only 1D float64 array).
sample_rateSampling rate of samples [Hz].
amplitudeScale factor applied to the samples.
SignalSource.value(t: float) -> float

Source waveform at time t (seconds), zero outside the span.