tidal.symbolic package#

Symbolic computation layer for Lagrangian-to-PDE pipeline.

This package provides the Python-side interface for loading and processing field equations derived symbolically from Lagrangians via Mathematica/xAct.

class tidal.symbolic.BoundaryCondition(type, value=None, derivative=None, gamma=None)[source]#

Bases: object

Boundary condition for one spatial axis.

Parameters:
type#

One of “periodic”, “dirichlet”, “neumann”, or “robin”.

Type:

str

value#

Fixed value for Dirichlet BCs, or Robin beta.

Type:

float | None

derivative#

Fixed normal derivative for Neumann BCs.

Type:

float | None

gamma#

Robin coefficient gamma in d_n f + gamma*f = beta.

Type:

float | None

derivative: float | None = None#
classmethod from_dict(data)[source]#

Create a BoundaryCondition from a dictionary.

Raises:

ValueError – If the BC type is not recognized.

Parameters:

data (Mapping[str, Any])

Return type:

BoundaryCondition

gamma: float | None = None#
to_side_bc()[source]#

Convert to a SideBCSpec for the operator layer.

Raises:

ValueError – If the BC type is “periodic” (not representable as a side BC).

Return type:

SideBCSpec

value: float | None = None#
type: str#
class tidal.symbolic.ComponentEquation(field_name, field_index, time_derivative_order, rhs_terms, constraint_solver=<factory>, kinetic_coefficient_symbolic=None)[source]#

Bases: object

Equation of motion for a single field component.

For a wave-type equation:

d^2/dt^2 field = sum of OperatorTerms

Parameters:
field_name#

Name of the field component (e.g., “A_0”, “phi”).

Type:

str

field_index#

Index of this component in the field array.

Type:

int

time_derivative_order#

Order of the time derivative on the LHS (2 for wave equations).

Type:

int

rhs_terms#

Terms on the RHS of the equation.

Type:

tuple[OperatorTerm, …]

constraint_solver#

Configuration for elliptic constraint solving. Only meaningful when time_derivative_order == 0.

Type:

ConstraintSolverConfig

classmethod from_dict(data, fields_lookup)[source]#

Create a ComponentEquation from a dictionary.

Raises:

ValueError – If the RHS type is not “linear_combination”, or if constraint_solver is enabled for a non-constraint equation.

Parameters:
Return type:

ComponentEquation

kinetic_coefficient_symbolic: str | None = None#

Symbolic kinetic coefficient when ExportJSON left RHS unnormalized.

Non-None only for equations derived with a parameter-based kinetic coefficient (e.g., xi in the dark photon torsion model). The RHS terms are stored WITHOUT the 1/kinetic_coefficient divisor. Use normalize_kinetic_coefficients(spec, params) to apply the normalization before passing the spec to a solver. If this evaluates to zero at the given parameters, the field becomes a constraint (kinetic term vanishes).

property kinetic_position_dependent: bool#

Whether the LHS kinetic coefficient depends on spatial coordinates.

