tidal.measurement package#

Measurement and analysis tools for coupled-field simulations.

Provides energy computation, conversion probability, mixing length extraction, spectral decomposition, and energy conservation diagnostics for simulation output from the TIDAL Lagrangian-to-PDE pipeline.

Typical usage:

from tidal.measurement import SimulationData, compute_conversion_probability
from tidal.measurement import compute_mixing_length, compute_mixing_spectrum

data = SimulationData.from_storage(storage, spec, grid, params)
# Or from a snapshot directory (memory-mapped, O(1) RAM):
# data = SimulationData.load("output_dir", spec)
result = compute_conversion_probability(data, "phi_0", "chi_0")
mixing = compute_mixing_length(result)
print(f"L_mix = {mixing.mixing_length:.4f} +/- {mixing.mixing_length_uncertainty:.4f}")
class tidal.measurement.AsymptoticConversionResult(P_final, P_reflected, P_transmitted, E_source_initial, E_target_final, source_field, target_field, source_wavevector)[source]#

Bases: object

Result of asymptotic conversion measurement.

Parameters:
P_final#

Total conversion at final snapshot: E_target(t_final) / E_source(0).

Type:

float

P_reflected#

Fraction of total conversion in backward-propagating target modes.

Type:

float

P_transmitted#

Fraction of total conversion in forward-propagating target modes.

Type:

float

E_source_initial#

Source group energy at t=0.

Type:

float

E_target_final#

Target group energy at final snapshot.

Type:

float

source_field#

Source field group name.

Type:

str

target_field#

Target field group name.

Type:

str

source_wavevector#

Spectral centroid wavevector of the source at t=0 (defines the forward direction).

Type:

tuple[float, …]

P_final: float#
P_reflected: float#
P_transmitted: float#
E_source_initial: float#
E_target_final: float#
source_field: str#
target_field: str#
source_wavevector: tuple[float, ...]#
class tidal.measurement.ConversionResult(times, probability, source_energy, target_energy, total_energy, relative_energy_error, source_field, target_field)[source]#

Bases: object

Result of a conversion probability measurement.

Parameters:
  • times (NDArray[np.float64])

  • probability (NDArray[np.float64])

  • source_energy (NDArray[np.float64])

  • target_energy (NDArray[np.float64])

  • total_energy (NDArray[np.float64])

  • relative_energy_error (NDArray[np.float64])

  • source_field (str)

  • target_field (str)

times#

Snapshot times.

Type:

ndarray, shape (n_snapshots,)

probability#

P(t) = E_target(t) / E_source(0).

Type:

ndarray, shape (n_snapshots,)

source_energy#

Source field energy over time.

Type:

ndarray, shape (n_snapshots,)

target_energy#

Target field energy over time.

Type:

ndarray, shape (n_snapshots,)

total_energy#

Total system energy (source + target) over time.

Type:

ndarray, shape (n_snapshots,)

relative_energy_error#

(E_total(t) - E_total(0)) / E_total(0).

Type:

ndarray, shape (n_snapshots,)

source_field#

Name of the source field.

Type:

str

target_field#

Name of the target field.

Type:

str

times: NDArray[np.float64]#
probability: NDArray[np.float64]#
source_energy: NDArray[np.float64]#
target_energy: NDArray[np.float64]#
total_energy: NDArray[np.float64]#
relative_energy_error: NDArray[np.float64]#
source_field: str#
target_field: str#
class tidal.measurement.CriticalFieldResult(rows, field_param, threshold, metric, outer_params)[source]#

Bases: object

Result of critical field extraction.

Parameters:
rows#

One dict per outer-param combination. Keys include outer param values plus B_min, inv_B_min, error columns, and quality.

Type:

list[dict[str, Any]]

field_param#

Name of the field-strength parameter that was collapsed.

Type:

str

threshold#

Metric threshold used for the crossing.

Type:

float

metric#

Metric name used for thresholding.

Type:

str

outer_params#

Remaining swept parameters after collapsing field_param.

Type:

dict[str, list[float]]

rows: list[dict[str, Any]]#
field_param: str#
threshold: float#
metric: str#
outer_params: dict[str, list[float]]#
class tidal.measurement.DispersionResult(wavenumbers, frequencies, power, peak_frequencies, peak_powers, field_name, rayleigh_resolution)[source]#

Bases: object

Dispersion relation extracted from simulation output.

Parameters:
  • wavenumbers (NDArray[np.float64])

  • frequencies (NDArray[np.float64])

  • power (NDArray[np.float64])

  • peak_frequencies (NDArray[np.float64])

  • peak_powers (NDArray[np.float64])

  • field_name (str)

  • rayleigh_resolution (float)

wavenumbers#

Radially binned wavenumber magnitudes |k|.

Type:

ndarray, shape (n_modes,)

frequencies#

Angular frequencies omega (rad/time), excluding DC.

Type:

ndarray, shape (n_freq,)

power#

Spectral power S(k, omega) = sum_i |A_hat_i(k, omega)|^2 summed over all fields in the group.

Type:

ndarray, shape (n_modes, n_freq)

peak_frequencies#

Dominant angular frequency at each k-bin (0.0 for inactive modes).

