Source code for pycif.plugins.modes.training.trainer

"""Single entry point for training surrogate models against a CIF obs operator.

Usage::

    trainer = SurrogateTrainer(obs_operator, backend="torch")  # or "jax"
    H      = trainer.wrap_operator()
    model, losses = trainer.train(model, sample_fn, n_steps=5000)
    trainer.export("surrogate.onnx", model, input_shape=(1, N_STATE))

Backends are loaded lazily: importing this module never triggers an
ImportError even when PyTorch or JAX are absent.
"""
from __future__ import annotations

from typing import Any, Callable, List, Optional, Tuple


[docs] class SurrogateTrainer: """Facade over surrogate backends; dispatches to PyTorch or JAX.""" def __init__(self, obs_operator: Any, backend: str = "torch") -> None: self.obs_operator = obs_operator self.backend_name = backend self._backend: Optional[Any] = None # ------------------------------------------------------------------ def _load_backend(self) -> Any: if self._backend is not None: return self._backend if self.backend_name == "torch": from .backends.torch_backend import TorchBackend self._backend = TorchBackend(self.obs_operator) elif self.backend_name == "jax": from .backends.jax_backend import JaxBackend self._backend = JaxBackend(self.obs_operator) else: raise ValueError( f"Unknown backend {self.backend_name!r}. Choose 'torch' or 'jax'." ) return self._backend # ------------------------------------------------------------------
[docs] def build_default_model( self, n_state: int, n_obs: int, hidden_size: int = 256, n_hidden_layers: int = 2, ) -> Any: """Return a default untrained model for the active backend. Uses ``torch.nn.Sequential`` for ``"torch"`` and a Flax ``linen.Module`` for ``"jax"``. Only the active backend's framework is imported. """ return self._load_backend().build_default_model( n_state, n_obs, hidden_size, n_hidden_layers )
# ------------------------------------------------------------------
[docs] def wrap_operator(self, run_mode: str = "fwd", reload_results: bool = False) -> Callable: """Return a framework-native differentiable callable for the CIF operator. Args: run_mode: ``"fwd"`` wraps the full forward; ``"tl"`` wraps the tangent-linear operator. The backward always uses the adjoint. reload_results: Forwarded to every operator call. """ return self._load_backend().wrap_operator( run_mode=run_mode, reload_results=reload_results )
[docs] def train( self, model: Any, sample_fn: Callable[[int], Any], n_steps: int = 5000, run_mode: str = "fwd", reload_results: bool = False, adjoint_consistency: bool = False, lambda_adj: float = 0.1, ) -> Tuple[Any, List[float]]: """Train *model* against the live CIF operator. Args: model: Framework-native model (``nn.Module`` or Flax module). sample_fn: ``callable(batch_size) -> tensor | ndarray`` drawing state-space samples; shape ``(N_state,)`` per sample. n_steps: Number of gradient steps. reload_results: If True, reloads already-computed CIF simulations from disk instead of re-running them. adjoint_consistency: If True, adds a VJP-agreement loss term (Mode 3); otherwise Mode 2 (online teacher only). lambda_adj: Weight of the adjoint consistency loss. Returns: ``(trained_model, losses)`` """ return self._load_backend().train( model, sample_fn, n_steps, run_mode=run_mode, reload_results=reload_results, adjoint_consistency=adjoint_consistency, lambda_adj=lambda_adj, )
[docs] def export(self, path: str, model: Any, input_shape: Tuple[int, ...]) -> None: """Export *model* to ONNX at *path*.""" self._load_backend().export_onnx(path, model, input_shape)