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:
objectBoundary condition for one spatial axis.
- 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:
- to_side_bc()[source]#
Convert to a
SideBCSpecfor the operator layer.- Raises:
ValueError – If the BC type is “periodic” (not representable as a side BC).
- Return type:
- class tidal.symbolic.ComponentEquation(field_name, field_index, time_derivative_order, rhs_terms, constraint_solver=<factory>, kinetic_coefficient_symbolic=None)[source]#
Bases:
objectEquation of motion for a single field component.
- For a wave-type equation:
d^2/dt^2 field = sum of OperatorTerms
- Parameters:
field_name (str)
field_index (int)
time_derivative_order (int)
rhs_terms (tuple[OperatorTerm, ...])
constraint_solver (ConstraintSolverConfig)
kinetic_coefficient_symbolic (str | None)
- 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:
- 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:
- 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.,
xiin the dark photon torsion model). The RHS terms are stored WITHOUT the 1/kinetic_coefficient divisor. Usenormalize_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:Truewhenkinetic_coefficient_symboliccontains a spatial coordinate call such asx[]ory[]; time-only dependence (t[]) returnsFalse. 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_REover the expression string). UnlikeOperatorTerm.position_dependent, which prefers explicitcoordinate_dependentmetadata, 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.
- 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:
objectConfiguration for elliptic constraint solving.
When
enabledis 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:
- boundary_conditions#
Per-axis boundary conditions (e.g.,
{"x": ..., "y": ...}).- Type:
- 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:
- classmethod from_dict(data)[source]#
Create from a dictionary or return default (disabled).
- Parameters:
data (Mapping[str, Any] | None) – Parsed
constraint_solverblock from JSON, or None.- Returns:
Configuration instance.
- Return type:
- Raises:
ValueError – If
methodis not one of the recognized solver methods.
- boundary_conditions: dict[str, BoundaryCondition]#
- 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:
objectComplete system of field equations derived from a Lagrangian.
- Parameters:
n_components (int)
dimension (int)
spatial_dimension (int)
equations (tuple[ComponentEquation, ...])
coupling_matrix_symbolic (tuple[tuple[str | None, ...], ...])
canonical (CanonicalStructure | None)
- equations#
Equations for each component.
- Type:
- coordinates#
Coordinate names from JSON spacetime.coordinates (e.g., (“t”, “x”, “y”)). Defaults to empty tuple; use
effective_coordinatesfor a guaranteed non-empty result that infers names from dimension when not set.
- canonical#
Canonical momentum and Hamiltonian structure from Legendre transform. Present when the JSON spec includes a
"canonical"section (generated bytidal derivefor 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’skinetic_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. Anidentityself-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 <= 2by construction; a post-check raisesValueErrorif any residual has order > 2, indicating the[perturbation]config misses some higher-derivative term.- Return type:
- Raises:
ValueError – If any ε=0 base equation still has
time_derivative_order > 2after 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_symbolicmentions a small parameter, splits M(ε) into M₀ (parameter-free) and per-parameter corrections c_p·p viasplit_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_parametersargument
- Parameters:
small_parameters (Sequence[str]) – The names declared in
[perturbation].small_parametersof the TOML.- Returns:
A new
EquationSystemwith the canonicalized equations. Thereturned spec is idempotent under this transform; calling it twice
produces the same result.
selfis 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:
- dependency_closure(seeds)[source]#
Fields whose equations the evolution of
seedscan ever read.The transitive closure of “the equation of X references Y” from
seeds(a velocity referencev_Ycounts 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:
- 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_termstuple 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 andfilter_by_order(1)for the Pass 1 source terms of a linear perturbative expansion.- Parameters:
n (int)
- Return type:
- 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:
- Return type:
- 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
PerturbativeSolveris needed or the plain modal path suffices.- Return type:
- 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 totidal.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-dependentM(x)is a k-space convolutionM̂(k−k′), which modal does not implement (GH #427); the time-domain backends handle it viagrid=(GH #382).- Return type:
- property implicit_dynamical_sector: ImplicitDynamicalSector#
Promoted order-0 rows carrying second-order structure (GH #457).
See
ImplicitDynamicalSectorfor 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).
- max_order()[source]#
Return the maximum
order_in_epsacross all RHS terms.Returns 0 for baseline theories (no
[perturbation]section or no terms with non-zero order). Use to gate--perturbative-ordervalidation and to size the Pass loop in the driver.- Return type:
- 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).
- equations: tuple[ComponentEquation, ...]#
- class tidal.symbolic.OperatorTerm(coefficient, operator, field, coefficient_symbolic=None, time_dependent=False, coordinate_dependent=(), order_in_eps=0)[source]#
Bases:
objectA single term in the RHS of a field equation.
Represents: coefficient * operator(field)
- Parameters:
- 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:
- 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.
- 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 WolframComputeOrderInEpshelper and consumed byEquationSystem.filter_by_orderto 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:
- 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:
- property position_dependent: bool#
Whether the coefficient depends on spatial coordinates.
Returns
Truewhencoordinate_dependentis non-empty (explicit declaration), or whencoefficient_symboliccontains a spatial coordinate call pattern such asx[]ory[](auto-detection for JSON exports that predate thecoordinate_dependentfield). Time-only dependence (t[]) returnsFalse.
- 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 > 2guard 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:
- 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_symbolicdirectly viatidal.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 producingf/xiin the JSON which becomes a ZeroDivisionError at xi=0). The kinetic coefficient is stored inComponentEquation.kinetic_coefficient_symbolic.This function evaluates those coefficients at
paramsand:K ≠ 0 — divides each RHS term by K (numeric
coefficient / K; symboliccoefficient_symbolicwrapped as(expr) / (kc_sym)so that subsequentCoefficientEvaluatorcalls remain correct).K = 0 — the kinetic term vanishes; the field becomes a constraint (
time_derivative_order=0, emptyrhs_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_symbolicare returned unchanged. The function is idempotent: calling it on an already-normalized spec (allkinetic_coefficient_symbolicare 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
EquationSystemwith all kinetic coefficients normalized.- Return type:
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).
- 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_tis 1,d<N>_tis N, and mixed time-space operators carry their T exponent (mixed_T_S1x= 1,mixed_T2_S1x= 2, numericmixed_<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.
- class tidal.symbolic.json_loader.LHSStructure(expression, time_order, space_order=0, kinetic_coefficient_symbolic=None)[source]#
Bases:
objectStructure 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:
- 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_coefficientat 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 isxi. Atxi=0torsion 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:
- Raises:
ValueError – If
orderdict 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:
objectA single term in the RHS of a field equation.
Represents: coefficient * operator(field)
- Parameters:
- 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:
- 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.
- 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 WolframComputeOrderInEpshelper and consumed byEquationSystem.filter_by_orderto 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:
- property position_dependent: bool#
Whether the coefficient depends on spatial coordinates.
Returns
Truewhencoordinate_dependentis non-empty (explicit declaration), or whencoefficient_symboliccontains a spatial coordinate call pattern such asx[]ory[](auto-detection for JSON exports that predate thecoordinate_dependentfield). Time-only dependence (t[]) returnsFalse.
- 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:
- class tidal.symbolic.json_loader.BoundaryCondition(type, value=None, derivative=None, gamma=None)[source]#
Bases:
objectBoundary condition for one spatial axis.
- 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:
- to_side_bc()[source]#
Convert to a
SideBCSpecfor the operator layer.- Raises:
ValueError – If the BC type is “periodic” (not representable as a side BC).
- Return type:
- class tidal.symbolic.json_loader.ConstraintSolverConfig(enabled=False, method='auto', boundary_conditions=<factory>, max_iterations=20, tolerance=1e-08)[source]#
Bases:
objectConfiguration for elliptic constraint solving.
When
enabledis 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:
- boundary_conditions#
Per-axis boundary conditions (e.g.,
{"x": ..., "y": ...}).- Type:
- 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:
- boundary_conditions: dict[str, BoundaryCondition]#
- classmethod from_dict(data)[source]#
Create from a dictionary or return default (disabled).
- Parameters:
data (Mapping[str, Any] | None) – Parsed
constraint_solverblock from JSON, or None.- Returns:
Configuration instance.
- Return type:
- Raises:
ValueError – If
methodis not one of the recognized solver methods.
- class tidal.symbolic.json_loader.HamiltonianFactor(field, operator)[source]#
Bases:
objectOne 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.
- 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:
- 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:
objectA single quadratic term in the Hamiltonian density.
H = Σ coefficient * factor_a * factor_b
- Parameters:
coefficient (float)
factor_a (HamiltonianFactor)
factor_b (HamiltonianFactor)
coefficient_symbolic (str | None)
term_class (str)
order_in_eps (int)
- factor_a#
First field factor.
- Type:
- factor_b#
Second field factor (may equal factor_a for squared terms).
- Type:
- 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 viaposition_dependentcovers those cases.
- term_class#
Classification:
"self"(both factors reference the same base field) or"interaction"(cross-field coupling). Defaults to"unknown"for older JSONs; useis_self_energyproperty which auto-classifies by comparing factor field names.- Type:
- order_in_eps#
Perturbative order of this term: total exponent in declared
small_parameters. Computed by Wolfram’sComputeOrderInEpsand emitted explicitly byParseSingleHamiltonianTerm. 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:
- factor_a: HamiltonianFactor#
- factor_b: HamiltonianFactor#
- property position_dependent: bool#
True if the coefficient is a function of spatial coordinates.
Returns
Truewhencoordinate_dependentis non-empty (explicit declaration), or whencoefficient_symboliccontains a coordinate call pattern such asx[]ory[](auto-detection for JSON exports that predate thecoordinate_dependentfield).
- property is_self_energy: bool#
True if both factors reference the same base field (self-energy).
Uses
term_classwhen available (from Wolfram export), otherwise classifies by comparing base field names (strippingv_prefix).
- classmethod from_dict(data)[source]#
Parse from JSON dict.
Reads the explicit
order_in_epsfield emitted by Wolfram’sComputeOrderInEps(single source of truth, matching the equation side atOperatorTerm.order_in_eps). For legacy JSONs without the explicit field, falls back to the heuristic1 if coefficient_symbolic is not None else 0— correct for current EH but coincidental in general. The strict consistency check lives intidal validate.- Parameters:
data (Mapping[str, Any])
- Return type:
- class tidal.symbolic.json_loader.CanonicalStructure(hamiltonian_terms, volume_element=None)[source]#
Bases:
objectCanonical 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
equationsarray directly.- Parameters:
hamiltonian_terms (tuple[HamiltonianTerm, ...])
volume_element (str | None)
- 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.Nonefor 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, ...]#
- class tidal.symbolic.json_loader.ComponentEquation(field_name, field_index, time_derivative_order, rhs_terms, constraint_solver=<factory>, kinetic_coefficient_symbolic=None)[source]#
Bases:
objectEquation of motion for a single field component.
- For a wave-type equation:
d^2/dt^2 field = sum of OperatorTerms
- Parameters:
field_name (str)
field_index (int)
time_derivative_order (int)
rhs_terms (tuple[OperatorTerm, ...])
constraint_solver (ConstraintSolverConfig)
kinetic_coefficient_symbolic (str | None)
- 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:
- 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.,
xiin the dark photon torsion model). The RHS terms are stored WITHOUT the 1/kinetic_coefficient divisor. Usenormalize_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:Truewhenkinetic_coefficient_symboliccontains a spatial coordinate call such asx[]ory[]; time-only dependence (t[]) returnsFalse. 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_REover the expression string). UnlikeOperatorTerm.position_dependent, which prefers explicitcoordinate_dependentmetadata, 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:
- class tidal.symbolic.json_loader.ImplicitDynamicalSector(fields, reasons)[source]#
Bases:
objectConstraint-classified rows that carry second-order structure.
time_derivative_order == 0states 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.fieldsis 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 abouttime_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 == 0directly for a routing decision, and never introduce a second classification path.- 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), orclosure(joined only through connectivity). Multiple tags are+-joined in sorted order.
- 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:
objectComplete system of field equations derived from a Lagrangian.
- Parameters:
n_components (int)
dimension (int)
spatial_dimension (int)
equations (tuple[ComponentEquation, ...])
coupling_matrix_symbolic (tuple[tuple[str | None, ...], ...])
canonical (CanonicalStructure | None)
- equations#
Equations for each component.
- Type:
- coordinates#
Coordinate names from JSON spacetime.coordinates (e.g., (“t”, “x”, “y”)). Defaults to empty tuple; use
effective_coordinatesfor a guaranteed non-empty result that infers names from dimension when not set.
- canonical#
Canonical momentum and Hamiltonian structure from Legendre transform. Present when the JSON spec includes a
"canonical"section (generated bytidal derivefor non-linearization theories). None for legacy specs or linearization theories.- Type:
CanonicalStructure | None
- equations: tuple[ComponentEquation, ...]#
- canonical: CanonicalStructure | None = None#
- 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
ImplicitDynamicalSectorfor 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
seedscan ever read.The transitive closure of “the equation of X references Y” from
seeds(a velocity referencev_Ycounts 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:
- 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_termstuple 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 andfilter_by_order(1)for the Pass 1 source terms of a linear perturbative expansion.- Parameters:
n (int)
- Return type:
- max_order()[source]#
Return the maximum
order_in_epsacross all RHS terms.Returns 0 for baseline theories (no
[perturbation]section or no terms with non-zero order). Use to gate--perturbative-ordervalidation and to size the Pass loop in the driver.- Return type:
- has_corrections()[source]#
Return True if any RHS term has
order_in_eps > 0.Cheap check used by the CLI to decide whether
PerturbativeSolveris needed or the plain modal path suffices.- Return type:
- 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 totidal.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-dependentM(x)is a k-space convolutionM̂(k−k′), which modal does not implement (GH #427); the time-domain backends handle it viagrid=(GH #382).- Return type:
- 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_symbolicmentions a small parameter, splits M(ε) into M₀ (parameter-free) and per-parameter corrections c_p·p viasplit_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_parametersargument
- Parameters:
small_parameters (Sequence[str]) – The names declared in
[perturbation].small_parametersof the TOML.- Returns:
A new
EquationSystemwith the canonicalized equations. Thereturned spec is idempotent under this transform; calling it twice
produces the same result.
selfis 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:
- 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’skinetic_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. Anidentityself-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 <= 2by construction; a post-check raisesValueErrorif any residual has order > 2, indicating the[perturbation]config misses some higher-derivative term.- Return type:
- Raises:
ValueError – If any ε=0 base equation still has
time_derivative_order > 2after 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:
- Return type:
- 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:
objectProvenance 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:
- 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 outsidekeepare 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
keepnames an unknown field, or is not closed (a kept equation references an omitted field) — pass aEquationSystem.dependency_closure().- Parameters:
- 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 > 2guard 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:
- 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_symbolicdirectly viatidal.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 producingf/xiin the JSON which becomes a ZeroDivisionError at xi=0). The kinetic coefficient is stored inComponentEquation.kinetic_coefficient_symbolic.This function evaluates those coefficients at
paramsand:K ≠ 0 — divides each RHS term by K (numeric
coefficient / K; symboliccoefficient_symbolicwrapped as(expr) / (kc_sym)so that subsequentCoefficientEvaluatorcalls remain correct).K = 0 — the kinetic term vanishes; the field becomes a constraint (
time_derivative_order=0, emptyrhs_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_symbolicare returned unchanged. The function is idempotent: calling it on an already-normalized spec (allkinetic_coefficient_symbolicare 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
EquationSystemwith all kinetic coefficients normalized.- Return type:
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):
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:
objectOne 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 termcoeff · op(\\phi_j).Operator labels follow the same vocabulary the JSON RHS uses (
identity,gradient_z,d2_t, …); the LaTeX renderer reusestidal.symbolic.latex.operator_to_latex()to map them.
- class tidal.symbolic.kinetic_matrix.KineticMatrix(row_fields, column_fields, cells)[source]#
Bases:
objectAssembled 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 perEquationSystem.equationsentry); 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.
- column_fields#
Field name for each column (length
n_cols). The firstn_rowsentries matchrow_fields(q-fields in equation order); any trailing entries are v-fields ordered by first appearance on a RHS.
- cells#
n_rows x n_colsgrid ofKineticMatrixCell.- Type:
tuple[tuple[tidal.symbolic.kinetic_matrix.KineticMatrixCell, …], …]
- cells: tuple[tuple[KineticMatrixCell, ...], ...]#
- 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 byeq.field_name). Columns are the union of:the same q-fields as the rows (in equation order), and
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 readskinetic_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 ofterm.field(possibly a v-field).
- Parameters:
spec (EquationSystem)
- Return type:
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}'
- 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:
- tidal.symbolic.latex.operator_to_latex(operator, field_latex)[source]#
Render an operator applied to a field in LaTeX.
- tidal.symbolic.latex.equation_to_latex(eq, spec)[source]#
Convert a single component equation to LaTeX.
- Parameters:
eq (ComponentEquation) – The equation to render.
spec (EquationSystem) – The parent equation system (for coordinates and tensor metadata).
- Returns:
LaTeX string (without environment wrapping).
- Return type:
- 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:
- Parameters:
terms (list[HamiltonianTerm])
spec (EquationSystem)
- tidal.symbolic.latex.kinetic_matrix_to_latex(km, spec)[source]#
Render an assembled
KineticMatrixas 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 anyv_<X>referenced on RHS terms (km.column_fields).Output wraps the cell grid in an
arrayenvironment 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. Reusesoperator_to_latex(),coefficient_to_latex(), andfield_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:
km (KineticMatrix)
spec (EquationSystem)
- Return type:
- 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.
- 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 ownalignedblock inside an outergather*— 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:
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,
ValueErroris 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:
Remapping operator names (
laplacian_z → laplacian_x)Removing terms with killed-axis operators
Updating spacetime metadata (dimension, signature, coordinates)
Remapping coordinate references in coefficient expressions
Handling volume element (keep if surviving-coord-only, error if not)
Adding provenance metadata
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:
Rational normal form (
_Ratio) — expressions are expanded into a quotient of Laurent polynomials overFractioncoefficients, 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.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:
EnumElement of the sign domain
{0, +, −, 0+, 0−, ⊤}.NONNEGATIVE/NONPOSITIVEare distinct fromPOSITIVE/NEGATIVEbecause 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'#
- class tidal.symbolic.sign_algebra.SignResult(sign, tactic, free_names=(), assumptions=<factory>, value=None, numeric=None)[source]#
Bases:
objectOutcome of a sign or ratio query, with the reasoning that produced it.
- Parameters:
- sign#
The lattice verdict.
Sign.UNKNOWNmeans not proven, never “probably zero” or “probably positive”.- Type:
- tactic#
Which rung of the ladder decided it (
"literal","normal-form","lattice","assumption","numeric") or"undecided".- Type:
- assumptions#
Caller-declared assumptions that were actually used. Never implicit, and always surfaced so a reader can audit what the verdict rests on.
- value#
The exact rational value when the query reduced to a constant.
- Type:
Fraction | None
- tidal.symbolic.sign_algebra.are_equal(left, right)[source]#
Decide whether two coefficient expressions are identically equal.
Three-valued:
Truewhen proven equal,Falsewhen proven different, andNonewhen neither could be established.
- 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 - xiversus(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.
- tidal.symbolic.sign_algebra.constant_ratio(numerator, denominator)[source]#
Return
numerator / denominatorwhen 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.
- tidal.symbolic.sign_algebra.evaluate_numeric(expr, parameters)[source]#
Evaluate expr numerically, or return
Noneif anything is unresolved.Delegates to
tidal.symbolic._kinetic_eval.evaluate_with_substitutions(), the repository’s existing restricted-AST evaluator, rather than reimplementing evaluation here.Eis bound to Euler’s number so thatE**uterms 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.
- tidal.symbolic.sign_algebra.free_names(expr)[source]#
Return the sorted free symbol names appearing in expr.
Eis excluded — it denotes Euler’s number, not a parameter.
- 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/-xiis exactly1regardless ofxi— but not always, and sums over distinct parameters generally do not cancel. Those cases returnSign.UNKNOWNrather than a guess.- Parameters:
numerator (str | float | None) – Coefficient expressions;
Nonemeans1.denominator (str | float | None) – Coefficient expressions;
Nonemeans1.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 sharpenskappa^2from0+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.valueset when the ratio reduced to an exact rational constant.- Return type:
- 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 sharpenskappa^2from0+to+. Leave it empty for parameters that genuinely reach zero (xi,b5).parameters (Mapping[str, float] | None) – Optional numeric values. These populate
SignResult.numericfor corroboration but never upgrade an unproven structural verdict.
- Returns:
The verdict, the deciding tactic, and the supporting context.
- Return type:
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_coefficientWhat 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 baselaplacian_xand aB0^2*rhocorrection — 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_familiesWhich components belong together? Grouped by
tensor_headand classified bytensor_indices, never by parsing the numeric suffix. Index 0 is the temporal component of a rank-1 field, but rank-3 torsion has components liket_13 = [2, 0, 2]where that reading is meaningless.coefficient_provenanceWhere 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_systemsDid 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_ungaugedcase). 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_coefficienthas exactly two consumers — thetidal inspectquery surface, andEquationSystem._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 inorder_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.
CoefficientEvaluatorcaches per(eq_idx, term_idx)and keeps anid(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:
objectEvery place one coefficient is recorded, grouped by how it relates.
The three groups are kept apart deliberately. Only
effectiveand its parts are this coefficient;matrix_entryis a redundant re-encoding of it;hamiltonian_termsis 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 (EffectiveCoefficient)
hamiltonian_terms (tuple[HamiltonianTerm, ...])
checks (tuple[ConsistencyCheck, ...])
- effective#
The coefficient itself, summed and kinetic-normalized.
- Type:
- matrix_entry#
The mass/coupling matrix encoding, for
identityoperators. Note the matrix convention ismatrix[i][j] = -(coefficient)and is not normalized by the kinetic coefficient.
- 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:
- effective: EffectiveCoefficient#
- hamiltonian_terms: tuple[HamiltonianTerm, ...]#
- checks: tuple[ConsistencyCheck, ...]#
- class tidal.symbolic.spec_query.ConsistencyCheck(name, status, detail)[source]#
Bases:
objectOne cross-representation check, and whether it could be settled.
- status#
"ok","mismatch", or"undecided"."undecided"is a first-class outcome, not a failure.- Type:
- class tidal.symbolic.spec_query.EffectiveCoefficient(equation_field, field, operator, terms, numerator, kinetic)[source]#
Bases:
objectThe 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:
- terms#
The individual contributing terms, kept so a reader can see the parts.
- Type:
tuple[OperatorTerm, …]
- terms: tuple[OperatorTerm, ...]#
- class tidal.symbolic.spec_query.EquationComparison(field, verdict, changed_keys, undecided_keys, detail)[source]#
Bases:
objectHow one equation differs between two specs.
- Parameters:
- 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:
- class tidal.symbolic.spec_query.FieldFamily(head, rank, members, indices, exact)[source]#
Bases:
objectComponents sharing a tensor head, classified by their index structure.
- Parameters:
- exact#
Truewhen grouping used exportedtensor_headmetadata.Falsemeans 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:
- 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_13has indices(2, 0, 2). ReturnsNonewhen index metadata is unavailable.
- 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_0alone againsta_1..a_3, and for rank-3 torsion it separates the twelve components into three classes that the flatt_0..t_23numbering 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.
- class tidal.symbolic.spec_query.SystemDiff(comparisons, only_left, only_right)[source]#
Bases:
objectResult of comparing two equation systems.
- Parameters:
- comparisons#
Per-equation results for fields present in both systems.
- Type:
- comparisons: tuple[EquationComparison, ...]#
- 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.
- 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:
- 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:
left (ComponentEquation) – The two versions.
right (ComponentEquation) – The two versions.
- Returns:
The verdict and the keys responsible for it.
- Return type:
- 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:
- 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-Heisenberga_0this 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: callfilter_by_order()first and read the coefficient off the filtered spec, which for the same component yields just1.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:
- tidal.symbolic.spec_query.field_families(spec)[source]#
Group a spec’s components into tensor families.
Uses the
tensor_head/tensor_rank/tensor_indicesmetadata exported since78374c1. Specs predating it fall back to splitting the component name on its final underscore, and the resulting families are markedexact=Falseso 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
Nonefor 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, …]