Type:

ndarray, shape (n_modes,)

peak_powers#

Spectral power at the dominant frequency per k-bin.

Type:

ndarray, shape (n_modes,)

field_name#

Which field (or comma-joined group) this dispersion was computed for.

Type:

str

rayleigh_resolution#

2*pi/T – minimum resolvable frequency difference.

Type:

float

wavenumbers: NDArray[np.float64]#
frequencies: NDArray[np.float64]#
power: NDArray[np.float64]#
peak_frequencies: NDArray[np.float64]#
peak_powers: NDArray[np.float64]#
field_name: str#
rayleigh_resolution: float#
class tidal.measurement.EffectiveMassResult(m2_eff, m2_eff_std, m2_eff_values, wavenumbers, frequencies, n_active_modes, field_name)[source]#

Bases: object

Result of effective mass extraction.

Parameters:
  • m2_eff (float)

  • m2_eff_std (float)

  • m2_eff_values (NDArray[np.float64])

  • wavenumbers (NDArray[np.float64])

  • frequencies (NDArray[np.float64])

  • n_active_modes (int)

  • field_name (str)

m2_eff#

Median effective mass squared across active modes.

Type:

float

m2_eff_std#

Standard deviation of m² across active modes (spread indicator).

Type:

float

m2_eff_values#

Per-mode m² = ω² - k² for all active modes.

Type:

ndarray

wavenumbers#

Wavenumber |k| for each active mode.

Type:

ndarray

frequencies#

Dominant ω(k) for each active mode.

Type:

ndarray

n_active_modes#

Number of active k-modes used in the estimate.

Type:

int

field_name#

Field group name (comma-joined).

Type:

str

m2_eff: float#
m2_eff_std: float#
m2_eff_values: NDArray[np.float64]#
wavenumbers: NDArray[np.float64]#
frequencies: NDArray[np.float64]#
n_active_modes: int#
field_name: str#
class tidal.measurement.EnergyDiagnostics(times, total_energy, relative_error, max_relative_error, is_conserved)[source]#

Bases: object

Energy conservation diagnostic result.

Parameters:
  • times (NDArray[np.float64])

  • total_energy (NDArray[np.float64])

  • relative_error (NDArray[np.float64])

  • max_relative_error (float)

  • is_conserved (bool)

times#
Type:

ndarray, shape (n_snapshots,)

total_energy#

Spatially-averaged energy density ⟨ε⟩ at each snapshot.

Type:

ndarray, shape (n_snapshots,)

relative_error#

(⟨ε⟩(t) - ⟨ε⟩(0)) / ⟨ε⟩(0).

Type:

ndarray, shape (n_snapshots,)

max_relative_error#

Peak relative energy density drift.

Type:

float

is_conserved#

Whether max_relative_error < threshold.

Type:

bool

times: NDArray[np.float64]#
total_energy: NDArray[np.float64]#
relative_error: NDArray[np.float64]#
max_relative_error: float#
is_conserved: bool#
class tidal.measurement.FieldEnergy(kinetic, gradient, mass, total)[source]#

Bases: object

Energy density decomposition for a single field at one snapshot.

All values are spatially-averaged energy densities ⟨ε⟩ = E / V_domain.

Parameters:
kinetic#

0.5 * ⟨π²⟩

Type:

float

gradient#

0.5 * ⟨|∇_self φ|²⟩ — gradient energy density over self-laplacian axes only. For scalar fields (full laplacian), this equals 0.5 * ⟨|∇φ|²⟩. For vector components with directional laplacians (e.g. laplacian_y), only the relevant axes.

Type:

float

mass#

0.5 * * ⟨φ²⟩

Type:

float

total#

Sum of kinetic + gradient + mass.

Type:

float

kinetic: float#
gradient: float#
mass: float#
total: float#
class tidal.measurement.MixingResult(mixing_length, mixing_length_uncertainty, dominant_frequency, frequency_fwhm, max_conversion, peaks)[source]#

Bases: object

Result of spectral mixing length extraction.

The mixing length is derived from the dominant peak of the temporal FFT of P(t), rather than from time-domain peak detection. This correctly identifies the physically meaningful oscillation timescale even in multi-scale systems where rapid noise oscillations sit on top of a slower mixing envelope.

The uncertainty comes from the half-width at half-maximum (HWHM) of the spectral peak: dL = (pi/omega**2) * HWHM where HWHM = FWHM / 2. A sharp spectral peak (small FWHM) gives a precise mixing length; a broad peak indicates the oscillation frequency is less well-defined.

Parameters:
mixing_length#

pi/omega_dom — half-period of the dominant oscillation frequency.

Type:

float

mixing_length_uncertainty#

Propagated from HWHM of the dominant spectral peak.

Type:

float

dominant_frequency#

omega_dom — angular frequency of the strongest spectral peak.

Type:

float

frequency_fwhm#

FWHM of the dominant peak (rad/time).

Type:

float

max_conversion#

max(P(t)) — peak conversion probability over the full timeseries.

Type:

float

peaks#

All detected spectral peaks, sorted by power descending. peaks[0] is the dominant peak.

Type:

tuple of SpectralPeak

