Source code for neuralfieldmanifold.models.deeplagfield

"""Joint order selection and time-varying AR coefficient estimation."""

from dataclasses import dataclass

import torch
from torch import nn
from torch.nn import functional as F

from .._validation import positive_integer


[docs] @dataclass(frozen=True) class DynamicsOutput: """Predicted dynamics for a batch of input sequences. Attributes ---------- coefficients : torch.Tensor, shape (batch, time, max_order) Lag-1-first coefficients, masked beyond each selected order. order_logits : torch.Tensor, shape (batch, n_orders) Unnormalized order-selection scores. order : torch.Tensor, shape (batch,) Selected AR order for each sequence. prediction : torch.Tensor, shape (batch, time) AR reconstruction using observed lag history. Missing history at the start of the sequence is zero-padded. """ coefficients: torch.Tensor order_logits: torch.Tensor order: torch.Tensor prediction: torch.Tensor
[docs] class DeepLagField(nn.Module): """Jointly estimate AR order and time-varying coefficients. Parameters ---------- sequence_length : int Number of time samples per input sequence. orders : tuple of int Increasing candidate AR orders, each smaller than sequence_length. hidden_size : int Hidden width of the order and dynamics multilayer perceptrons. Notes ----- The order network selects a lag mask that conditions the dynamics network. Coefficients beyond the selected order are zeroed. Training uses hard straight-through Gumbel sampling; evaluation uses argmax unless sample_order=True is passed to forward. """ def __init__(self, *, sequence_length=600, orders=(2, 3, 4, 5, 6), hidden_size=128): super().__init__() self.sequence_length = positive_integer(sequence_length, "sequence_length") self.hidden_size = positive_integer(hidden_size, "hidden_size") orders = tuple(positive_integer(p, "order") for p in orders) if ( not orders or tuple(sorted(set(orders))) != orders or max(orders) >= sequence_length ): raise ValueError( "orders must be unique, increasing, and smaller than sequence_length" ) self.orders = orders self.max_order = max(orders) self.order_block = nn.Sequential( nn.Linear(sequence_length, hidden_size), nn.ReLU(), nn.Linear(hidden_size, hidden_size), nn.ReLU(), nn.Linear(hidden_size, len(orders)), ) self.dynamics_block = nn.Sequential( nn.Linear(sequence_length + len(orders), hidden_size), nn.ReLU(), nn.Linear(hidden_size, hidden_size), nn.ReLU(), nn.Linear(hidden_size, sequence_length * self.max_order), )
[docs] def forward(self, x, *, temperature=1.0, sample_order=None): """Estimate coefficients, select orders, and reconstruct each sequence. Parameters ---------- x : torch.Tensor, shape (batch, sequence_length) Floating-point input sequences. temperature : float Positive temperature for Gumbel sampling. sample_order : bool or None Sample orders if True, or use argmax if False. None samples in training mode and uses argmax in evaluation mode. Returns ------- DynamicsOutput Coefficients, order logits, selected orders, and predictions. """ if ( x.ndim != 2 or x.shape[1] != self.sequence_length or not x.is_floating_point() ): raise ValueError( "Expected a floating tensor of shape (batch, sequence_length)" ) if temperature <= 0: raise ValueError("temperature must be positive") logits = self.order_block(x) if sample_order is None: sample_order = self.training if sample_order: selection = F.gumbel_softmax(logits, tau=temperature, hard=True) else: selection = F.one_hot(logits.argmax(-1), len(self.orders)).to(x.dtype) indices = selection.argmax(-1) orders = torch.as_tensor(self.orders, device=x.device)[indices] active = ( torch.arange(self.max_order, device=x.device)[None, :] < orders[:, None] ).to(x.dtype) raw = self.dynamics_block(torch.cat([x, selection], dim=1)) coefficients = ( raw.reshape(len(x), self.sequence_length, self.max_order) * active[:, None, :] ) lagged = x.new_zeros(len(x), self.sequence_length, self.max_order) for k in range(self.max_order): lagged[:, k + 1 :, k] = x[:, : self.sequence_length - k - 1] return DynamicsOutput( coefficients, logits, orders, (coefficients * lagged).sum(-1) )
[docs] def save(self, path): """Save architecture settings and weights, without optimizer state.""" torch.save( { "config": dict( sequence_length=self.sequence_length, orders=self.orders, hidden_size=self.hidden_size, ), "state_dict": self.state_dict(), }, path, )
[docs] @classmethod def load(cls, path, *, device="cpu"): """Load a package checkpoint in deterministic evaluation mode.""" payload = torch.load(path, map_location=device, weights_only=True) model = cls(**payload["config"]).to(device) model.load_state_dict(payload["state_dict"]) return model.eval()