Mirrors OperatorTerm.position_dependent: True when kinetic_coefficient_symbolic contains a spatial coordinate call such as x[] or y[]; time-only dependence (t[]) returns False. Solvers that evaluate the kinetic coefficient without a grid (the per-mode modal builders — see GH #421) must refuse such equations rather than evaluate at a single point or silently fall back to M = 1.

GH #447(e): detection is REGEX-ONLY (_COORD_CALL_RE over the expression string). Unlike OperatorTerm.position_dependent, which prefers explicit coordinate_dependent metadata, the LHS schema carries no coordinate metadata for kinetics — nothing is ignored today, but if the schema ever grows an explicit declaration for the kinetic coefficient, this property must learn to read it.

field_name: str#
field_index: int#
time_derivative_order: int#
rhs_terms: tuple[OperatorTerm, ...]#
constraint_solver: ConstraintSolverConfig#
class tidal.symbolic.ConstraintSolverConfig(enabled=False, method='auto', boundary_conditions=<factory>, max_iterations=20, tolerance=1e-08)[source]#

Bases: object

Configuration for elliptic constraint solving.

When enabled is True, the constraint equation is solved at each timestep rather than remaining frozen at its initial value.

Solver Methods#

  • “auto” (default): Automatically selects the best solver. Uses FFT for fully periodic grids (O(N log N)), sparse matrix for non-periodic grids (O(N) via LU). Handles all constraint types: Poisson, Helmholtz, algebraic, anisotropic, etc.

  • “fft”: Force FFT solver. Requires fully periodic grid.

  • “matrix”: Force sparse matrix solver. Works with any BCs.

  • “poisson”: Original py-pde Poisson solver. Requires exactly one laplacian(self_field) term with no other self-referencing operators. Will warn if non-laplacian self-terms are present. Backward compatible.

Coupled Constraint Parameters#

When multiple constraints reference each other’s fields, the solver iterates using Gauss-Seidel until convergence or max_iterations. For fully periodic grids, coupled constraints are solved exactly via Fourier-space block solve (no iteration needed).

enabled#

Whether to solve the constraint elliptically. Default False preserves existing frozen-constraint behavior.

Type:

bool

method#

Solver method: “auto”, “fft”, “matrix”, or “poisson”.

Type:

str

boundary_conditions#

Per-axis boundary conditions (e.g., {"x": ..., "y": ...}).

Type:

dict[str, BoundaryCondition]

max_iterations#

Maximum Gauss-Seidel iterations for coupled constraints. Must be >= 1.

Type:

int

tolerance#

Convergence threshold for coupled constraint iteration. Iteration stops when max|field_new - field_old| < tolerance (scaled by field magnitude for robustness). Must be > 0.

Type:

float

enabled: bool = False#
classmethod from_dict(data)[source]#

Create from a dictionary or return default (disabled).

Parameters:

data (Mapping[str, Any] | None) – Parsed constraint_solver block from JSON, or None.

Returns:

Configuration instance.

Return type:

ConstraintSolverConfig

Raises:

ValueError – If method is not one of the recognized solver methods.

max_iterations: int = 20#
method: str = 'auto'#
tolerance: float = 1e-08#
boundary_conditions: dict[str, BoundaryCondition]#
Parameters:
class tidal.symbolic.EquationSystem(n_components, dimension, spatial_dimension, component_names, equations, mass_matrix, coupling_matrix, metadata, coordinates=(), signature=(), mass_matrix_symbolic=(), coupling_matrix_symbolic=(), canonical=None)[source]#

Bases: object

Complete system of field equations derived from a Lagrangian.

Parameters:
n_components#

Number of field components.

Type:

int

dimension#

Spacetime dimension (e.g., 2 for 1+1D).

Type:

int

spatial_dimension#

Number of spatial dimensions (dimension - 1).

Type:

int

component_names#

Names of field components in order.

Type:

tuple[str, …]

equations#

Equations for each component.

Type:

tuple[ComponentEquation, …]

mass_matrix#

Mass matrix M^2_ij for coupled systems.

Type:

tuple[tuple[float, …], …]

coupling_matrix#

Coupling matrix for field interactions.

Type:

tuple[tuple[float, …], …]

metadata#

Additional metadata (source, gauge, etc.)

Type:

dict[str, Any]

coordinates#

Coordinate names from JSON spacetime.coordinates (e.g., (“t”, “x”, “y”)). Defaults to empty tuple; use effective_coordinates for a guaranteed non-empty result that infers names from dimension when not set.

Type:

tuple[str, …]

canonical#

Canonical momentum and Hamiltonian structure from Legendre transform. Present when the JSON spec includes a "canonical" section (generated by tidal derive for non-linearization theories). None for legacy specs or linearization theories.

Type:

CanonicalStructure | None

base_spec(small_parameters=None)[source]#

Return the Pass 0 base spec with LHS demoted where required.

This is the correct entry point for the iterative perturbative driver (v6 plan, Gap B). It extends filter_by_order(0) with a check on each equation’s kinetic_coefficient_symbolic:

  • If the kinetic coefficient evaluates to literal zero when every small parameter is set to zero, the LHS is promoted by the correction. Demote it to an algebraic constraint: time_derivative_order = 0, kinetic_coefficient_symbolic = None. An identity self-term is prepended to the RHS if one is not already present, so Schur elimination detects the field as a proper constraint.

  • Otherwise keep the LHS as-is (the kinetic coefficient survives at ε=0 — this is a normal dynamical field or a kinetic term that only depends on non-perturbative parameters).

Parameters:

small_parameters (sequence of str, optional) – Small-parameter names. When absent, read from self.metadata.get("perturbation", {}).get( "small_parameters", []).

Returns:

A new spec whose equations are the ε=0 base system. Every surviving equation has time_derivative_order <= 2 by construction; a post-check raises ValueError if any residual has order > 2, indicating the [perturbation] config misses some higher-derivative term.

Return type:

EquationSystem

Raises:

ValueError – If any ε=0 base equation still has time_derivative_order     > 2 after demotion (corresponds to a third- or higher-order term that isn’t proportional to any declared small parameter — spec / config mismatch).

canonical: CanonicalStructure | None = None#
canonicalize_kinetic_for_perturbation(small_parameters)[source]#

Translate small-parameter kinetic dependence into order-1 RHS terms.

Uses the perturbative identity (see #301 Phase 3 / #303):

(M₀ + εM₁)⁻¹ (K₀ + εK₁) ≈ M₀⁻¹ K₀ + ε M₀⁻¹ (K₁ − M₁·M₀⁻¹·K₀)

so the perturbative hierarchy stays clean even when a small parameter ε enters the LHS kinetic coefficient. For each equation whose kinetic_coefficient_symbolic mentions a small parameter, splits M(ε) into M₀ (parameter-free) and per-parameter corrections c_p·p via split_small_parameter_kinetic(), stores M₀ as the new kinetic coefficient, and appends synthesized order-1 RHS terms -c_p·p·K₀/M₀ for each base (order-0) RHS term K₀. Pass 0 then sees a truly ε=0 baseline; Pass 1’s existing Duhamel kernel integrates both the original K₁ terms and the synthesized corrections identically.

No-op for equations with:

  • kinetic_coefficient_symbolic is None (M = 1 implicitly)

  • No small parameter present in the kinetic

  • Empty small_parameters argument

Parameters:

small_parameters (Sequence[str]) – The names declared in [perturbation].small_parameters of the TOML.

Returns:

  • A new EquationSystem with the canonicalized equations. The

  • returned spec is idempotent under this transform; calling it twice

  • produces the same result. self is unchanged.

Raises:

tidal.symbolic._kinetic_eval.KineticEvalError – If any kinetic coefficient has structure outside the perturbative contract (bilinear in two small parameters, quadratic in one, small-parameter denominator, parenthesized sub-sum). See split_small_parameter_kinetic().

Return type:

EquationSystem

coordinates: tuple[str, ...] = ()#
coupling_matrix_symbolic: tuple[tuple[str | None, ...], ...] = ()#
dependency_closure(seeds)[source]#

Fields whose equations the evolution of seeds can ever read.

The transitive closure of “the equation of X references Y” from seeds (a velocity reference v_Y counts as Y). Every kept equation is then complete — no kept row references an omitted field — so the closure evolves EXACTLY on its own. Fields outside the closure that are sourced BY it do not affect its exactness; they are simply not computed (GH #468 route 3: the observable-sector closure that rescues the localized implicit-dynamical class from the full-pencil refusal).

Structural on purpose: an edge exists whenever a term references the field, whatever its coefficient’s value at the run’s parameters — the conservative direction (never fewer fields than needed).

Raises:

ValueError – If a seed is not a component of this system.

Parameters:

seeds (Iterable[str])

Return type:

frozenset[str]

property effective_coordinates: tuple[str, ...]#

Coordinate names, inferred from dimension if not set explicitly.

property equation_map: dict[str, int]#

Map from field name to equation index. Cached on frozen dataclass.

filter_by_order(n)[source]#

Return a copy retaining only RHS terms with order_in_eps == n.

The equations (and their LHS structures) are preserved unchanged; only the rhs_terms tuple is filtered. Equations whose filtered RHS becomes empty are kept — callers may need them for state layout purposes (e.g., an evolution equation without a source in this order still requires its slot in the integrator).

Use filter_by_order(0) for the Pass 0 base equations and filter_by_order(1) for the Pass 1 source terms of a linear perturbative expansion.

Parameters:

n (int)

Return type:

EquationSystem

classmethod from_dict(data, *, strict_v6=True)[source]#

Create an EquationSystem from a dictionary (parsed JSON).

Raises:
  • ValueError – If the JSON data is invalid or component references are inconsistent.

  • TypeError – If a metadata parameter has an unsupported type.

Parameters:
  • data (Mapping[str, Any])

  • strict_v6 (bool)

Return type:

EquationSystem

property has_constraint_velocity_terms: bool#

True if the Hamiltonian has kinetic coupling between constraint fields.

Detects time_derivative(C_i) x time_derivative(C_j) terms where both C_i and C_j are constraint fields (time_derivative_order == 0). These indicate the naive Hamiltonian H = sum(pi * v) - L is not the correct conserved quantity for this theory (Dirac-Bergmann theory).

See GitHub issue #178 for details.

has_corrections()[source]#

Return True if any RHS term has order_in_eps > 0.

Cheap check used by the CLI to decide whether PerturbativeSolver is needed or the plain modal path suffices.

Return type:

bool

has_position_dependent_kinetic()[source]#

Return True if any dynamical equation has a position-dependent kinetic.

Restricted to dynamical equations (time_derivative_order > 0) — consumer-faithful to tidal.solver._kinetic.build_inverse_kinetic_diag(), which skips constraint rows. Used by the modal solver’s eligibility check and entry guards (GH #421): a position-dependent M(x) is a k-space convolution M̂(k−k′), which modal does not implement (GH #427); the time-domain backends handle it via grid= (GH #382).

Return type:

bool

property implicit_dynamical_sector: ImplicitDynamicalSector#

Promoted order-0 rows carrying second-order structure (GH #457).

See ImplicitDynamicalSector for semantics and the one-definition rule. The classification is provenance-agnostic: it reads whatever equations this system holds, so JSON-native order-0 rows and ε-demoted base-spec rows are treated identically (the demotion-injected identity self-term has time order 0 and never creates an edge).

Deliberately NOT edges (measured-healthy machinery, WS2 oracle): constraint-row time references of DYNAMICAL fields (S_cd velocity slots / deferred acceleration substitution) and dynamical-row references of any kind (those route through A_dc/M/D/K — their defects are GH #458, not classification).

mass_matrix_symbolic: tuple[tuple[str | None, ...], ...] = ()#
max_order()[source]#

Return the maximum order_in_eps across all RHS terms.

Returns 0 for baseline theories (no [perturbation] section or no terms with non-zero order). Use to gate --perturbative-order validation and to size the Pass loop in the driver.

Return type:

int

signature: tuple[int, ...] = ()#
property spatial_coordinates: tuple[str, ...]#

Spatial coordinate names (all except first, which is time).

property state_layout: tuple[tuple[str, str], ...]#

State vector layout as (field_name, slot_type) tuples.

slot_type is “field” or “momentum”. Second-order components produce two entries (field, momentum); first-order/constraint produce one (field).

property state_size: int#

Total number of state fields.

Second-order components contribute 2 slots (field + momentum). First-order and constraint components contribute 1 slot (field only).

property time_orders: tuple[int, ...]#

Per-component time derivative orders.

n_components: int#
dimension: int#
spatial_dimension: int#
component_names: tuple[str, ...]#
equations: tuple[ComponentEquation, ...]#
mass_matrix: tuple[tuple[float, ...], ...]#
coupling_matrix: tuple[tuple[float, ...], ...]#
metadata: dict[str, Any]#
class tidal.symbolic.OperatorTerm(coefficient, operator, field, coefficient_symbolic=None, time_dependent=False, coordinate_dependent=(), order_in_eps=0)[source]#

Bases: object

A single term in the RHS of a field equation.

Represents: coefficient * operator(field)

Parameters:
  • coefficient (float)

  • operator (str)

  • field (str)

  • coefficient_symbolic (str | None)

  • time_dependent (bool)

  • coordinate_dependent (tuple[str, ...])

  • order_in_eps (int)

coefficient#

Numeric coefficient for this term.

Type:

float

operator#

Name of the differential operator (“laplacian”, “identity”, “gradient_x”, etc.)

Type:

str

field#

Name of the field this operator acts on.

Type:

str

coefficient_symbolic#

Optional symbolic name for the coefficient (e.g., “m2”, “-kappa”). When present, the coefficient can be overridden at runtime by passing a parameters dict to the PDE constructor.

Type:

str | None

time_dependent#

Whether the coefficient depends on time. For curved spacetime, terms like -2H∂_t φ (Hubble friction) have time-dependent coefficients when the conformal factor Ω(t) varies with time. Default False for flat spacetime.

Type:

bool

coordinate_dependent#

Coordinate names the coefficient depends on (e.g., (“x”, “y”) for position-dependent coefficients on curved spatial surfaces, or (“t”,) for time-dependent). Empty tuple for constant coefficients.

Type:

tuple[str, …]

order_in_eps#

Order of this term in the small parameters configured by the theory’s [perturbation] section. 0 for the base (unperturbed) theory; 1 for a first-order correction, etc. Set by the Wolfram ComputeOrderInEps helper and consumed by EquationSystem.filter_by_order to separate base from correction terms for the iterative perturbative solver (v6 plan, Stage 2). Defaults to 0 for backward compatibility with JSON files predating the field.

Type:

int

coefficient_symbolic: str | None = None#
coordinate_dependent: tuple[str, ...] = ()#
classmethod from_dict(data)[source]#

Create an OperatorTerm from a dictionary.

Raises:

ValueError – If required keys are missing or operator is unknown.

Parameters:

data (Mapping[str, Any])

Return type:

OperatorTerm

order_in_eps: int = 0#
property position_dependent: bool#

Whether the coefficient depends on spatial coordinates.

Returns True when coordinate_dependent is non-empty (explicit declaration), or when coefficient_symbolic contains a spatial coordinate call pattern such as x[] or y[] (auto-detection for JSON exports that predate the coordinate_dependent field). Time-only dependence (t[]) returns False.

time_dependent: bool = False#
coefficient: float#
operator: str#
field: str#
tidal.symbolic.load_equation_system(json_path, *, strict_v6=True)[source]#

Load an equation system from a JSON file.

Parameters:
  • json_path (Path | str) – Path to the JSON file exported from Mathematica.

  • strict_v6 (bool, optional) – When True (default), the v6 time_order > 2 guard raises if the JSON has higher-derivative fields without a [perturbation] section. Set to False for read-only callers (LaTeX rendering, inspection) that do not evolve the system — the guard becomes a warning so the spec still loads.

Returns:

The parsed equation system.

Return type:

EquationSystem

Raises:

FileNotFoundError – If the JSON file does not exist.

tidal.symbolic.normalize_kinetic_coefficients(spec, params)[source]#

Apply symbolic kinetic-coefficient normalization to an equation system.

Deprecated since version All: time-domain solver backends (cvode/ida/leapfrog/scipy) now consume kinetic_coefficient_symbolic directly via tidal.solver._kinetic.build_inverse_kinetic_diag(), matching the modal solver’s existing behavior. The canonical spec form carries the kinetic coefficient on the LHS and the un-normalized RHS; every backend applies M⁻¹ at setup. This function is retained only for external callers that pre-normalized specs before the root fix landed (see #301, #304). It will be removed in a future release.

ExportJSON.wl emits equations with a parameter-based kinetic coefficient (e.g. xi) un-divided when the coefficient cannot be safely divided symbolically (to avoid Wolfram producing f/xi in the JSON which becomes a ZeroDivisionError at xi=0). The kinetic coefficient is stored in ComponentEquation.kinetic_coefficient_symbolic.

This function evaluates those coefficients at params and:

  • K ≠ 0 — divides each RHS term by K (numeric coefficient / K; symbolic coefficient_symbolic wrapped as (expr) / (kc_sym) so that subsequent CoefficientEvaluator calls remain correct).

  • K = 0 — the kinetic term vanishes; the field becomes a constraint (time_derivative_order=0, empty rhs_terms). This is the physical xi=0 limit where a formerly dynamical field loses its kinetic energy and its EOM degenerates to an algebraic condition.

Equations without kinetic_coefficient_symbolic are returned unchanged. The function is idempotent: calling it on an already-normalized spec (all kinetic_coefficient_symbolic are None) is a no-op.

Parameters:
  • spec (EquationSystem) – The equation system as loaded from JSON (pre-normalization).

  • params (dict[str, float]) – User-provided parameter values (e.g. {"xi": 0.1, "alpha": 0.5}).

Returns:

A new EquationSystem with all kinetic coefficients normalized.

Return type:

EquationSystem

Submodules#

tidal.symbolic.json_loader module#

Load and validate JSON equation specifications from Mathematica/xAct export.

This module provides the data structures and parsing logic for loading field equations that were derived symbolically from Lagrangians.

tidal.symbolic.json_loader.AXIS_LETTERS: tuple[str, ...] = ('x', 'y', 'z', 'w', 'v', 'u')#

Canonical spatial axis letters, ordered by dimension index. Supports up to 6 spatial dimensions (sufficient for all foreseeable physics).

tidal.symbolic.json_loader.is_known_operator(name)[source]#

Check whether an operator name is recognized.

Accepts static operators (identity, laplacian, gradient_x, …), user-registered custom operators (via register_operator), dynamic patterns for generic Nth-order derivatives (derivative_3_x, derivative_5_y, derivative_2x_1y, …), mixed time-space derivative operators (mixed_T2_S2x, mixed_T_S1x, …), and pure higher-order time operators on RHS (d2_t, d3_t, d4_t).

Parameters:

name (str)

Return type:

bool

tidal.symbolic.json_loader.operator_time_order(name)[source]#

Time-derivative order carried by an RHS operator name.

Complements is_known_operator(): static/spatial operators are order 0, first_derivative_t is 1, d<N>_t is N, and mixed time-space operators carry their T exponent (mixed_T_S1x = 1, mixed_T2_S1x = 2, numeric mixed_<t>_<s>... = t). The total time order of a term is this value plus one when the term targets a velocity slot (v_-prefixed field name).

This is the single symbolic-side source of truth used by EquationSystem.implicit_dynamical_sector (GH #457); solver-side operator decompositions must agree with it.

Parameters:

name (str)

Return type:

int

class tidal.symbolic.json_loader.LHSStructure(expression, time_order, space_order=0, kinetic_coefficient_symbolic=None)[source]#

Bases: object

Structure describing the left-hand side of a PDE.

Supports different PDE types: - Elliptic (time_order=0): ∇²φ = f (Poisson, Laplace) - Parabolic (time_order=1): ∂_t φ = … (heat, diffusion) - Hyperbolic (time_order=2): ∂²_t φ = … (wave) - Higher order: ∂^n_t φ = …

Parameters:
  • expression (str)

  • time_order (int)

  • space_order (int)

  • kinetic_coefficient_symbolic (str | None)

expression#

String representation (e.g., “d2_t(phi_0)”, “d_t(phi)”, “phi”).

Type:

str

time_order#

Order of time derivative on LHS (0, 1, 2, or higher).

Type:

int

space_order#

Order of space derivative on LHS (usually 0).

Type:

int

expression: str#
time_order: int#
space_order: int = 0#
kinetic_coefficient_symbolic: str | None = None#

Symbolic expression for the kinetic coefficient when ExportJSON left RHS unnormalized.

When non-None, the RHS terms are stored WITHOUT the kinetic coefficient divisor. Python must multiply RHS terms by 1/kinetic_coefficient at runtime. If this evaluates to zero for the given parameters, the field is a constraint (the kinetic term vanishes and the EOM becomes algebraic).

Example: for a torsion theory with L ½ξ(∂T)², the torsion EOM kinetic coefficient is xi. At xi=0 torsion becomes algebraically constrained.

classmethod from_dict(data)[source]#

Create LHSStructure from structured JSON data.

Expected format: {“expression”: “…”, “order”: {“time”: N, “space”: 0}} Optional: "kinetic_coefficient_symbolic" — see class docstring.

Parameters:

data (Mapping[str, Any]) – The structured LHS data from JSON.

Returns:

Parsed LHS structure.

Return type:

LHSStructure

Raises:

ValueError – If order dict does not contain a 'time' key.

class tidal.symbolic.json_loader.OperatorTerm(coefficient, operator, field, coefficient_symbolic=None, time_dependent=False, coordinate_dependent=(), order_in_eps=0)[source]#

Bases: object

A single term in the RHS of a field equation.

Represents: coefficient * operator(field)

Parameters:
  • coefficient (float)

  • operator (str)

  • field (str)

  • coefficient_symbolic (str | None)

  • time_dependent (bool)

  • coordinate_dependent (tuple[str, ...])

  • order_in_eps (int)

coefficient#

Numeric coefficient for this term.

Type:

float

operator#

Name of the differential operator (“laplacian”, “identity”, “gradient_x”, etc.)

Type:

str

field#

Name of the field this operator acts on.

Type:

str

coefficient_symbolic#

Optional symbolic name for the coefficient (e.g., “m2”, “-kappa”). When present, the coefficient can be overridden at runtime by passing a parameters dict to the PDE constructor.

Type:

str | None

time_dependent#

Whether the coefficient depends on time. For curved spacetime, terms like -2H∂_t φ (Hubble friction) have time-dependent coefficients when the conformal factor Ω(t) varies with time. Default False for flat spacetime.

Type:

bool

coordinate_dependent#

Coordinate names the coefficient depends on (e.g., (“x”, “y”) for position-dependent coefficients on curved spatial surfaces, or (“t”,) for time-dependent). Empty tuple for constant coefficients.

Type:

tuple[str, …]

order_in_eps#

Order of this term in the small parameters configured by the theory’s [perturbation] section. 0 for the base (unperturbed) theory; 1 for a first-order correction, etc. Set by the Wolfram ComputeOrderInEps helper and consumed by EquationSystem.filter_by_order to separate base from correction terms for the iterative perturbative solver (v6 plan, Stage 2). Defaults to 0 for backward compatibility with JSON files predating the field.

Type:

int

coefficient: float#
operator: str#
field: str#
coefficient_symbolic: str | None = None#
time_dependent: bool = False#
coordinate_dependent: tuple[str, ...] = ()#
order_in_eps: int = 0#
property position_dependent: bool#

Whether the coefficient depends on spatial coordinates.

Returns True when coordinate_dependent is non-empty (explicit declaration), or when coefficient_symbolic contains a spatial coordinate call pattern such as x[] or y[] (auto-detection for JSON exports that predate the coordinate_dependent field). Time-only dependence (t[]) returns False.

classmethod from_dict(data)[source]#

Create an OperatorTerm from a dictionary.

Raises:

ValueError – If required keys are missing or operator is unknown.

Parameters:

data (Mapping[str, Any])

Return type:

OperatorTerm

class tidal.symbolic.json_loader.BoundaryCondition(type, value=None, derivative=None, gamma=None)[source]#

Bases: object

Boundary condition for one spatial axis.

Parameters:
type#

One of “periodic”, “dirichlet”, “neumann”, or “robin”.

Type:

str

value#

Fixed value for Dirichlet BCs, or Robin beta.

Type:

float | None

derivative#

Fixed normal derivative for Neumann BCs.

Type:

float | None

gamma#

Robin coefficient gamma in d_n f + gamma*f = beta.

Type:

float | None

type: str#
value: float | None = None#
derivative: float | None = None#
gamma: float | None = None#
classmethod from_dict(data)[source]#

Create a BoundaryCondition from a dictionary.

Raises:

ValueError – If the BC type is not recognized.

Parameters:

data (Mapping[str, Any])

Return type:

BoundaryCondition

to_side_bc()[source]#

Convert to a SideBCSpec for the operator layer.

Raises:

ValueError – If the BC type is “periodic” (not representable as a side BC).

Return type:

SideBCSpec

class tidal.symbolic.json_loader.ConstraintSolverConfig(enabled=False, method='auto', boundary_conditions=<factory>, max_iterations=20, tolerance=1e-08)[source]#

Bases: object

Configuration for elliptic constraint solving.

When enabled is True, the constraint equation is solved at each timestep rather than remaining frozen at its initial value.

Solver Methods#

  • “auto” (default): Automatically selects the best solver. Uses FFT for fully periodic grids (O(N log N)), sparse matrix for non-periodic grids (O(N) via LU). Handles all constraint types: Poisson, Helmholtz, algebraic, anisotropic, etc.

  • “fft”: Force FFT solver. Requires fully periodic grid.

  • “matrix”: Force sparse matrix solver. Works with any BCs.

  • “poisson”: Original py-pde Poisson solver. Requires exactly one laplacian(self_field) term with no other self-referencing operators. Will warn if non-laplacian self-terms are present. Backward compatible.

Coupled Constraint Parameters#

When multiple constraints reference each other’s fields, the solver iterates using Gauss-Seidel until convergence or max_iterations. For fully periodic grids, coupled constraints are solved exactly via Fourier-space block solve (no iteration needed).

enabled#

Whether to solve the constraint elliptically. Default False preserves existing frozen-constraint behavior.

Type:

bool

method#

Solver method: “auto”, “fft”, “matrix”, or “poisson”.

Type:

str

boundary_conditions#

Per-axis boundary conditions (e.g., {"x": ..., "y": ...}).

Type:

dict[str, BoundaryCondition]

max_iterations#

Maximum Gauss-Seidel iterations for coupled constraints. Must be >= 1.

Type:

int

tolerance#

Convergence threshold for coupled constraint iteration. Iteration stops when max|field_new - field_old| < tolerance (scaled by field magnitude for robustness). Must be > 0.

Type:

float

enabled: bool = False#
method: str = 'auto'#
boundary_conditions: dict[str, BoundaryCondition]#
max_iterations: int = 20#
tolerance: float = 1e-08#
classmethod from_dict(data)[source]#

Create from a dictionary or return default (disabled).

Parameters:

data (Mapping[str, Any] | None) – Parsed constraint_solver block from JSON, or None.

Returns:

Configuration instance.

Return type:

ConstraintSolverConfig

Raises:

ValueError – If method is not one of the recognized solver methods.

Parameters:
class tidal.symbolic.json_loader.HamiltonianFactor(field, operator)[source]#

Bases: object

One factor in a quadratic Hamiltonian term.

Represents a field (or its time/spatial derivative) that appears as a multiplicative factor in a term of the component-form Hamiltonian density.

Parameters:
field#

Component field name (e.g., “A_1”, “phi_0”).

Type:

str

operator#

Differential operator: “identity” (bare field), “time_derivative” (∂_t field, maps to canonical momentum at evaluation), or a spatial operator (“gradient_x”, “laplacian”, etc.).

Type:

str

field: str#
operator: str#
classmethod from_dict(data)[source]#

Parse from JSON dict.

Parameters:

data (Mapping[str, Any])

Return type:

HamiltonianFactor

class tidal.symbolic.json_loader.HamiltonianTerm(coefficient, factor_a, factor_b, coefficient_symbolic=None, coordinate_dependent=(), term_class='unknown', order_in_eps=0)[source]#

Bases: object

A single quadratic term in the Hamiltonian density.

H = Σ coefficient * factor_a * factor_b

Parameters:
coefficient#

Numeric coefficient.

Type:

float

factor_a#

First field factor.

Type:

HamiltonianFactor

factor_b#

Second field factor (may equal factor_a for squared terms).

Type:

HamiltonianFactor

coefficient_symbolic#

Symbolic coefficient expression (for parameter override).

Type:

str | None

coordinate_dependent#

Spatial axes the coefficient depends on (e.g. ("x", "y")). When non-empty, the coefficient must be evaluated on the grid. Older JSON exports omit this field; auto-detection via position_dependent covers those cases.

Type:

tuple[str, …]

term_class#

Classification: "self" (both factors reference the same base field) or "interaction" (cross-field coupling). Defaults to "unknown" for older JSONs; use is_self_energy property which auto-classifies by comparing factor field names.

Type:

str

order_in_eps#

Perturbative order of this term: total exponent in declared small_parameters. Computed by Wolfram’s ComputeOrderInEps and emitted explicitly by ParseSingleHamiltonianTerm. Defaults to 0 for non-perturbative theories and direct hand-construction; legacy JSONs without the explicit field are tagged via heuristic (1 if coefficient_symbolic else 0) and should be re-derived.

Type:

int

coefficient: float#
factor_a: HamiltonianFactor#
factor_b: HamiltonianFactor#
coefficient_symbolic: str | None = None#
coordinate_dependent: tuple[str, ...] = ()#
term_class: str = 'unknown'#
order_in_eps: int = 0#
property position_dependent: bool#

True if the coefficient is a function of spatial coordinates.

Returns True when coordinate_dependent is non-empty (explicit declaration), or when coefficient_symbolic contains a coordinate call pattern such as x[] or y[] (auto-detection for JSON exports that predate the coordinate_dependent field).

property is_self_energy: bool#

True if both factors reference the same base field (self-energy).

Uses term_class when available (from Wolfram export), otherwise classifies by comparing base field names (stripping v_ prefix).

property base_field_a: str#

Base field name for factor_a (strips v_ velocity prefix).

property base_field_b: str#

Base field name for factor_b (strips v_ velocity prefix).

classmethod from_dict(data)[source]#

Parse from JSON dict.

Reads the explicit order_in_eps field emitted by Wolfram’s ComputeOrderInEps (single source of truth, matching the equation side at OperatorTerm.order_in_eps). For legacy JSONs without the explicit field, falls back to the heuristic 1 if coefficient_symbolic is not None else 0 — correct for current EH but coincidental in general. The strict consistency check lives in tidal validate.

Parameters:

data (Mapping[str, Any])

Return type:

HamiltonianTerm

class tidal.symbolic.json_loader.CanonicalStructure(hamiltonian_terms, volume_element=None)[source]#

Bases: object

Canonical structure: Hamiltonian terms for energy measurement.

The Hamiltonian’s bilinear terms are used by energy measurement to compute H(q, v). The E-L equations are stored in the equations array directly.

Parameters:
hamiltonian_terms#

Quadratic terms in the component-form Hamiltonian density. Used by energy measurement to compute H(q, v).

Type:

tuple[HamiltonianTerm, …]

volume_element#

Symbolic expression for sqrt|det(g_spatial)|, the spatial volume element. None for flat (Minkowski) spacetimes where the volume element is 1. Used by energy measurement to weight the Hamiltonian density before spatial integration.

Type:

str or None

hamiltonian_terms: tuple[HamiltonianTerm, ...]#
volume_element: str | None = None#
classmethod from_dict(data)[source]#

Parse from JSON canonical section.

Parameters:

data (Mapping[str, Any])

Return type:

CanonicalStructure

class tidal.symbolic.json_loader.ComponentEquation(field_name, field_index, time_derivative_order, rhs_terms, constraint_solver=<factory>, kinetic_coefficient_symbolic=None)[source]#

Bases: object

Equation of motion for a single field component.

For a wave-type equation:

d^2/dt^2 field = sum of OperatorTerms

Parameters:
field_name#

Name of the field component (e.g., “A_0”, “phi”).

Type:

str

field_index#

Index of this component in the field array.

Type:

int

time_derivative_order#

Order of the time derivative on the LHS (2 for wave equations).

Type:

int

rhs_terms#

Terms on the RHS of the equation.

Type:

tuple[OperatorTerm, …]

constraint_solver#

Configuration for elliptic constraint solving. Only meaningful when time_derivative_order == 0.

Type:

ConstraintSolverConfig

field_name: str#
field_index: int#
time_derivative_order: int#
rhs_terms: tuple[OperatorTerm, ...]#
constraint_solver: ConstraintSolverConfig#
kinetic_coefficient_symbolic: str | None = None#

Symbolic kinetic coefficient when ExportJSON left RHS unnormalized.

Non-None only for equations derived with a parameter-based kinetic coefficient (e.g., xi in the dark photon torsion model). The RHS terms are stored WITHOUT the 1/kinetic_coefficient divisor. Use normalize_kinetic_coefficients(spec, params) to apply the normalization before passing the spec to a solver. If this evaluates to zero at the given parameters, the field becomes a constraint (kinetic term vanishes).

property kinetic_position_dependent: bool#

Whether the LHS kinetic coefficient depends on spatial coordinates.

Mirrors OperatorTerm.position_dependent: True when kinetic_coefficient_symbolic contains a spatial coordinate call such as x[] or y[]; time-only dependence (t[]) returns False. Solvers that evaluate the kinetic coefficient without a grid (the per-mode modal builders — see GH #421) must refuse such equations rather than evaluate at a single point or silently fall back to M = 1.

GH #447(e): detection is REGEX-ONLY (_COORD_CALL_RE over the expression string). Unlike OperatorTerm.position_dependent, which prefers explicit coordinate_dependent metadata, the LHS schema carries no coordinate metadata for kinetics — nothing is ignored today, but if the schema ever grows an explicit declaration for the kinetic coefficient, this property must learn to read it.

classmethod from_dict(data, fields_lookup)[source]#

Create a ComponentEquation from a dictionary.

Raises:

ValueError – If the RHS type is not “linear_combination”, or if constraint_solver is enabled for a non-constraint equation.

Parameters:
Return type:

ComponentEquation

class tidal.symbolic.json_loader.ImplicitDynamicalSector(fields, reasons)[source]#

Bases: object

Constraint-classified rows that carry second-order structure.

time_derivative_order == 0 states the true LHS fact of a row; it does NOT guarantee the row is algebraic. Rows whose RHS references time derivatives of OTHER order-0 fields (d2_t(C) → M_cc mass coupling, first_derivative_t(C) / v_C → D_cc damping coupling) form a coupled second-order subsystem that algebraic (Schur) elimination cannot represent — folding them as algebraic is the GH #457 defect, measured at O(1) residuals on 20 shipped specs.

fields is the closure of that subsystem: the connected components (over order-0 rows, with an edge for every inter-constraint time reference) that contain at least one edge. Promoted rows must be routed through the rank-deficient-mass machinery (state slots, mass matrix row M[fi,fi] = 0, per-mode Schur), consistently across the modal paths; everything else about time_derivative_order — LaTeX display, demotion detection, schema validation — keeps reading the stored LHS fact.

ONE-DEFINITION RULE: this accessor is the only source of the implicit-dynamical/residual split. Routing or layout code must consult it — never test time_derivative_order == 0 directly for a routing decision, and never introduce a second classification path.

Parameters:
fields#

Field names of the implicit-dynamical order-0 rows.

Type:

frozenset[str]

reasons#

Per implicit-dynamical field, why it is in the sector: carries M_cc / carries D_cc (the row references another constraint’s acceleration / velocity), mass-targeted / velocity-targeted (another constraint row references this field’s acceleration / velocity), or closure (joined only through connectivity). Multiple tags are +-joined in sorted order.

Type:

Mapping[str, str]

fields: frozenset[str]#
reasons: Mapping[str, str]#
class tidal.symbolic.json_loader.EquationSystem(n_components, dimension, spatial_dimension, component_names, equations, mass_matrix, coupling_matrix, metadata, coordinates=(), signature=(), mass_matrix_symbolic=(), coupling_matrix_symbolic=(), canonical=None)[source]#

Bases: object

Complete system of field equations derived from a Lagrangian.

Parameters:
n_components#

Number of field components.

Type:

int

dimension#

Spacetime dimension (e.g., 2 for 1+1D).

Type:

int

spatial_dimension#

Number of spatial dimensions (dimension - 1).

Type:

int

component_names#

Names of field components in order.

Type:

tuple[str, …]

equations#

Equations for each component.

Type:

tuple[ComponentEquation, …]

mass_matrix#

Mass matrix M^2_ij for coupled systems.

Type:

tuple[tuple[float, …], …]

coupling_matrix#

Coupling matrix for field interactions.

Type:

tuple[tuple[float, …], …]

metadata#

Additional metadata (source, gauge, etc.)

Type:

dict[str, Any]

coordinates#

Coordinate names from JSON spacetime.coordinates (e.g., (“t”, “x”, “y”)). Defaults to empty tuple; use effective_coordinates for a guaranteed non-empty result that infers names from dimension when not set.

Type:

tuple[str, …]

canonical#

Canonical momentum and Hamiltonian structure from Legendre transform. Present when the JSON spec includes a "canonical" section (generated by tidal derive for non-linearization theories). None for legacy specs or linearization theories.

Type:

CanonicalStructure | None

n_components: int#
dimension: int#
spatial_dimension: int#
component_names: tuple[str, ...]#
equations: tuple[ComponentEquation, ...]#
mass_matrix: tuple[tuple[float, ...], ...]#
coupling_matrix: tuple[tuple[float, ...], ...]#
metadata: dict[str, Any]#
coordinates: tuple[str, ...] = ()#
signature: tuple[int, ...] = ()#
mass_matrix_symbolic: tuple[tuple[str | None, ...], ...] = ()#
coupling_matrix_symbolic: tuple[tuple[str | None, ...], ...] = ()#
canonical: CanonicalStructure | None = None#
property time_orders: tuple[int, ...]#

Per-component time derivative orders.

property state_size: int#

Total number of state fields.

Second-order components contribute 2 slots (field + momentum). First-order and constraint components contribute 1 slot (field only).

property state_layout: tuple[tuple[str, str], ...]#

State vector layout as (field_name, slot_type) tuples.

slot_type is “field” or “momentum”. Second-order components produce two entries (field, momentum); first-order/constraint produce one (field).

property effective_coordinates: tuple[str, ...]#

Coordinate names, inferred from dimension if not set explicitly.

property spatial_coordinates: tuple[str, ...]#

Spatial coordinate names (all except first, which is time).

property has_constraint_velocity_terms: bool#

True if the Hamiltonian has kinetic coupling between constraint fields.

Detects time_derivative(C_i) x time_derivative(C_j) terms where both C_i and C_j are constraint fields (time_derivative_order == 0). These indicate the naive Hamiltonian H = sum(pi * v) - L is not the correct conserved quantity for this theory (Dirac-Bergmann theory).

See GitHub issue #178 for details.

property equation_map: dict[str, int]#

Map from field name to equation index. Cached on frozen dataclass.

property implicit_dynamical_sector: ImplicitDynamicalSector#

Promoted order-0 rows carrying second-order structure (GH #457).

See ImplicitDynamicalSector for semantics and the one-definition rule. The classification is provenance-agnostic: it reads whatever equations this system holds, so JSON-native order-0 rows and ε-demoted base-spec rows are treated identically (the demotion-injected identity self-term has time order 0 and never creates an edge).

Deliberately NOT edges (measured-healthy machinery, WS2 oracle): constraint-row time references of DYNAMICAL fields (S_cd velocity slots / deferred acceleration substitution) and dynamical-row references of any kind (those route through A_dc/M/D/K — their defects are GH #458, not classification).

dependency_closure(seeds)[source]#

Fields whose equations the evolution of seeds can ever read.

The transitive closure of “the equation of X references Y” from seeds (a velocity reference v_Y counts as Y). Every kept equation is then complete — no kept row references an omitted field — so the closure evolves EXACTLY on its own. Fields outside the closure that are sourced BY it do not affect its exactness; they are simply not computed (GH #468 route 3: the observable-sector closure that rescues the localized implicit-dynamical class from the full-pencil refusal).

Structural on purpose: an edge exists whenever a term references the field, whatever its coefficient’s value at the run’s parameters — the conservative direction (never fewer fields than needed).

Raises:

ValueError – If a seed is not a component of this system.

Parameters:

seeds (Iterable[str])

Return type:

frozenset[str]

filter_by_order(n)[source]#

Return a copy retaining only RHS terms with order_in_eps == n.

The equations (and their LHS structures) are preserved unchanged; only the rhs_terms tuple is filtered. Equations whose filtered RHS becomes empty are kept — callers may need them for state layout purposes (e.g., an evolution equation without a source in this order still requires its slot in the integrator).

Use filter_by_order(0) for the Pass 0 base equations and filter_by_order(1) for the Pass 1 source terms of a linear perturbative expansion.

Parameters:

n (int)

Return type:

EquationSystem

max_order()[source]#

Return the maximum order_in_eps across all RHS terms.

Returns 0 for baseline theories (no [perturbation] section or no terms with non-zero order). Use to gate --perturbative-order validation and to size the Pass loop in the driver.

Return type:

int

has_corrections()[source]#

Return True if any RHS term has order_in_eps > 0.

Cheap check used by the CLI to decide whether PerturbativeSolver is needed or the plain modal path suffices.

Return type:

bool

has_position_dependent_kinetic()[source]#

Return True if any dynamical equation has a position-dependent kinetic.

Restricted to dynamical equations (time_derivative_order > 0) — consumer-faithful to tidal.solver._kinetic.build_inverse_kinetic_diag(), which skips constraint rows. Used by the modal solver’s eligibility check and entry guards (GH #421): a position-dependent M(x) is a k-space convolution M̂(k−k′), which modal does not implement (GH #427); the time-domain backends handle it via grid= (GH #382).

Return type:

bool

canonicalize_kinetic_for_perturbation(small_parameters)[source]#

Translate small-parameter kinetic dependence into order-1 RHS terms.

Uses the perturbative identity (see #301 Phase 3 / #303):

(M₀ + εM₁)⁻¹ (K₀ + εK₁) ≈ M₀⁻¹ K₀ + ε M₀⁻¹ (K₁ − M₁·M₀⁻¹·K₀)

so the perturbative hierarchy stays clean even when a small parameter ε enters the LHS kinetic coefficient. For each equation whose kinetic_coefficient_symbolic mentions a small parameter, splits M(ε) into M₀ (parameter-free) and per-parameter corrections c_p·p via split_small_parameter_kinetic(), stores M₀ as the new kinetic coefficient, and appends synthesized order-1 RHS terms -c_p·p·K₀/M₀ for each base (order-0) RHS term K₀. Pass 0 then sees a truly ε=0 baseline; Pass 1’s existing Duhamel kernel integrates both the original K₁ terms and the synthesized corrections identically.

No-op for equations with:

  • kinetic_coefficient_symbolic is None (M = 1 implicitly)

  • No small parameter present in the kinetic

  • Empty small_parameters argument

Parameters:

small_parameters (Sequence[str]) – The names declared in [perturbation].small_parameters of the TOML.

Returns:

  • A new EquationSystem with the canonicalized equations. The

  • returned spec is idempotent under this transform; calling it twice

  • produces the same result. self is unchanged.

Raises:

tidal.symbolic._kinetic_eval.KineticEvalError – If any kinetic coefficient has structure outside the perturbative contract (bilinear in two small parameters, quadratic in one, small-parameter denominator, parenthesized sub-sum). See split_small_parameter_kinetic().

Return type:

EquationSystem

base_spec(small_parameters=None)[source]#

Return the Pass 0 base spec with LHS demoted where required.

This is the correct entry point for the iterative perturbative driver (v6 plan, Gap B). It extends filter_by_order(0) with a check on each equation’s kinetic_coefficient_symbolic:

  • If the kinetic coefficient evaluates to literal zero when every small parameter is set to zero, the LHS is promoted by the correction. Demote it to an algebraic constraint: time_derivative_order = 0, kinetic_coefficient_symbolic = None. An identity self-term is prepended to the RHS if one is not already present, so Schur elimination detects the field as a proper constraint.

  • Otherwise keep the LHS as-is (the kinetic coefficient survives at ε=0 — this is a normal dynamical field or a kinetic term that only depends on non-perturbative parameters).

Parameters:

small_parameters (sequence of str, optional) – Small-parameter names. When absent, read from self.metadata.get("perturbation", {}).get( "small_parameters", []).

Returns:

A new spec whose equations are the ε=0 base system. Every surviving equation has time_derivative_order <= 2 by construction; a post-check raises ValueError if any residual has order > 2, indicating the [perturbation] config misses some higher-derivative term.

Return type:

EquationSystem

Raises:

ValueError – If any ε=0 base equation still has time_derivative_order     > 2 after demotion (corresponds to a third- or higher-order term that isn’t proportional to any declared small parameter — spec / config mismatch).

classmethod from_dict(data, *, strict_v6=True)[source]#

Create an EquationSystem from a dictionary (parsed JSON).

Raises:
  • ValueError – If the JSON data is invalid or component references are inconsistent.

  • TypeError – If a metadata parameter has an unsupported type.

Parameters:
  • data (Mapping[str, Any])

  • strict_v6 (bool)

Return type:

EquationSystem

tidal.symbolic.json_loader.validate_json_schema(data)[source]#

Validate that the JSON data matches the expected schema.

Raises:

ValueError – If required fields are missing or have invalid types.

Parameters:

data (Mapping[str, Any])

Return type:

None

class tidal.symbolic.json_loader.SpecRestriction(parent_spec, seeds, evolved, omitted, dropped_hamiltonian_terms, reason)[source]#

Bases: object

Provenance of a spec restricted to a dependency closure (GH #468).

Stored under metadata["restriction"] of the restricted spec so the artifact is self-describing: every downstream tool can see which fields were evolved, which were omitted, and why.

Parameters:
parent_spec: str#
seeds: tuple[str, ...]#
evolved: tuple[str, ...]#
omitted: tuple[str, ...]#
dropped_hamiltonian_terms: int#
reason: str#
to_dict()[source]#

JSON-ready form.

Return type:

dict[str, Any]

tidal.symbolic.json_loader.restrict_spec_dict(data, keep, *, parent_spec='', seeds=(), reason='')[source]#

Restrict a raw spec dict to the fields in keep (a dependency closure).

Dict-level on purpose: the result is a first-class spec JSON that EquationSystem.from_dict() re-validates, so every downstream tool (simulate, measure, inspect, resume) works on it unchanged. Fields outside keep are ABSENT from the result — never present-but-wrong. Hamiltonian terms touching an omitted field are dropped and counted; the energy measurement refuses when the count is nonzero and names the sector quantity instead.

Raises:

ValueError – If keep names an unknown field, or is not closed (a kept equation references an omitted field) — pass a EquationSystem.dependency_closure().

Parameters:
  • data (Mapping[str, Any])

  • keep (Iterable[str])

  • parent_spec (str)

  • seeds (Iterable[str])

  • reason (str)

Return type:

tuple[dict[str, Any], SpecRestriction]

tidal.symbolic.json_loader.load_equation_system(json_path, *, strict_v6=True)[source]#

Load an equation system from a JSON file.

Parameters:
  • json_path (Path | str) – Path to the JSON file exported from Mathematica.

  • strict_v6 (bool, optional) – When True (default), the v6 time_order > 2 guard raises if the JSON has higher-derivative fields without a [perturbation] section. Set to False for read-only callers (LaTeX rendering, inspection) that do not evolve the system — the guard becomes a warning so the spec still loads.

Returns:

The parsed equation system.

Return type:

EquationSystem

Raises:

FileNotFoundError – If the JSON file does not exist.

tidal.symbolic.json_loader.normalize_kinetic_coefficients(spec, params)[source]#

Apply symbolic kinetic-coefficient normalization to an equation system.

Deprecated since version All: time-domain solver backends (cvode/ida/leapfrog/scipy) now consume kinetic_coefficient_symbolic directly via tidal.solver._kinetic.build_inverse_kinetic_diag(), matching the modal solver’s existing behavior. The canonical spec form carries the kinetic coefficient on the LHS and the un-normalized RHS; every backend applies M⁻¹ at setup. This function is retained only for external callers that pre-normalized specs before the root fix landed (see #301, #304). It will be removed in a future release.

ExportJSON.wl emits equations with a parameter-based kinetic coefficient (e.g. xi) un-divided when the coefficient cannot be safely divided symbolically (to avoid Wolfram producing f/xi in the JSON which becomes a ZeroDivisionError at xi=0). The kinetic coefficient is stored in ComponentEquation.kinetic_coefficient_symbolic.

This function evaluates those coefficients at params and:

  • K ≠ 0 — divides each RHS term by K (numeric coefficient / K; symbolic coefficient_symbolic wrapped as (expr) / (kc_sym) so that subsequent CoefficientEvaluator calls remain correct).

  • K = 0 — the kinetic term vanishes; the field becomes a constraint (time_derivative_order=0, empty rhs_terms). This is the physical xi=0 limit where a formerly dynamical field loses its kinetic energy and its EOM degenerates to an algebraic condition.

Equations without kinetic_coefficient_symbolic are returned unchanged. The function is idempotent: calling it on an already-normalized spec (all kinetic_coefficient_symbolic are None) is a no-op.

Parameters:
  • spec (EquationSystem) – The equation system as loaded from JSON (pre-normalization).

  • params (dict[str, float]) – User-provided parameter values (e.g. {"xi": 0.1, "alpha": 0.5}).

Returns:

A new EquationSystem with all kinetic coefficients normalized.

Return type:

EquationSystem

tidal.symbolic.kinetic_matrix module#

Kinetic-matrix assembly from a derived EquationSystem.

Reorganizes the per-equation JSON output into the $mathcal{K}(partial_t, partial_z)$ wave-operator matrix defined in manuscript/sections/theory.tex (label KineticMatrix):

\[\mathcal{L}^{(2)} = \tfrac{1}{2}\,\xi^\mathsf{T}\, \mathcal{K}(\partial_t, \partial_z)\,\xi\]

i.e., each row $i$ and column $j$ of $mathcal{K}$ is the differential-operator polynomial acting on $phi_j$ in the linearized equation of motion for $phi_i$. The diagonal $i = j$ collects the LHS kinetic prefactor and any RHS self-terms; the off-diagonal entries collect inter-field couplings (Gertsenshtein $h leftrightarrow A$, dark-photon $A leftrightarrow T$, etc.).

This module operates purely on the existing JSON equations[] block (no Wolfram changes, no JSON-schema changes, no re-derivation) — see GitHub issue #372 for the design discussion.

class tidal.symbolic.kinetic_matrix.KineticMatrixCell(entries=())[source]#

Bases: object

One entry $\mathcal{K}_{ij}$ in the kinetic matrix.

A cell aggregates the contributions to the operator polynomial acting on column-field $\phi_j$ in equation $i$: each (operator_label, coefficient_symbolic) pair encodes one term coeff · op(\\phi_j).

Operator labels follow the same vocabulary the JSON RHS uses (identity, gradient_z, d2_t, …); the LaTeX renderer reuses tidal.symbolic.latex.operator_to_latex() to map them.

Parameters:

entries (tuple[tuple[str, str], ...])

entries: tuple[tuple[str, str], ...] = ()#
is_zero()[source]#
Return type:

bool

class tidal.symbolic.kinetic_matrix.KineticMatrix(row_fields, column_fields, cells)[source]#

Bases: object

Assembled kinetic matrix for an EquationSystem.

The matrix may be rectangular for theories that carry velocity-pair fields (v_<X>) on the right-hand side of EOMs. Rows are indexed by the q-fields with equations (one per EquationSystem.equations entry); columns are indexed by the union of q-fields and any v-fields referenced on a RHS, with q-fields first.

Parameters:
row_fields#

Field name for each row (length n_rows). Matches the q-field of the corresponding equation.

Type:

tuple[str, …]

column_fields#

Field name for each column (length n_cols). The first n_rows entries match row_fields (q-fields in equation order); any trailing entries are v-fields ordered by first appearance on a RHS.

Type:

tuple[str, …]

cells#

n_rows x n_cols grid of KineticMatrixCell.

Type:

tuple[tuple[tidal.symbolic.kinetic_matrix.KineticMatrixCell, …], …]

row_fields: tuple[str, ...]#
column_fields: tuple[str, ...]#
cells: tuple[tuple[KineticMatrixCell, ...], ...]#
row_index(name)[source]#

Return the row index of name, or None if absent.

Parameters:

name (str)

Return type:

int | None

column_index(name)[source]#

Return the column index of name, or None if absent.

Parameters:

name (str)

Return type:

int | None

get(row, col)[source]#
Parameters:
Return type:

KineticMatrixCell

property n_rows: int#
property n_cols: int#
property is_square: bool#
property fields: tuple[str, ...]#
field_index(name)[source]#
Parameters:

name (str)

Return type:

int | None

property n: int#
tidal.symbolic.kinetic_matrix.build_kinetic_matrix(spec)[source]#

Assemble $mathcal{K}$ from the equation system.

Rows are the dynamical EOMs in spec.equations (one per equation, indexed by eq.field_name). Columns are the union of:

  1. the same q-fields as the rows (in equation order), and

  2. any velocity-pair fields v_<X> referenced on a RHS, ordered by first appearance.

The matrix is rectangular when (2) is non-empty: theories with first-time-derivative couplings (e.g. the $delta_1$-weighted gradient_z(v_X) $= partial_zpartial_t X$ terms in the R²/nonminimal sectors) acquire extra columns labeled $dot{X}$ in the rendered matrix.

Cell semantics:

  • LHS contribution lands on the q-diagonal cell $mathcal{K}_{ii_q}$ where $i_q$ is the column index of eq.field_name. The LHS reads kinetic_coeff * d^{time_order}_t phi_i; encoded as (d^k_t, +kinetic_coeff).

  • RHS contributions are negated when moved to the LHS to put the EOM in the form $mathcal{K}xi = 0$. Each RHS term (coeff, op, field=j) contributes (op, -coeff) to $mathcal{K}_{ij}$ where $j$ is the column index of term.field (possibly a v-field).

Parameters:

spec (EquationSystem)

Return type:

KineticMatrix

tidal.symbolic.latex module#

Convert JSON equation specifications to LaTeX math notation.

Provides functions to render TIDAL equation systems as publication-ready LaTeX, including:

  • Component PDEs with proper operator notation

  • Lagrangian expressions with tensor index notation (\\tensor{} package)

  • Hamiltonian density terms

  • Symbolic coefficients (Mathematica InputForm → LaTeX)

Primary public entry point:

  • system_to_latex(spec, ...) — full equation system

tidal.symbolic.latex.load_symbol_overrides(path)[source]#

Load tensor-head / parameter-name overrides from a TOML file.

The file shape is:

[tensor_heads]
Ftorsion = "\\\\tilde{F}"

[parameters]
deltam = "\\\\delta_m"

Both sections are optional. Calling this function replaces the previous override state; pass an empty/missing file to clear.

Parameters:

path (Path | str)

Return type:

None

tidal.symbolic.latex.coefficient_to_latex(expr)[source]#

Convert a Mathematica-style symbolic coefficient to LaTeX.

Examples

>>> coefficient_to_latex("-(B0^2*kappa^2)")
'-B_0^{2} \\\\kappa^{2}'
>>> coefficient_to_latex("1/2")
'\\\\tfrac{1}{2}'
Parameters:

expr (str)

Return type:

str

tidal.symbolic.latex.field_to_latex(name, *, tensor_meta=None, coordinates=())[source]#

Convert a field component name to LaTeX.

Parameters:
  • name (str) – Field name (e.g., “h_5”, “phi_0”, “v_phi_0”).

  • tensor_meta (dict, optional) – Tensor metadata from enriched JSON: {"tensor_head": "h", "tensor_rank": 2, "tensor_indices": [2, 2]}.

  • coordinates (tuple[str, ...], optional) – Coordinate names for resolving index labels (e.g., (“t”, “x”, “y”, “z”)).

Returns:

LaTeX string for the field.

Return type:

str

tidal.symbolic.latex.operator_to_latex(operator, field_latex)[source]#

Render an operator applied to a field in LaTeX.

Parameters:
  • operator (str) – Operator name (e.g., “laplacian_x”, “gradient_y”, “identity”).

  • field_latex (str) – Already-rendered LaTeX for the field.

Returns:

LaTeX expression for the term.

Return type:

str

tidal.symbolic.latex.equation_to_latex(eq, spec)[source]#

Convert a single component equation to LaTeX.

Parameters:
Returns:

LaTeX string (without environment wrapping).

Return type:

str

tidal.symbolic.latex.hamiltonian_to_latex(terms, spec)[source]#

Render the Hamiltonian density as a LaTeX equation.

The rendered Hamiltonian is restricted to the self-GW + self-EM sector relevant to the conversion measurement: cross-sector (GW<->EM) terms are filtered out at render time, and torsion-sector terms have already been dropped at JSON emission by Wolfram’s $tidalHamiltonianFilter. The LHS therefore uses \\supset (not =) to mark the displayed expression as a proper subset of the full canonical density.

Returns:

LaTeX for \\mathscr{H} \\supset ....

Return type:

str

Parameters:
tidal.symbolic.latex.kinetic_matrix_to_latex(km, spec)[source]#

Render an assembled KineticMatrix as a labeled array.

The matrix may be rectangular for theories with velocity-pair columns: rows are indexed by the q-field equations (km.row_fields), columns include both q-fields and any v_<X> referenced on RHS terms (km.column_fields).

Output wraps the cell grid in an array environment with explicit row and column labels — column labels in the first header row, row labels in a leading column — so the reader can identify which equation and which column-field each cell refers to. Reuses operator_to_latex(), coefficient_to_latex(), and field_to_latex() for cell and label rendering, so the operators and field names here render identically to the EOM listings. Plane-wave axis remapping is applied as a post-pass.

Parameters:
Return type:

str

tidal.symbolic.latex.lagrangian_to_latex(expr)[source]#

Convert a Lagrangian expression from xAct notation to LaTeX.

This is a best-effort conversion. The xAct abstract index notation is rich and idiosyncratic; this handles the patterns found in the 33 example JSONs in this project.

Parameters:

expr (str) – The lagrangian_expr string from JSON metadata.

Returns:

LaTeX representation using \\tensor{} for index placement.

Return type:

str

tidal.symbolic.latex.system_to_latex(spec, *, output_format='align', include_hamiltonian=True, include_lagrangian=True)[source]#

Convert a full equation system to LaTeX.

Parameters:
  • spec (EquationSystem) – The equation system to render.

  • output_format ({"align", "gather", "document", "raw"}) – Output format. "gather" emits each equation as its own aligned block inside an outer gather* — used by the Appendix-E driver for per-equation centering.

  • include_hamiltonian (bool) – Whether to include the Hamiltonian density.

  • include_lagrangian (bool) – Whether to include the Lagrangian expression.

Returns:

LaTeX output.

Return type:

str

tidal.symbolic.reduction module#

Plane-wave dimensional reduction for JSON equation specs.

After the Wolfram pipeline exports a JSON spec with the original higher-dimensional metadata (dimension, coordinates, operator names), this module transforms it into a clean reduced-dimension spec suitable for efficient simulation.

For example, a 3+1D theory reduced along z produces a 1+1D spec with: - dimension: 2, coordinates: ["t", "x"], signature: [-1, 1] - Operators remapped: laplacian_z laplacian_x, gradient_z gradient_x - Killed-axis operators (laplacian_x, gradient_y, etc.) removed - coordinate_dependent arrays and coefficient_symbolic strings updated - Provenance metadata recording the reduction

Curved-coordinate support: - Coefficients depending only on the surviving coordinate are preserved and remapped - Volume element is kept if it depends only on the surviving coordinate - If any surviving coefficient or volume element references a killed coordinate,

ValueError is raised (incompatible reduction)

tidal.symbolic.reduction.reduce_spec(spec_data, reduction_config)[source]#

Apply plane-wave dimensional reduction to a JSON equation spec.

Transforms a higher-dimensional spec into a clean 1+1D spec by:

  1. Remapping operator names (laplacian_z laplacian_x)

  2. Removing terms with killed-axis operators

  3. Updating spacetime metadata (dimension, signature, coordinates)

  4. Remapping coordinate references in coefficient expressions

  5. Handling volume element (keep if surviving-coord-only, error if not)

  6. Adding provenance metadata

Parameters:
  • spec_data (dict) – Parsed JSON spec (as loaded by json.loads).

  • reduction_config (dict) – The [reduction] section from the TOML config.

Returns:

Transformed spec with reduced dimension.

Return type:

dict

tidal.symbolic.sign_algebra module#

Sound sign and ratio decisions for symbolic coefficient strings.

Coefficients exported by ExportJSON.wl are strings like -xi, -kappa^(-2), B0^2/2 or -1 + 2*B0^2*rho. Asking a physics question about a spec — “is this component’s laplacian the same sign as its siblings?”, “did re-derivation change this equation?” — means deciding the sign or the ratio of such expressions, usually with no numeric values for the free parameters.

The governing requirement is soundness: a definite answer is returned only when it is proven. Anything else is Sign.UNKNOWN. Callers may then escalate (supply parameters, declare assumptions) or report that they cannot tell — but they never receive a guess. Every confident-but-wrong diagnosis recorded in GH #401 was a case where a proof or an honest UNKNOWN was available; see the module tests, which pin one case per misreading.

Two independent decision procedures cooperate:

  1. Rational normal form (_Ratio) — expressions are expanded into a quotient of Laurent polynomials over Fraction coefficients, with non-polynomial subexpressions (E**u, coordinate symbols) held as opaque atoms. Two expressions whose quotient is a rational constant are then decided exactly, by cross-multiplication — never by division. This is what makes an overall rescaling of an equation provably invisible.

  2. Sign lattice (Sign) — the classical sign domain from abstract interpretation, {0, +, −, 0+, 0−, ⊤}. It decides the residue that normal form cannot: kappa^(-2) > 0, E^u > 0, sums of same-signed summands.

NONNEGATIVE is deliberately distinct from POSITIVE. kappa^2 is zero at kappa = 0, and a zero kinetic coefficient means a field is constrained rather than dynamical (see kinetic_coefficient_symbolic), so collapsing the two would let a caller claim a field is dynamical when it may not be.

Parsing reuses tidal.symbolic._kinetic_eval.normalize_inputform() and admits only the restricted node set that module already allows — literals, names, unary ±, and + - * / **. Nothing is ever eval-ed.

References

Cousot & Cousot (1977), Abstract interpretation: a unified lattice model, POPL — the sign domain and the role of ⊤ as “unknown”.

class tidal.symbolic.sign_algebra.Sign(value)[source]#

Bases: Enum

Element of the sign domain {0, +, −, 0+, 0−, ⊤}.

NONNEGATIVE/NONPOSITIVE are distinct from POSITIVE/NEGATIVE because the zero case is physically meaningful here: a vanishing kinetic coefficient turns a dynamical field into a constrained one.

ZERO = 'zero'#
POSITIVE = 'positive'#
NEGATIVE = 'negative'#
NONNEGATIVE = 'nonnegative'#
NONPOSITIVE = 'nonpositive'#
UNKNOWN = 'unknown'#
property is_definite: bool#

Whether this element pins the sign exactly (0, + or ).

property symbol: str#

Short display form, e.g. "+", "0+", "?".

class tidal.symbolic.sign_algebra.SignResult(sign, tactic, free_names=(), assumptions=<factory>, value=None, numeric=None)[source]#

Bases: object

Outcome of a sign or ratio query, with the reasoning that produced it.

Parameters:
sign#

The lattice verdict. Sign.UNKNOWN means not proven, never “probably zero” or “probably positive”.

Type:

Sign

tactic#

Which rung of the ladder decided it ("literal", "normal-form", "lattice", "assumption", "numeric") or "undecided".

Type:

str

free_names#

Unresolved parameter names, for reporting when undecided.

Type:

tuple[str, …]

assumptions#

Caller-declared assumptions that were actually used. Never implicit, and always surfaced so a reader can audit what the verdict rests on.

Type:

tuple[str, …]

value#

The exact rational value when the query reduced to a constant.

Type:

Fraction | None

numeric#

Corroborating numeric evaluation, reported separately and never used to justify sign.

Type:

float | None

sign: Sign#
tactic: str#
free_names: tuple[str, ...] = ()#
assumptions: tuple[str, ...]#
value: Fraction | None = None#
numeric: float | None = None#
property is_definite: bool#

Whether the sign was proven exactly.

describe()[source]#

Return a one-line human summary including the deciding tactic.

Return type:

str

tidal.symbolic.sign_algebra.are_equal(left, right)[source]#

Decide whether two coefficient expressions are identically equal.

Three-valued: True when proven equal, False when proven different, and None when neither could be established.

Parameters:
  • left (str | float | None) – Coefficient expressions; None means 1.

  • right (str | float | None) – Coefficient expressions; None means 1.

Returns:

Proven equality, proven inequality, or undecided.

Return type:

bool | None

tidal.symbolic.sign_algebra.canonical_form(expr)[source]#

Return a canonical string for expr, stable under rewriting.

Two expressions that differ only in how they were written — 3*chi - xi versus (3*chi) + (-xi), or a reordered product — produce the same string. Expressions that cannot be expanded rationally fall back to a structural rendering, which is still stable but only decides syntactic equality.

Parameters:

expr (str | float | ast.expr | None) – Coefficient expression, or an already-parsed node.

Returns:

Canonical representation.

Return type:

str

tidal.symbolic.sign_algebra.constant_ratio(numerator, denominator)[source]#

Return numerator / denominator when it is exactly a rational constant.

Decided by cross-multiplication on the rational normal form, so no division is performed and no parameter values are needed. This is what makes an overall rescaling of an equation provably invisible: if both sides were multiplied by the same factor, the ratio is exactly 1.

Parameters:
  • numerator (str | float | None) – Coefficient expressions; None means 1.

  • denominator (str | float | None) – Coefficient expressions; None means 1.

Returns:

The exact ratio, or None when it is not a constant (or cannot be expanded rationally).

Return type:

Fraction | None

tidal.symbolic.sign_algebra.evaluate_numeric(expr, parameters)[source]#

Evaluate expr numerically, or return None if anything is unresolved.

Delegates to tidal.symbolic._kinetic_eval.evaluate_with_substitutions(), the repository’s existing restricted-AST evaluator, rather than reimplementing evaluation here. E is bound to Euler’s number so that E**u terms from localized-background coefficients evaluate.

This result is only ever corroboration: it is reported alongside a structural verdict and never used to justify one, because a value at one point in parameter space proves nothing about the sign in general.

Parameters:
  • expr (str | float | None) – Coefficient expression; None means 1.

  • parameters (Mapping[str, float] | None) – Symbol values. None disables evaluation.

Returns:

The value, or None when unresolved, non-finite, or malformed.

Return type:

float | None

tidal.symbolic.sign_algebra.free_names(expr)[source]#

Return the sorted free symbol names appearing in expr.

E is excluded — it denotes Euler’s number, not a parameter.

Parameters:

expr (str | float | None) – Coefficient expression.

Returns:

Sorted distinct parameter names.

Return type:

tuple[str, …]

tidal.symbolic.sign_algebra.ratio_sign(numerator, denominator, *, assume_positive=None, assume_nonzero=None, parameters=None)[source]#

Decide the sign of numerator / denominator.

This is the primitive behind every relative question asked of a spec: comparing sibling components, or comparing an equation before and after re-derivation. Ratios sometimes cancel unknown parameters — -xi/-xi is exactly 1 regardless of xi — but not always, and sums over distinct parameters generally do not cancel. Those cases return Sign.UNKNOWN rather than a guess.

Parameters:
  • numerator (str | float | None) – Coefficient expressions; None means 1.

  • denominator (str | float | None) – Coefficient expressions; None means 1.

  • assume_positive (Iterable[str] | None) – Parameter names the caller declares strictly positive. Only these are assumed; nothing is positive by default. Any assumption actually used is recorded on the result.

  • assume_nonzero (Iterable[str] | None) – Parameter names the caller declares merely non-vanishing, without claiming a sign. This is the usual physical situation for a coupling such as kappa, whose vanishing would remove the Einstein-Hilbert term entirely, and it sharpens kappa^2 from 0+ to +. Leave it empty for parameters that genuinely reach zero (xi, b5).

  • parameters (Mapping[str, float] | None) – Optional numeric values, used only for corroboration.

Returns:

The verdict, with SignResult.value set when the ratio reduced to an exact rational constant.

Return type:

SignResult

tidal.symbolic.sign_algebra.sign_of(expr, *, assume_positive=None, assume_nonzero=None, parameters=None)[source]#

Decide the sign of a single coefficient expression.

Parameters:
  • expr (str | float | None) – Coefficient in Wolfram InputForm, a number, or None (= 1).

  • assume_positive (Iterable[str] | None) – Parameter names the caller declares strictly positive. Only these are assumed; nothing is positive by default. Any assumption actually used is recorded on the result.

  • assume_nonzero (Iterable[str] | None) – Parameter names the caller declares merely non-vanishing, without claiming a sign. This is the usual physical situation for a coupling such as kappa, whose vanishing would remove the Einstein-Hilbert term entirely, and it sharpens kappa^2 from 0+ to +. Leave it empty for parameters that genuinely reach zero (xi, b5).

  • parameters (Mapping[str, float] | None) – Optional numeric values. These populate SignResult.numeric for corroboration but never upgrade an unproven structural verdict.

Returns:

The verdict, the deciding tactic, and the supporting context.

Return type:

SignResult

tidal.symbolic.spec_query module#

Semantic accessors for equation specifications.

from_dict() gives a typed model of a spec. This module adds the semantic one: the physics-level questions a reader actually asks, each with a single vetted implementation.

The questions, and the trap in each (GH #401 records one wrong answer per row):

effective_coefficient

What is the coefficient of operator(field) in an equation? It is the sum of every matching term, divided by the LHS kinetic coefficient. Components routinely carry more than one matching term — the Euler-Heisenberg photons carry a base laplacian_x and a B0^2*rho correction — so taking “the” matching term is wrong, and ignoring the kinetic coefficient is the single most repeated mistake in this codebase (#237, #258, #302, and twice in #401).

field_families

Which components belong together? Grouped by tensor_head and classified by tensor_indices, never by parsing the numeric suffix. Index 0 is the temporal component of a rank-1 field, but rank-3 torsion has components like t_13 = [2, 0, 2] where that reading is meaningless.

coefficient_provenance

Where is this coefficient written down, and do those places agree? Parts of it live across several RHS terms and the LHS; it is duplicated in the mass/coupling matrices; and a related but distinct quantity lives in canonical.hamiltonian_terms. These three relationships are reported separately, because presenting the Hamiltonian counterpart as “the same coefficient elsewhere” would be its own misreading.

compare_equations / diff_systems

Did re-derivation change the physics? Multiplying an equation through by a constant — both the kinetic coefficient and every RHS term — changes nothing physical. A naive diff reports such a rescaling as three separate “fixes” (the gertsenshtein_ungauged case). Flipping only the RHS, with the kinetic coefficient unchanged, is a real change (the #397 defect). The two are distinguished here rather than by eye.

All sign and equality reasoning delegates to tidal.symbolic.sign_algebra, which answers only when it can prove an answer. Anything undecided is reported as such and never guessed.

Why this layer is Python and not Wolfram#

Summing a component’s terms looks like symbolic work, and this project keeps symbolic work in Wolfram. The distinction is that this module reads already-derived output to answer a question; it never derives, and never writes back. Three facts fix the boundary:

  • Its results never re-enter the pipeline. effective_coefficient has exactly two consumers — the tidal inspect query surface, and EquationSystem._compute_matrices_from_terms, whose matrices are themselves display-only. Nothing it produces reaches a solver or is written to a spec file. It does compose strings ("(a) + (b)", "(num)/(kin)"), and that is safe precisely because the composed expression is consumed by analysis and display, never fed back into the derivation.

  • Merging the duplicate terms upstream in Wolfram was considered and rejected. Of 6460 multi-term (field, operator) keys in the committed corpus, 6367 are identical in every attribute and could be merged at export — but 93 differ in order_in_eps, and the perturbative driver depends on that split (EquationSystem.filter_by_order(0) must still see only the base term). Merging would break it and would not remove the need to sum.

  • Term-level identity is load-bearing for the solver. CoefficientEvaluator caches per (eq_idx, term_idx) and keeps an id(term) reverse index, so terms are addressed individually. Separate terms also carry provenance: 1.0 (base Maxwell) and -2*B0^2*rho (the Euler-Heisenberg correction) are physically distinct contributions, and collapsing them at export would discard that.

So the summation belongs to the reader, not to the derivation — which is why it lives here, and why there is exactly one implementation of it (GH #403, #404).

class tidal.symbolic.spec_query.CoefficientProvenance(effective, order_spread, matrix_entry, hamiltonian_terms, checks)[source]#

Bases: object

Every place one coefficient is recorded, grouped by how it relates.

The three groups are kept apart deliberately. Only effective and its parts are this coefficient; matrix_entry is a redundant re-encoding of it; hamiltonian_terms is a different quantity in a different formalism, related by a derivation and carrying its own factor convention. Flattening them into one list would invite the very confusion this module exists to prevent.

Parameters:
effective#

The coefficient itself, summed and kinetic-normalized.

Type:

EffectiveCoefficient

order_spread#

Contributing term expressions grouped by order_in_eps.

Type:

dict[int, tuple[str, …]]

matrix_entry#

The mass/coupling matrix encoding, for identity operators. Note the matrix convention is matrix[i][j] = -(coefficient) and is not normalized by the kinetic coefficient.

Type:

str | float | None

hamiltonian_terms#

Hamiltonian terms mentioning the same field pair — related, distinct.

Type:

tuple[HamiltonianTerm, …]

checks#

Only checks that can be settled without unproven factor reasoning.

Type:

tuple[ConsistencyCheck, …]

effective: EffectiveCoefficient#
order_spread: dict[int, tuple[str, ...]]#
matrix_entry: str | float | None#
hamiltonian_terms: tuple[HamiltonianTerm, ...]#
checks: tuple[ConsistencyCheck, ...]#
class tidal.symbolic.spec_query.ConsistencyCheck(name, status, detail)[source]#

Bases: object

One cross-representation check, and whether it could be settled.

Parameters:
name#

Short identifier, e.g. "numeric-vs-symbolic".

Type:

str

status#

"ok", "mismatch", or "undecided". "undecided" is a first-class outcome, not a failure.

Type:

str

detail#

Human-readable explanation.

Type:

str

name: str#
status: str#
detail: str#
class tidal.symbolic.spec_query.EffectiveCoefficient(equation_field, field, operator, terms, numerator, kinetic)[source]#

Bases: object

The coefficient of operator(field) in one equation, fully resolved.

“Effective” means two things have already been applied: every matching RHS term has been summed, and the LHS kinetic coefficient has been divided out. The value is the primary result; its sign is one derived view of it.

Parameters:
equation_field#

Field whose equation this came from.

Type:

str

field#

Field the operator acts on.

Type:

str

operator#

Operator name.

Type:

str

terms#

The individual contributing terms, kept so a reader can see the parts.

Type:

tuple[OperatorTerm, …]

numerator#

Their sum, as an expression string.

Type:

str

kinetic#

The LHS kinetic coefficient, or None when the LHS is bare (= 1).

Type:

str | None

equation_field: str#
field: str#
operator: str#
terms: tuple[OperatorTerm, ...]#
numerator: str#
kinetic: str | None#
property exists: bool#

Whether any term contributed.

property expression: str#

The effective coefficient as a single expression string.

property term_count: int#

How many RHS terms were summed — more than one is common.

sign(*, assume_positive=None, assume_nonzero=None, parameters=None)[source]#

Return the proven sign of this effective coefficient.

Parameters:
  • assume_positive (Iterable[str] | None) – Caller-declared parameter facts; see sign_algebra.sign_of().

  • assume_nonzero (Iterable[str] | None) – Caller-declared parameter facts; see sign_algebra.sign_of().

  • parameters (Mapping[str, float] | None) – Optional values, used only for corroboration.

Returns:

The verdict and the tactic that decided it.

Return type:

SignResult

value(parameters)[source]#

Evaluate numerically at parameters, or None if unresolved.

Parameters:

parameters (Mapping[str, float])

Return type:

float | None

class tidal.symbolic.spec_query.EquationComparison(field, verdict, changed_keys, undecided_keys, detail)[source]#

Bases: object

How one equation differs between two specs.

Parameters:
field#

The component compared.

Type:

str

verdict#

"identical" — byte-equal effective coefficients. "representational" — every effective coefficient is unchanged even though the written form differs, i.e. the equation was rescaled. "real" — at least one effective coefficient provably changed. "undecided" — a difference exists that could not be settled.

Type:

str

changed_keys#

operator(field) keys whose effective coefficient changed.

Type:

tuple[str, …]

undecided_keys#

Keys that could neither be proven equal nor proven different.

Type:

tuple[str, …]

detail#

Human-readable summary.

Type:

str

field: str#
verdict: str#
changed_keys: tuple[str, ...]#
undecided_keys: tuple[str, ...]#
detail: str#
class tidal.symbolic.spec_query.FieldFamily(head, rank, members, indices, exact)[source]#

Bases: object

Components sharing a tensor head, classified by their index structure.

Parameters:
head#

Tensor head, e.g. "a", "h", "t".

Type:

str

rank#

Tensor rank; 0 when unknown.

Type:

int

members#

Component names in spec order.

Type:

tuple[str, …]

indices#

Each component’s index tuple, when known.

Type:

dict[str, tuple[int, …]]

exact#

True when grouping used exported tensor_head metadata. False means it fell back to splitting the name on its last underscore, which is a guess — 12 of the committed example specs predate the metadata.

Type:

bool

head: str#
rank: int#
members: tuple[str, ...]#
indices: dict[str, tuple[int, ...]]#
exact: bool#
temporal_slots(component)[source]#

Return how many of component’s indices are temporal (zero).

This replaces “index 0 is the temporal component”, which holds for a rank-1 field but not for rank-3 torsion, where t_13 has indices (2, 0, 2). Returns None when index metadata is unavailable.

Parameters:

component (str)

Return type:

int | None

group_by_temporal_slots()[source]#

Group members by their number of temporal indices.

This is descriptive metadata, not a comparison partition. It is the correct replacement for “component 0 is the temporal one” (#401 row 4): for the photon it recovers a_0 alone against a_1..a_3, and for rank-3 torsion it separates the twelve components into three classes that the flat t_0..t_23 numbering completely obscures.

Do not restrict sign comparisons to within a group. Each equation is normalized by its own kinetic coefficient, so every evolution equation in a family should agree in sign regardless of index structure — and the #397 defect is precisely a temporal component disagreeing with its spatial siblings, which a within-group comparison would never look at. Measured on the committed corpus, partitioning by temporal slots misses 13 of the 19 files carrying that defect.

Return type:

dict[int, tuple[str, …]]

class tidal.symbolic.spec_query.SystemDiff(comparisons, only_left, only_right)[source]#

Bases: object

Result of comparing two equation systems.

Parameters:
comparisons#

Per-equation results for fields present in both systems.

Type:

tuple[EquationComparison, …]

only_left, only_right

Components present in just one system.

Type:

tuple[str, …]

comparisons: tuple[EquationComparison, ...]#
only_left: tuple[str, ...]#
only_right: tuple[str, ...]#
property real: tuple[EquationComparison, ...]#

Equations whose physics provably changed.

property representational: tuple[EquationComparison, ...]#

Equations rewritten without changing their physics.

property undecided: tuple[EquationComparison, ...]#

Equations whose difference could not be settled.

property has_real_changes: bool#

Whether any equation provably changed, or a component appeared/vanished.

tidal.symbolic.spec_query.coefficient_provenance(spec, equation_field, field, operator)[source]#

Gather every recorded form of one coefficient, with its relationships.

Parameters:
  • spec (EquationSystem) – The loaded system.

  • equation_field (str) – Which equation to read.

  • field (str) – Field the operator acts on.

  • operator (str) – Operator name.

Returns:

The parts, the duplicate encodings, and the related-but-distinct Hamiltonian terms, kept separate.

Return type:

CoefficientProvenance

Raises:

KeyError – If equation_field is not a component of spec.

tidal.symbolic.spec_query.compare_equations(left, right)[source]#

Compare two versions of one equation, separating real from cosmetic change.

An equation multiplied through by any non-zero constant — the kinetic coefficient and every RHS term — is physically unchanged. Comparing effective coefficients makes that rescaling invisible automatically, while a flip of the RHS alone still shows up as real.

Parameters:
Returns:

The verdict and the keys responsible for it.

Return type:

EquationComparison

tidal.symbolic.spec_query.diff_systems(left, right)[source]#

Compare two equation systems, separating real changes from rescalings.

Parameters:
  • left (EquationSystem) – The two systems, e.g. a committed spec and a re-derived one.

  • right (EquationSystem) – The two systems, e.g. a committed spec and a re-derived one.

Returns:

Per-equation verdicts plus components unique to either side.

Return type:

SystemDiff

tidal.symbolic.spec_query.effective_coefficient(equation, field, operator)[source]#

Return the effective coefficient of operator(field) in equation.

Sums every matching term and records the kinetic coefficient, so callers cannot accidentally use one term of several, or forget the LHS divisor.

Sums across perturbative orders. Terms are summed regardless of their order_in_eps, so for Euler-Heisenberg a_0 this returns (1.0) + (-2*B0^2*rho) — the ε⁰ base term plus the ε¹ correction. That is the coefficient of the equation as given. Order selection is a spec-level operation performed upstream: call filter_by_order() first and read the coefficient off the filtered spec, which for the same component yields just 1.0.

Parameters:
  • equation (ComponentEquation) – Equation to read.

  • field (str) – Field the operator acts on.

  • operator (str) – Operator name.

Returns:

The resolved coefficient; check EffectiveCoefficient.exists.

Return type:

EffectiveCoefficient

tidal.symbolic.spec_query.field_families(spec)[source]#

Group a spec’s components into tensor families.

Uses the tensor_head / tensor_rank / tensor_indices metadata exported since 78374c1. Specs predating it fall back to splitting the component name on its final underscore, and the resulting families are marked exact=False so callers can tell a guess from a fact.

Parameters:

spec (EquationSystem) – The loaded equation system.

Returns:

Families in order of first appearance.

Return type:

tuple[FieldFamily, …]

tidal.symbolic.spec_query.self_terms(equation, operator=None)[source]#

Return the terms where equation acts on its own field.

Parameters:
  • equation (ComponentEquation) – The equation to search.

  • operator (str | None) – Restrict to one operator, or None for every self-term.

Returns:

Matching self-terms.

Return type:

tuple[OperatorTerm, …]

tidal.symbolic.spec_query.terms_for(equation, field, operator)[source]#

Return all RHS terms of equation matching field and operator.

Plural by design. A component may carry several terms with the same (field, operator) key — for example a numeric base term and a symbolic background correction — and summing them is the caller’s whole question.

Parameters:
  • equation (ComponentEquation) – The equation to search.

  • field (str) – Field the operator acts on.

  • operator (str) – Operator name, e.g. "laplacian_x".

Returns:

Matching terms in their original order; empty when none match.

Return type:

tuple[OperatorTerm, …]