mixing_length: float#
mixing_length_uncertainty: float#
dominant_frequency: float#
frequency_fwhm: float#
max_conversion: float#
peaks: tuple[SpectralPeak, ...]#
class tidal.measurement.MixingSpectrum(frequencies, power, dominant_frequency, dominant_mixing_length, rayleigh_resolution)[source]#

Bases: object

Frequency decomposition of the conversion probability timeseries.

Reports angular frequencies at which P(t) oscillates — no theory-specific conversion. For each frequency omega, the half-period pi/omega gives the mixing timescale at that frequency.

The frequency resolution is rayleigh_resolution = 2*pi/T where T is the observation window duration.

Parameters:
  • frequencies (NDArray[np.float64])

  • power (NDArray[np.float64])

  • dominant_frequency (float)

  • dominant_mixing_length (float)

  • rayleigh_resolution (float)

frequencies#

Angular frequencies omega (rad/time), excluding DC.

Type:

ndarray

power#

|P_hat(omega)|**2 at each frequency.

Type:

ndarray

dominant_frequency#

omega of the strongest oscillation peak.

Type:

float

dominant_mixing_length#

pi / dominant_frequency — half-period at the dominant frequency.

Type:

float

rayleigh_resolution#

2*pi/T — fundamental frequency resolution from the observation window duration. This is the minimum resolvable frequency difference.

Type:

float

frequencies: NDArray[np.float64]#
power: NDArray[np.float64]#
dominant_frequency: float#
dominant_mixing_length: float#
rayleigh_resolution: float#
class tidal.measurement.ResonanceResult(wavenumbers, omega_source, omega_target, resonance_mismatch, resonant_modes, n_resonant_modes, conversion_bandwidth, peak_conversion_k, source_field, target_field)[source]#

Bases: object

Resonance analysis for coupled fields.

Parameters:
  • wavenumbers (NDArray[np.float64])

  • omega_source (NDArray[np.float64])

  • omega_target (NDArray[np.float64])

  • resonance_mismatch (NDArray[np.float64])

  • resonant_modes (NDArray[np.bool_])

  • n_resonant_modes (int)

  • conversion_bandwidth (float)

  • peak_conversion_k (float)

  • source_field (str)

  • target_field (str)

wavenumbers#

Shared wavenumber bins where both fields have active modes.

Type:

ndarray, shape (n_shared,)

omega_source#

Source field frequencies at shared k-bins.

Type:

ndarray, shape (n_shared,)

omega_target#

Target field frequencies at shared k-bins.

Type:

ndarray, shape (n_shared,)

resonance_mismatch#

|omega_s - omega_t| / omega_avg per shared mode.

Type:

ndarray, shape (n_shared,)

resonant_modes#

Boolean mask: which modes are near-resonant.

Type:

ndarray, shape (n_shared,)

n_resonant_modes#

Count of resonant modes.

Type:

int

conversion_bandwidth#

FWHM of the resonance mismatch curve in k-space (0.0 if < 2 resonant).

Type:

float

peak_conversion_k#

Wavenumber with minimum mismatch (closest to exact resonance).

Type:

float

source_field#

Source field name.

Type:

str

target_field#

Target field name.

Type:

str

wavenumbers: NDArray[np.float64]#
omega_source: NDArray[np.float64]#
omega_target: NDArray[np.float64]#
resonance_mismatch: NDArray[np.float64]#
resonant_modes: NDArray[np.bool_]#
n_resonant_modes: int#
conversion_bandwidth: float#
peak_conversion_k: float#
source_field: str#
target_field: str#
class tidal.measurement.SimulationData(times, fields, velocities, grid_spacing, grid_bounds, periodic, spec, parameters, bc_types=None, dt=None)[source]#

Bases: object

Full time-history of a simulation, ready for measurement.

Both fields and velocities store arrays of shape (n_snapshots, *grid_shape) — one spatial snapshot per recorded time. Constraint fields (time_derivative_order == 0) have no velocity entry.

Parameters:
times#

Snapshot times.

Type:

ndarray, shape (n_snapshots,)

fields#

Mapping field_name (n_snapshots, *grid_shape) arrays.

Type:

dict[str, ndarray]

velocities#

Mapping field_name (n_snapshots, *grid_shape) velocity arrays (v = dq/dt). Only present for 2nd-order (wave) fields.

Type:

dict[str, ndarray]

grid_spacing#

Cell size per spatial axis, e.g. (dx, dy).

Type:

tuple[float, …]

grid_bounds#

Domain bounds per spatial axis.

Type:

tuple[tuple[float, float], …]

periodic#

Whether each spatial axis is periodic.

Type:

tuple[bool, …]

spec#

The equation specification (fields, equations, matrices).

Type:

EquationSystem

parameters#

Resolved parameter values used in the simulation.

Type:

dict[str, float]

bc_types#

Per-axis boundary condition type (e.g. ("periodic", "neumann")). None for legacy data where BC info was not recorded. Used by the energy module for BC-aware gradient computation.

Type:

tuple[str, …] or None

bc_types: tuple[str, ...] | None = None#
dt: float | None = None#
property dynamical_fields: tuple[str, ...]#

