Source code for continuo.io.solution

"""The Solution object returned by a simulation.

A :class:`Solution` is a light, framework-free container for a solved
perfect-foresight path. It glues the per-segment results into one time
grid and offers direct array access; the optional pandas / xarray
conversions are imported lazily so those packages stay optional.

Each :class:`Segment` keeps its own realised slice plus the metadata of
the regime it belongs to — the active exogenous configuration
(``info_set``) and the terminal steady state used as the jump anchor — so
downstream code can label or compare regimes (e.g. the discontinuous jump
in a control at a surprise) without re-deriving the revelation structure.
"""

from __future__ import annotations

from dataclasses import dataclass, field

import numpy as np

__all__ = ["Segment", "Solution"]


[docs] @dataclass(eq=False) class Segment: """One realised segment of the path and the regime it was solved under.""" start_time: float times: np.ndarray path: np.ndarray names: tuple[str, ...] info_set: dict[str, float] terminal_ss: dict[str, float] iterations: int def __getitem__(self, name: str) -> np.ndarray: return self.path[:, self.names.index(name)]
[docs] @dataclass(eq=False) class Solution: """A solved path over ``[0, T]``, glued from one or more segments.""" segments: tuple[Segment, ...] names: tuple[str, ...] diagnostics: dict = field(default_factory=dict) converged: bool = True t: np.ndarray = field(init=False) path: np.ndarray = field(init=False) def __post_init__(self) -> None: # Segments hold non-overlapping realised slices, so the full grid is # just their concatenation (each reveal time appears exactly once). if self.segments: self.t = np.concatenate([segment.times for segment in self.segments]) self.path = np.concatenate([segment.path for segment in self.segments], axis=0) else: self.t = np.empty(0) self.path = np.empty((0, len(self.names))) def __getitem__(self, name: str) -> np.ndarray: """The path of one variable over the full grid.""" return self.path[:, self.names.index(name)] def __getattr__(self, name: str) -> np.ndarray: # Attribute alias (sol.K) for variables, when there is no real # attribute of that name. __getattr__ only fires on missing lookups. try: names = object.__getattribute__(self, "names") except AttributeError: raise AttributeError(name) from None if name in names: return self[name] raise AttributeError(name)
[docs] def to_dataframe(self): """Return the path as a time-indexed :class:`pandas.DataFrame`.""" import pandas as pd return pd.DataFrame(self.path, index=pd.Index(self.t, name="t"), columns=list(self.names))
[docs] def to_xarray(self): """Return the path as an :class:`xarray.Dataset` over the time coordinate.""" import xarray as xr data = {name: ("t", self[name]) for name in self.names} return xr.Dataset(data, coords={"t": self.t})