Skip to content

PINC

Conventional soft-IC PINN-for-control (PINC) surrogate: generic MLP with an autograd derivative.

PINC

PINC(n_state: int, n_cond: int, hidden_size: int = 64, hidden_layer: int = 2, act: type = nn.Tanh, ic_mode: str = 'soft', hard_ic_style: str = 'subtract', t_zero: float = -1.0, derivative_mode: str = 'forward')

Bases: Module

Continuous-time PINN-for-control surrogate: a generic MLP over [x_k | cond | t].

The conventional PINC architecture (Antonelo et al., 2021): a feed-forward network maps the initial state, conditioning, and normalized time directly to the state, x(t) = NN([x_k, cond, t]). It is the deliberate foil to :class:~tsfast.pinn.ddpinn.DampedAnsatzPINN and shares its I/O contract — same normalized [-1, 1] coordinates, same row layout [x_k (n_state) | cond (n_cond) | t (1)], same forward(X, derivative_flag) signature — so it rides the same collocation sampler, residual, and :class:~tsfast.pinn.ddpinn.DDPINNRollout unchanged. It differs in exactly two ways:

  • The initial condition is, by default (ic_mode="soft"), not exact — there is no ansatz pinning x(t_zero) = x_k; it is enforced softly by :class:PINCLearner via a penalty term. With ic_mode="hard" it is made exact by construction — x(t) = x_k + NN([feat, t]) − NN([feat, t_zero]) — the DD-PINN's trick on a generic basis, which needs no IC loss (train it with the base :class:SurrogatePINNLearner). The soft/hard pair isolates the "exact IC by construction" advantage; hard-vs-DD-PINN isolates the basis.
  • The time-derivative comes from autograd, not a closed form — derivative_flag=True differentiates the network output w.r.t. the time channel. (The hard-IC offset terms are constant in t, so they do not change dx/dt.)

The derivative is computed by forward-mode AD by default: since time is a single scalar input and the state is the (multi-channel) output, one dual forward pass gives dx/dt for all channels at cost O(1) in the output dimension, versus the O(n_state) backward passes reverse mode needs. The two modes are numerically identical (and both support the training double-backward); reverse is roughly break-even at n_state = 2 and forward pulls ahead as the state grows.

Parameters:

Name Type Description Default
n_state int

state dimension; x_k occupies the first n_state input channels and equals the output dimension.

required
n_cond int

conditioning channels (control + collocation vars) between x_k and t.

required
hidden_size int

width of the MLP.

64
hidden_layer int

number of hidden layers in the MLP.

2
act type

activation function class; defaults to Tanh — smooth (so the autograd derivative is well-behaved), matching the DD-PINN trunk and the standard PINN choice, since :class:~tsfast.models._core.layers.SeqLinear otherwise defaults to Mish.

Tanh
ic_mode str

"soft" (default; IC enforced by a :class:PINCLearner penalty) or "hard" (IC exact by construction, no IC loss; train with the base :class:~tsfast.pinn.ddpinn.SurrogatePINNLearner).

'soft'
hard_ic_style str

how the hard IC is constructed (ic_mode="hard" only): "subtract"x_k + (NN([feat, t]) − NN([feat, t_zero])) (the DD-PINN's −sin c trick; leaves dx/dt unconstrained, costs two net evals), or "multiply"x_k + (t − t_zero)·NN([feat, t]) (Lagaris trial-function form; one net eval, but pins dx/dt|_{t_zero} = NN([feat, t_zero])).

'subtract'
t_zero float

normalized-time value mapped to physical t = 0 (-1 for the [-1, 1] box); used only by ic_mode="hard" and must match the rollout's time map.

-1.0
derivative_mode str

"forward" (forward-mode AD, O(1) in outputs) or "reverse" (per-channel backward, O(n_state)). Identical results; "forward" scales better.

'forward'
Source code in tsfast/pinn/pinc.py
def __init__(
    self,
    n_state: int,
    n_cond: int,
    hidden_size: int = 64,
    hidden_layer: int = 2,
    act: type = nn.Tanh,
    ic_mode: str = "soft",
    hard_ic_style: str = "subtract",
    t_zero: float = -1.0,
    derivative_mode: str = "forward",
):
    super().__init__()
    if derivative_mode not in ("forward", "reverse"):
        raise ValueError(f"derivative_mode must be 'forward' or 'reverse', got {derivative_mode!r}")
    if ic_mode not in ("soft", "hard"):
        raise ValueError(f"ic_mode must be 'soft' or 'hard', got {ic_mode!r}")
    if hard_ic_style not in ("subtract", "multiply"):
        raise ValueError(f"hard_ic_style must be 'subtract' or 'multiply', got {hard_ic_style!r}")
    self.n_state = n_state
    self.n_cond = n_cond
    self.ic_mode = ic_mode
    self.hard_ic_style = hard_ic_style
    self.t_zero = t_zero
    self.derivative_mode = derivative_mode
    self.net = SeqLinear(
        n_state + n_cond + 1,
        n_state,
        hidden_size=hidden_size,
        hidden_layer=hidden_layer,
        act=act,
    )

PINCLearner

PINCLearner(*args, ic_weight: float = 1.0, t_zero: float = -1.0, **kw)

Bases: SurrogatePINNLearner

Physics-only trainer for a :class:PINC, adding the soft initial-condition penalty.

Extends :class:~tsfast.pinn.ddpinn.SurrogatePINNLearner with the IC term the DD-PINN gets for free by construction. Each collocation point's (x_k, cond) is re-evaluated at the time origin and penalized toward x_k, so the total objective is::

loss = residual(x_phys, cond_phys, dxdt_phys) + ic_weight · ‖NN([x_k, cond, t_zero]) − x_k‖²

The residual is the physical-unit ODE residual (unchanged from the base learner); the IC term is in normalized coordinates (the model's native space). They therefore live in different units, so ic_weight is a genuine tuning knob — the price of the soft IC, which the hard-IC DD-PINN avoids entirely. Everything else (collocation sampler, scalers, chain-rule factor, rollout) is inherited unchanged, keeping the PINC and DD-PINN arms comparable.

Parameters:

Name Type Description Default
ic_weight float

weight on the soft initial-condition penalty (relative to the physical residual).

1.0
t_zero float

normalized-time value mapped to physical t = 0 (-1 for the [-1, 1] time box); must match the rollout's time map and the DD-PINN default.

-1.0
Source code in tsfast/pinn/pinc.py
def __init__(self, *args, ic_weight: float = 1.0, t_zero: float = -1.0, **kw):
    super().__init__(*args, **kw)
    self.ic_weight = ic_weight
    self.t_zero = t_zero

physics_loss

physics_loss(X: Tensor) -> Tensor

ODE residual plus the soft IC penalty on a batch of normalized collocation points.

Source code in tsfast/pinn/pinc.py
def physics_loss(self, X: Tensor) -> Tensor:
    """ODE residual plus the soft IC penalty on a batch of normalized collocation points."""
    residual = super().physics_loss(X)
    x_k = X[..., : self.n_state]
    cond = X[..., self.n_state : self.n_state + self.n_cond]
    t0 = torch.full_like(X[..., -1:], self.t_zero)
    ic_pred = self.model(torch.cat([x_k, cond, t0], dim=-1))  # derivative_flag=False
    ic_loss = F.mse_loss(ic_pred, x_k)
    return residual + self.ic_weight * ic_loss