Field names with time_derivative_order >= 2 (have velocities).

classmethod from_directory(path, spec)[source]#

Load from a snapshot directory with memory-mapped arrays (O(1) RAM).

The directory must contain metadata.json and per-field .npy files written by SnapshotWriter. Arrays are opened as read-only memory maps — only the pages actually accessed by measurement functions are loaded into RAM.

Parameters:
  • path (Path or str) – Path to the snapshot directory.

  • spec (EquationSystem) – JSON-derived equation specification.

Raises:
Return type:

SimulationData

classmethod from_result(result, spec, grid_info, parameters=None, dt=None)[source]#

Build from a solver result dict (IDA/leapfrog output).

This is the native-path constructor — no py-pde types involved. Directly slices the flat state vector using StateLayout.

Parameters:
  • result (dict) – Solver output with keys "t" (1D times array) and "y" (2D array of shape (n_snapshots, total_flat_size)).

  • spec (EquationSystem) – Equation specification.

  • grid_info (GridInfo) – Spatial grid descriptor.

  • parameters (dict, optional) – Resolved parameter values.

  • dt (float, optional) – Time-step size used by the solver (for conservation diagnostics).

Raises:

ValueError – If result has no snapshots or flat vector size doesn’t match the layout.

Return type:

SimulationData

classmethod load(path, spec)[source]#

Load from a snapshot directory (memory-mapped, O(1) RAM).

Parameters:
  • path (Path or str) – Path to snapshot directory.

  • spec (EquationSystem) – JSON-derived equation specification.

Raises:

ValueError – If path is not a directory.

Return type:

SimulationData

property n_snapshots: int#

Number of time snapshots.

save(path)[source]#

Save to a snapshot directory (metadata.json + .npy files).

This is the inverse of load() / from_directory(). Overwrites any existing files in the directory.

Parameters:

path (Path or str) – Directory to write into (created if it does not exist).

Returns:

The directory that was written.

Return type:

Path

property volume_element: float#

Uniform cell volume dx * dy * ....

times: NDArray[np.float64]#
fields: dict[str, NDArray[np.float64]]#
velocities: dict[str, NDArray[np.float64]]#
grid_spacing: tuple[float, ...]#
grid_bounds: tuple[tuple[float, float], ...]#
periodic: tuple[bool, ...]#
spec: EquationSystem#
parameters: dict[str, float]#
class tidal.measurement.SnapshotWriter(output_dir, field_names, velocity_names, grid_shape, n_snapshots, grid_spacing, grid_bounds, periodic, parameters=None, spec_path=None, flush_interval=10, bc_types=None, dt=None)[source]#

Bases: object

Stream simulation snapshots to disk with O(1) memory.

Pre-allocates one .npy file per field/velocity (plus times.npy) using numpy.memmap, then writes each snapshot in-place. The exact number of snapshots must be known at construction time — compute it as int(t_end / snapshot_interval) + 1.

Use as a context manager for automatic close():

with SnapshotWriter(output_dir, ...) as writer:
    for t, fields, velocities in simulation:
        writer.append(t, fields, velocities)
Parameters:
  • output_dir (Path) – Directory to create. Must not already exist.

  • field_names (list[str]) – Names of field arrays to store (e.g. ["phi_0", "chi_0"]).

  • velocity_names (list[str]) – Names of velocity arrays to store (e.g. ["phi_0", "chi_0"]). Stored as v_{name}.npy.

  • grid_shape (tuple[int, ...]) – Spatial grid shape (e.g. (96, 96)).

  • n_snapshots (int) – Exact number of snapshots to write.

  • grid_spacing (tuple[float, ...]) – Cell size per spatial axis.

  • grid_bounds (tuple[tuple[float, float], ...]) – Domain bounds per spatial axis.

  • periodic (tuple[bool, ...]) – Whether each spatial axis is periodic.

  • parameters (dict[str, float] or None) – Resolved parameter values.

  • spec_path (Path or None) – Path to the JSON spec file (for auto-discovery by tidal measure).

  • flush_interval (int)

  • bc_types (tuple[str, ...] | None)

  • dt (float | None)

append(t, fields, velocities)[source]#

Write one snapshot at the next time index.

Parameters:
  • t (float) – Simulation time for this snapshot.

  • fields (dict[str, ndarray]) – Mapping field_name spatial_array for this snapshot.

  • velocities (dict[str, ndarray]) – Mapping field_name spatial_array for velocities.

Raises:

ValueError – If the writer is closed, the snapshot count is exceeded, the time is non-finite or non-monotonic, or a field/velocity array has the wrong shape.

Return type:

None

close()[source]#

Flush all mmaps and write metadata.json.

It is safe to call close() multiple times.

Return type:

None

property count: int#

Number of snapshots written so far.

property n_snapshots: int#

Total number of snapshots pre-allocated.

property output_dir: Path#

Directory where snapshot files are written.

class tidal.measurement.SpectralPeak(frequency, power, mixing_length, fwhm, mixing_length_uncertainty)[source]#

Bases: object

A detected peak in the mixing power spectrum.

