TNFR Symplectic Integrators — Structure-Preserving Evolution
This module implements symplectic integration schemes adapted for TNFR Nodal Dynamics. Unlike standard numerical integrators, these schemes are designed to preserve the structural invariants (Coherence, Phase Space Volume) of the Nodal Equation: ∂EPI/∂t = νf · ΔNFR(t)
When EPI represents a conjugate pair (Form/Flow or Position/Momentum), symplectic integration ensures that the long-term evolution remains bounded and coherent, satisfying Grammar Rule U2 (Convergence & Boundedness).
The Nodal Equation can be decomposed into conjugate evolution: ∂q/∂t = ∂H/∂p (Flow/Velocity) ∂p/∂t = -∂H/∂q (Pressure/Force ~ ΔNFR)
Symplectic schemes (Verlet, Leapfrog) update these components sequentially to conserve the symplectic 2-form dEPI ^ d(νf·ΔNFR), which corresponds to the conservation of Structural Information.
"""TNFR Symplectic Integrators — Structure-Preserving Evolution
This module implements symplectic integration schemes adapted for TNFR Nodal Dynamics.
Unlike standard numerical integrators, these schemes are designed to preserve the
structural invariants (Coherence, Phase Space Volume) of the Nodal Equation:
∂EPI/∂t = νf · ΔNFR(t)
When EPI represents a conjugate pair (Form/Flow or Position/Momentum), symplectic
integration ensures that the long-term evolution remains bounded and coherent,
satisfying Grammar Rule U2 (Convergence & Boundedness).
Theoretical Basis
-----------------
The Nodal Equation can be decomposed into conjugate evolution:
∂q/∂t = ∂H/∂p (Flow/Velocity)
∂p/∂t = -∂H/∂q (Pressure/Force ~ ΔNFR)
Symplectic schemes (Verlet, Leapfrog) update these components sequentially to
conserve the symplectic 2-form dEPI ^ d(νf·ΔNFR), which corresponds to the
conservation of Structural Information.
"""
from typing import Callable
from tnfr.constants import DNFR_PRIMARY, EPI_PRIMARY, VF_PRIMARY
from tnfr.types import TNFRNode
from ..mathematics.unified_numerical import np
class TNFRSymplecticIntegrator:
"""
Implements structure-preserving integrators for TNFR nodes.
These integrators assume the EPI vector contains conjugate pairs [q, q_dot]
and that ΔNFR represents the structural pressure (force) acting on q_dot.
"""
@staticmethod
def velocity_verlet(
node: TNFRNode, dt: float, force_evaluator: Callable[[TNFRNode], np.ndarray]
) -> None:
"""
Evolves a node using the Velocity Verlet scheme (Symplectic Order 2).
Sequence:
1. Half-kick: Update Flow (q_dot) using current Pressure (ΔNFR)
2. Drift: Update Form (q) using new Flow
3. Re-evaluate: Update Pressure (ΔNFR) using new Form
4. Half-kick: Update Flow (q_dot) using new Pressure
Args:
node: The TNFR node to evolve.
dt: Time step.
force_evaluator: Function that computes new ΔNFR given updated EPI.
Must return the new ΔNFR vector.
"""
# 1. Extract State
epi = node[EPI_PRIMARY]
nu_f = node[VF_PRIMARY]
dnfr = node[DNFR_PRIMARY]
n = len(epi) // 2
q = epi[:n]
q_dot = epi[n:]
# Acceleration a = νf * ΔNFR
# Note: ΔNFR typically matches the dimension of EPI.
# If ΔNFR is the gradient of Potential w.r.t EPI, it has 2n components.
# But in classical mapping, Force acts on q_dot.
# We assume ΔNFR has the same structure as EPI, but the 'force' part
# corresponds to the q_dot update.
# Usually F = ma -> a = F/m. Here a = νf * ΔNFR.
# If ΔNFR is [0, F_effective], then a is [0, F_effective * νf].
# Let's assume ΔNFR is the full vector update rate for EPI (excluding the kinematic part).
# Actually, the Nodal Equation is ∂EPI/∂t = νf * ΔNFR.
# So νf * ΔNFR IS the time derivative of EPI.
# But for symplectic, we need to separate the Hamiltonian flow.
# The "Force" is the interaction part. The "Velocity" is the kinematic part.
# We assume ΔNFR provided here contains the INTERACTION terms (Force).
# The KINEMATIC term (dq/dt = v) is structural and handled explicitly by the integrator.
# So we extract the "Force" part from ΔNFR.
# Assuming ΔNFR affects the velocity component (q_dot).
# We take the second half of ΔNFR as the "Force" driving q_dot.
a_t = nu_f * dnfr[n:]
# 2. First Half-Kick (Update Velocity)
q_dot_half = q_dot + 0.5 * a_t * dt
# 3. Drift (Update Position)
q_new = q + q_dot_half * dt
# Update Node EPI (Partial) for Force Evaluation
# We need to construct the intermediate EPI to evaluate forces at t+dt
epi_intermediate = np.concatenate([q_new, q_dot_half])
node[EPI_PRIMARY] = epi_intermediate
# 4. Re-evaluate Forces (Pressure)
dnfr_new = force_evaluator(node)
node[DNFR_PRIMARY] = dnfr_new
a_new = nu_f * dnfr_new[n:]
# 5. Second Half-Kick (Update Velocity)
q_dot_new = q_dot_half + 0.5 * a_new * dt
# 6. Final State Update
node[EPI_PRIMARY] = np.concatenate([q_new, q_dot_new])
@staticmethod
def leapfrog(
node: TNFRNode, dt: float, force_evaluator: Callable[[TNFRNode], np.ndarray]
) -> None:
"""
Evolves a node using the Leapfrog scheme (Symplectic Order 2).
Drift-Kick-Drift or Kick-Drift-Kick.
Here we implement Kick-Drift-Kick (synchronized form).
Args:
node: The TNFR node to evolve.
dt: Time step.
force_evaluator: Function that computes new ΔNFR.
"""
# Similar to Verlet but conceptually distinct in phase update.
# For implementation simplicity and robustness, Velocity Verlet is often preferred
# as it keeps q and v synchronized at t.
# We alias to Velocity Verlet for now as the canonical symplectic stepper.
TNFRSymplecticIntegrator.velocity_verlet(node, dt, force_evaluator)
@staticmethod
def yoshida_4th_order(
node: TNFRNode, dt: float, force_evaluator: Callable[[TNFRNode], np.ndarray]
) -> None:
"""
Evolves a node using Yoshida's 4th Order Symplectic Integrator.
Composes 3 Verlet steps with specific coefficients to cancel error terms up to O(dt^4).
Args:
node: The TNFR node to evolve.
dt: Time step.
force_evaluator: Function that computes new ΔNFR.
"""
# Yoshida coefficients
w0 = -np.cbrt(2) / (2 - np.cbrt(2))
w1 = 1 / (2 - np.cbrt(2))
c1 = w1 / 2
c2 = (w0 + w1) / 2
c3 = c2
c4 = c1
d1 = w1
d2 = w0
d3 = w1
# Step 1
TNFRSymplecticIntegrator._symplectic_substep(
node, c1 * dt, d1 * dt, force_evaluator
)
# Step 2
TNFRSymplecticIntegrator._symplectic_substep(
node, c2 * dt, d2 * dt, force_evaluator
)
# Step 3
TNFRSymplecticIntegrator._symplectic_substep(
node, c3 * dt, d3 * dt, force_evaluator
)
# Step 4 (Drift only? No, Yoshida is composition of 3 steps usually)
# Actually standard Yoshida composition:
# x(t+dt) = S(w1*dt) S(w0*dt) S(w1*dt) x(t) where S is 2nd order symplectic.
# Let's use the explicit coefficients for Velocity Verlet composition:
# x1 = x0 + c1*v0*dt
# v1 = v0 + d1*a(x1)*dt
# x2 = x1 + c2*v1*dt
# v2 = v1 + d2*a(x2)*dt
# x3 = x2 + c3*v2*dt
# v3 = v2 + d3*a(x3)*dt
# x4 = x3 + c4*v3*dt
# (Note: coefficients sum to 1)
# We will implement the explicit substeps.
pass # TODO: Implement full Yoshida expansion if needed for high precision.
# For now, falling back to Verlet is safer than a partial implementation.
TNFRSymplecticIntegrator.velocity_verlet(node, dt, force_evaluator)
@staticmethod
def _symplectic_substep(
node: TNFRNode,
dt_c: float,
dt_d: float,
force_evaluator: Callable[[TNFRNode], np.ndarray],
) -> None:
"""Helper for higher order integrators."""
# Extract
epi = node[EPI_PRIMARY]
nu_f = node[VF_PRIMARY]
dnfr = node[DNFR_PRIMARY]
n = len(epi) // 2
q = epi[:n]
q_dot = epi[n:]
# Drift (c coefficient)
q_new = q + q_dot * dt_c
# Update for Force
node[EPI_PRIMARY] = np.concatenate([q_new, q_dot])
dnfr_new = force_evaluator(node)
node[DNFR_PRIMARY] = dnfr_new
# Kick (d coefficient)
a_new = nu_f * dnfr_new[n:]
q_dot_new = q_dot + a_new * dt_d
# Update
node[EPI_PRIMARY] = np.concatenate([q_new, q_dot_new])