Each peak represents a frequency at which P(t) oscillates. The mixing length pi/omega gives the half-period of energy exchange at that frequency. FWHM measures how sharply defined the oscillation is — a narrow peak means a coherent, well-defined oscillation; a broad peak means the frequency is less certain.

Parameters:
frequency#

Angular frequency omega (rad/time) of the spectral peak.

Type:

float

power#

|P_hat(omega)|**2 — spectral power at this frequency.

Type:

float

mixing_length#

pi/omega — half-period of energy exchange at this frequency.

Type:

float

fwhm#

Full width at half maximum of the peak (rad/time).

Type:

float

mixing_length_uncertainty#

Propagated from half-width: (pi/omega**2) * HWHM where HWHM = FWHM / 2.

Type:

float

frequency: float#
power: float#
mixing_length: float#
fwhm: float#
mixing_length_uncertainty: float#
class tidal.measurement.SpectralSnapshot(wavenumbers, power_spectrum)[source]#

Bases: object

Spectral decomposition of a field at one time.

Parameters:
  • wavenumbers (NDArray[np.float64])

  • power_spectrum (NDArray[np.float64])

wavenumbers#

Radially binned wavenumber magnitudes |k|.

Type:

ndarray

power_spectrum#

|φ̂(k)|² averaged over shells of constant |k|.

Type:

ndarray

wavenumbers: NDArray[np.float64]#
power_spectrum: NDArray[np.float64]#
class tidal.measurement.SystemEnergy(per_field, interaction, total)[source]#

Bases: object

Energy density for the full coupled system at one snapshot.

All values are spatially-averaged energy densities ⟨ε⟩ = E / V_domain.

Parameters:
per_field#

Energy density breakdown per field (operator-aware gradient).

Type:

dict[str, FieldEnergy]

interaction#

Cross-field coupling energy density: total potential density minus per-field self-potential densities. Uses operator-aware gradient axes, so this is zero when fields are uncoupled and only one field is excited.

Type:

float

total#

Complete Hamiltonian density: kinetic + virial + constraint self-energy.

Type:

float

per_field: dict[str, FieldEnergy]#
interaction: float#
total: float#
class tidal.measurement.VelocityMismatchResult(source_velocity, target_velocity, mismatch, shared_wavenumbers, max_mismatch, mean_mismatch)[source]#

Bases: object

Velocity mismatch between two field groups.

Parameters:
source_velocity#

Velocity analysis for the source field.

Type:

VelocityResult

target_velocity#

Velocity analysis for the target field.

Type:

VelocityResult

mismatch#

|v_g_source(k) - v_g_target(k)| at shared wavenumber bins.

Type:

ndarray

shared_wavenumbers#

Wavenumbers at which both fields have active modes.

Type:

ndarray

max_mismatch#

Maximum velocity mismatch across all shared modes.

Type:

float

mean_mismatch#

Mean velocity mismatch across shared modes.

Type:

float

source_velocity: VelocityResult#
target_velocity: VelocityResult#
mismatch: NDArray[np.float64]#
shared_wavenumbers: NDArray[np.float64]#
max_mismatch: float#
mean_mismatch: float#
class tidal.measurement.VelocityResult(wavenumbers, group_velocity, phase_velocity, group_velocity_mean, phase_velocity_mean, n_active_modes, field_name)[source]#

Bases: object

Velocity analysis from dispersion relation.

Parameters:
  • wavenumbers (NDArray[np.float64])

  • group_velocity (NDArray[np.float64])

  • phase_velocity (NDArray[np.float64])

  • group_velocity_mean (float)

  • phase_velocity_mean (float)

  • n_active_modes (int)

  • field_name (str)

wavenumbers#

Wavenumber |k| for active modes.

Type:

ndarray, shape (n_active,)

group_velocity#

Group velocity d omega / dk per active mode.

Type:

ndarray, shape (n_active,)

phase_velocity#

Phase velocity omega / k per active mode.

Type:

ndarray, shape (n_active,)

group_velocity_mean#

Amplitude-weighted mean group velocity.

Type:

float

phase_velocity_mean#

Amplitude-weighted mean phase velocity.

Type:

float

n_active_modes#

Number of active modes used.

Type:

int

field_name#

Field group name (comma-joined).

Type:

str

wavenumbers: NDArray[np.float64]#
group_velocity: NDArray[np.float64]#
phase_velocity: NDArray[np.float64]#
group_velocity_mean: float#
phase_velocity_mean: float#
n_active_modes: int#
field_name: str#
tidal.measurement.check_energy_conservation(data, threshold=0.001)[source]#

Check whether energy density is conserved over the simulation.

For symplectic (leapfrog) solvers, the physical Hamiltonian oscillates by O(dt²) around the shadow Hamiltonian. When data.dt is available, the threshold is automatically raised to max(threshold, 10 * dt²) so that the expected shadow-Hamiltonian offset does not cause false FAIL results.

Parameters:
Return type:

EnergyDiagnostics

Raises:

ValueError – If threshold is not positive.

tidal.measurement.compute_asymptotic_conversion(data, source_fields, target_fields=None)[source]#

Compute asymptotic scattering observables.

The forward/backward directional split is defined by the source field’s initial propagation direction (spectral centroid wavevector at t=0). See module docstring for details on frame independence.

Parameters:
  • data (SimulationData) – Simulation output.

  • source_fields (str or sequence of str) – Source field name(s).

  • target_fields (str, sequence of str, or None) – Target field name(s). None → all dynamical fields not in source.

Return type:

AsymptoticConversionResult

Raises:

ValueError – If source/target fields are invalid or overlap, or if the source has zero initial energy.

tidal.measurement.compute_conversion_probability(data, source_field, target_field)[source]#

Compute wave conversion probability P(t) = E_target(t) / E_source(0).

This is the primary measurement for the Gertsenshtein effect. The source field is excited with some initial energy; the target field starts at zero. Coupling terms transfer energy between them over time.

Energy is computed via the canonical Hamiltonian (kinetic + gradient + mass), including the spatial volume element sqrt|g_spatial| for curved coordinates. This gives physically correct results for both flat and curved spacetimes.

Parameters:
  • data (SimulationData) – Full simulation output.

  • source_field (str) – Name of the source field (e.g. "phi_0").

  • target_field (str) – Name of the target field (e.g. "chi_0").

Return type:

ConversionResult

Raises:

ValueError – If source_field or target_field is not a valid field name, if they are the same field, or if the source has zero initial energy.

tidal.measurement.compute_critical_field(results, field_param, metric='P_final', threshold=0.99, *, interpolate=True)[source]#

Find minimum field strength for a metric to cross a threshold.

For each unique combination of the “outer” swept parameters (everything except field_param), the rows are sorted by field_param and scanned upward. The first crossing of metric >= threshold defines B_min.

Parameters:
  • results (SweepResults) – Sweep data with field_param as one of the swept parameters.

  • field_param (str) – The field-strength parameter to threshold on (e.g. "B0").

  • metric (str) – Metric column to compare against threshold (default "P_final").

  • threshold (float) – Target value for metric (default 0.99 — full conversion).

  • interpolate (bool) – If True, linearly interpolate between bracketing grid points for sub-grid accuracy.

Return type:

CriticalFieldResult

Raises:

ValueError – If field_param is not a swept parameter or metric is not found.

tidal.measurement.compute_dispersion(data, field_names, *, min_amplitude=1e-12)[source]#

Extract dispersion relation omega(k) from simulation output.

Algorithm#

  1. For each field in field_names, compute spatial rfftn per snapshot to get complex Fourier coefficients phi_hat_i(k, t).

  2. For each spatial mode k, temporal FFT of the complex coefficient gives S_i(k, omega).

  3. Sum spectral power across all fields: S_group(k, omega) = sum_i S_i(k, omega).

  4. Radially bin S_group(k, omega) into |k| shells.

  5. Peak detection: argmax(S_group) per k-bin extracts omega(k).

  6. Modes with combined max amplitude below threshold are inactive.

Using complex coefficients (not |phi_hat|) avoids frequency doubling artifacts from taking the absolute value before FFT. Summing power over a field group makes the measurement rotationally covariant within the group (no dependence on which single component is selected).

param data:

Simulation output with time-resolved field snapshots.

type data:

SimulationData

param field_names:

Field or group of fields to extract the dispersion relation for. All fields must be dynamical (time_derivative_order >= 2); constraint fields raise ValueError.

type field_names:

str or sequence of str

param min_amplitude:

Modes with combined max |phi_hat(k, t)| below this threshold are treated as inactive. Default 1e-12.

type min_amplitude:

float, optional

rtype:

DispersionResult

raises ValueError:

If any field is unknown, is a constraint field (time_derivative_order < 2), fewer than 3 snapshots, the timestep is non-uniform, or any equation term is position-dependent (uniform medium required).

Parameters:
Return type:

DispersionResult

tidal.measurement.compute_effective_mass(data, field_names, *, min_amplitude=1e-12)[source]#

Extract effective mass from the dispersion relation.

Parameters:
  • data (SimulationData) – Simulation output.

  • field_names (str or sequence of str) – Field name(s) to analyze. Multiple fields are summed (rotationally covariant within the group).

  • min_amplitude (float) – Minimum Fourier amplitude for a mode to be considered active.

Returns:

Effective mass and per-mode breakdown.

Return type:

EffectiveMassResult

Raises:

ValueError – If no active modes are found or dispersion cannot be computed.

tidal.measurement.compute_energy_timeseries(data, fields=None)[source]#

Compute energy density for every snapshot in the simulation.

Parameters:
  • data (SimulationData)

  • fields (set[str] or None) – If given, only evaluate Hamiltonian terms involving these base field names. Passed through to compute_system_energy.

Returns:

  • times (ndarray, shape (n_snapshots,))

  • per_field (dict[str, ndarray]) – Each value is shape (n_snapshots,) — energy density of that field.

  • interaction (ndarray, shape (n_snapshots,))

  • total (ndarray, shape (n_snapshots,))

Return type:

tuple[NDArray[np.float64], dict[str, NDArray[np.float64]], NDArray[np.float64], NDArray[np.float64]]

tidal.measurement.compute_group_conversion(data, source_fields, target_fields=None)[source]#

Measure energy conversion between field groups.

Computes P(t) = E_targets(t) / E_sources(0) where each energy is the sum of per-field canonical Hamiltonian energies across the group. This is the natural measurement for the Gertsenshtein effect where source and target are multi-component tensor/vector field groups.

Energy is computed via compute_energy_timeseries which correctly handles volume weights, operator-aware gradient axes, and position- dependent masses for curved spacetimes.

Parameters:
  • data (SimulationData) – Full simulation output.

  • source_fields (str or sequence of str) – Source field name(s). A single string is treated as a one-element group.

  • target_fields (str, sequence of str, or None) – Target field name(s). None means all other dynamical fields (time_derivative_order >= 2) not in source_fields.

Returns:

source_field / target_field are comma-joined group names.

Return type:

ConversionResult

Raises:

ValueError – If any field name is invalid, if source and target overlap, if the target group is empty, or if the source group has zero initial energy.

tidal.measurement.compute_mixing_length(conversion, *, min_prominence=0.01)[source]#

Extract the characteristic mixing length from the power spectrum of P(t).

Computes the temporal FFT of the conversion probability P(t), finds spectral peaks, and derives the mixing length from the dominant peak. This spectral approach correctly identifies the physically meaningful oscillation timescale even in multi-scale systems where rapid oscillations sit on top of a slower mixing envelope.

The uncertainty comes from the half-width at half-maximum (HWHM) of the spectral peak. Error propagation: L = pi/omega, dL = (pi/omega**2) * HWHM where HWHM = FWHM/2.

Parameters:
Return type:

MixingResult

Raises:

ValueError – If fewer than 3 time points, if timestep is non-uniform, if min_prominence is not in (0, 1), or if no spectral peaks are found above the prominence threshold.

tidal.measurement.compute_mixing_spectrum(conversion)[source]#

Compute the frequency decomposition of P(t).

Returns the power spectrum of the conversion probability timeseries, showing which oscillation frequencies participate in the energy exchange. This is the temporal analog of spatial spectral decomposition — answering “at what timescales does energy transfer occur?”

Angular frequencies are reported directly (consistent with compute_spectrum() which reports spatial wavenumbers as angular frequencies). No theory-specific conversion is applied. For each frequency omega, the half-period pi/omega gives the corresponding mixing timescale.

Parameters:

conversion (ConversionResult) – Output of compute_conversion_probability() or compute_group_conversion().

Return type:

MixingSpectrum

Raises:

ValueError – If the timestep is non-uniform or if fewer than 3 time points.

tidal.measurement.compute_mode_amplitudes(data, field_name)[source]#

Track mode amplitudes |φ̂(k)| over time.

Parameters:
Returns:

  • times (ndarray, shape (n_snapshots,))

  • wavenumbers (ndarray, shape (n_modes,))

  • amplitudes (ndarray, shape (n_snapshots, n_modes)) – |FFT coefficient| at each (time, |k|) bin.

Raises:

ValueError – If field_name is not in the spec.

Return type:

tuple[NDArray[np.float64], NDArray[np.float64], NDArray[np.float64]]

tidal.measurement.compute_reference_threshold(formula, reference_b, fixed_params, sim_settings)[source]#

Compute E-M reference conversion probability from an analytical formula.

Parameters:
  • formula (str) – "boccaletti" (localized Gaussian B-field) or "uniform" (uniform B, periodic domain).

  • reference_b (float) – Reference magnetic field strength.

  • fixed_params (dict[str, Any]) – Fixed sweep parameters (must include "kappa"; "R" for Boccaletti).

  • sim_settings (dict[str, Any]) – Simulation settings ("t_end" for uniform formula).

Returns:

Reference conversion probability P_EM.

Return type:

float

Raises:

ValueError – If required parameters are missing or formula is unknown.

tidal.measurement.compute_resonance_analysis(data, source_field, target_field, *, resonance_threshold=0.1, min_amplitude=1e-12)[source]#

Identify resonant modes and compute conversion bandwidth.

A mode k is resonant when |omega_source(k) - omega_target(k)| / omega_avg(k) < threshold.

Parameters:
  • data (SimulationData) – Simulation output.

  • source_field (str or sequence of str) – Source field name(s).

  • target_field (str or sequence of str) – Target field name(s).

  • resonance_threshold (float, optional) – Relative frequency mismatch threshold for resonance.

  • min_amplitude (float, optional) – Minimum Fourier amplitude for active modes.

Return type:

ResonanceResult

tidal.measurement.compute_snapshot_count(t_end, snapshot_interval)[source]#

Compute the exact number of snapshots for a simulation.

Parameters:
  • t_end (float) – Total simulation time.

  • snapshot_interval (float) – Time between snapshots.

Returns:

Exact snapshot count (includes the initial state at t=0).

Return type:

int

Raises:

ValueError – If t_end or snapshot_interval are non-positive.

tidal.measurement.compute_spectral_energy(field_data, velocity_data, mass_squared, grid_spacing, _periodic)[source]#

Compute per-mode energy density ε(k) = 0.5 * [|π̂_k|² + (k²+m²)|φ̂_k|²] / .

Returns radially-averaged spectral energy density consistent with the spatial-average convention: Σ_k ε(k) = ⟨ε⟩ (Parseval).

Parameters:
  • field_data (ndarray) – Field snapshot.

  • velocity_data (ndarray or None) – Momentum snapshot (None for constraint fields).

  • mass_squared (float or ndarray) – Mass-squared term (scalar or position-dependent array).

  • grid_spacing (tuple[float, ...]) – Cell sizes per axis.

  • _periodic (tuple[bool, ...]) – Per-axis periodicity (reserved for future windowing).

Returns:

  • wavenumbers (ndarray) – Radially binned |k| values.

  • spectral_energy (ndarray) – Energy per wavenumber bin.

Raises:

TypeError – If mass_squared is an ndarray (position-dependent mass breaks the Fourier-diagonal structure).

Return type:

tuple[NDArray[np.float64], NDArray[np.float64]]

tidal.measurement.compute_spectrum(field_data, grid_spacing, periodic)[source]#

Compute the radially-averaged power spectrum of a field snapshot.

Parameters:
  • field_data (ndarray, shape (*grid_shape)) – Real-valued field on the spatial grid.

  • grid_spacing (tuple[float, ...]) – Cell sizes per axis.

  • periodic (tuple[bool, ...]) – Per-axis periodicity.

Return type:

SpectralSnapshot

tidal.measurement.compute_system_energy(data, t_idx, _ctx=None, fields=None)[source]#

Compute Hamiltonian energy density at snapshot t_idx.

Uses the Legendre-transform Hamiltonian from canonical structure, decomposed into per-field self-energy and cross-field interaction. This is the only physically correct energy computation — the Hamiltonian coefficients include all metric/kinetic prefactors from the covariant Lagrangian, making it coordinate-invariant.

Parameters:
  • data (SimulationData)

  • t_idx (int) – Snapshot index.

  • fields (set[str] or None) – If given, only evaluate Hamiltonian terms involving these base field names. Passed through to _compute_hamiltonian_per_field.

  • _ctx (_HamiltonianContext | None)

Raises:

ValueError – If t_idx is out of range, or if the spec lacks hamiltonian_terms (re-derive with tidal derive to populate them).

Return type:

SystemEnergy

tidal.measurement.compute_velocities(data, field_names, *, min_amplitude=1e-12)[source]#

Extract group and phase velocities from the dispersion relation.

Parameters:
  • data (SimulationData) – Simulation output with time-resolved field snapshots.

  • field_names (str or sequence of str) – Field or group of fields to analyze.

  • min_amplitude (float, optional) – Minimum Fourier amplitude for a mode to be considered active.

Return type:

VelocityResult

Raises:

ValueError – If no active modes found or dispersion cannot be computed.

tidal.measurement.compute_velocity_mismatch(data, source_field, target_field, *, min_amplitude=1e-12)[source]#

Compute group velocity mismatch between two field groups.

Parameters:
  • data (SimulationData) – Simulation output.

  • source_field (str or sequence of str) – Source field name(s).

  • target_field (str or sequence of str) – Target field name(s).

  • min_amplitude (float, optional) – Minimum Fourier amplitude for active modes.

Return type:

VelocityMismatchResult

Raises:

ValueError – If no shared active modes between source and target.

tidal.measurement.create_snapshot_callback(output_dir, spec, grid, t_end, snapshot_interval, parameters=None, spec_path=None)[source]#

Create a SnapshotWriter + callback for streaming snapshots to disk.

The returned callback accepts (state, time) where state is a pde.FieldCollection. Wrap it in CallbackTracker(callback, ...) and pass to pde.solve(). Call writer.close() after the solve.

Parameters:
  • output_dir (Path or str) – Directory to write snapshot files into.

  • spec (EquationSystem) – Equation system (provides field/velocity names and state layout).

  • grid (CartesianGrid) – py-pde grid (provides shape, spacing, bounds, periodicity).

  • t_end (float) – Total simulation time.

  • snapshot_interval (float) – Time between snapshots.

  • parameters (dict or None) – Resolved parameter values to store in metadata.

  • spec_path (Path or None) – Path to the JSON spec file (for auto-discovery by tidal measure).

Returns:

  • writer (SnapshotWriter) – Call writer.close() after the solve finishes.

  • callback (callable) – Pass to CallbackTracker(callback, interrupts=snapshot_interval).

Return type:

tuple[SnapshotWriter, Callable[[Any, float], None]]

tidal.measurement.critical_field_to_sweep_results(result, original)[source]#

Convert a CriticalFieldResult to a standard SweepResults.

The field-strength parameter is removed from swept_params, and B_min / inv_B_min become metric columns. The resulting object can be serialized and plotted with existing sweep infrastructure.

Parameters:
Return type:

SweepResults

tidal.measurement.summarize(data)[source]#

Compute a measurement summary of the simulation.

Returns:

  • per_field_energy: dict[str, list[float]] time series

  • interaction_energy: list[float]

  • total_energy: list[float]

  • energy_conservation: EnergyDiagnostics

  • field_peaks: dict[str, tuple[float, float]] (initial, final peak amplitude)

Return type:

dict with keys

Parameters:

data (SimulationData)