Source code for pySimBlocks.core.config

# ******************************************************************************
#                                  pySimBlocks
#                     Copyright (c) 2026 Université de Lille & INRIA
# ******************************************************************************
#  This program is free software: you can redistribute it and/or modify it
#  under the terms of the GNU Lesser General Public License as published by
#  the Free Software Foundation, either version 3 of the License, or (at your
#  option) any later version.
#
#  This program is distributed in the hope that it will be useful, but WITHOUT
#  ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
#  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public License
#  for more details.
#
#  You should have received a copy of the GNU Lesser General Public License
#  along with this program.  If not, see <https://www.gnu.org/licenses/>.
# ******************************************************************************
#  Authors: see Authors.txt
# ******************************************************************************

from dataclasses import dataclass, field
from typing import Any, Dict, List


# ---------------------------------------------------------------------
# Classes
# ---------------------------------------------------------------------
[docs] @dataclass(frozen=True) class SimulationConfig: """Simulation execution configuration. Contains only execution-related parameters. Must not hold any model or block-specific information. """ #: Simulation time step in seconds. dt: float #: Simulation end time in seconds. T: float #: Simulation start time in seconds. t0: float = 0.0 #: Integration scheme, either ``"fixed"`` or ``"variable"``. solver: str = "fixed" #: Signals to log during simulation. logging: List[str] = field(default_factory=list) #: Clock source, either ``"internal"`` or ``"external"``. clock: str = "internal"
[docs] def validate(self) -> None: """Verify that the configuration is consistent. Checks that dt > 0, T > t0, solver and clock are known values. Raises: ValueError: If any parameter is invalid or out of range. """ if self.dt <= 0.0: raise ValueError("SimulationConfig.dt must be > 0") if self.T <= self.t0: raise ValueError("SimulationConfig.T must be > t0") if self.solver not in {"fixed", "variable"}: raise ValueError( f"Unknown solver '{self.solver}'. " "Allowed values: {'fixed', 'variable'}" ) if self.clock not in {"internal", "external"}: raise ValueError( f"Unknown clock '{self.clock}'. " "Allowed values: {'internal', 'external'}" )
[docs] @dataclass class PlotConfig: """Plot configuration. Describes how logged signals should be visualized. Contains no plotting logic. """ #: List of plot descriptors. Each descriptor is a dict with at least a #: ``"signals"`` field, which is a list of signal names to plot together plots: List[Dict[str, Any]]
[docs] def validate(self) -> None: """Verify that each plot descriptor has the required fields. Classic plots require ``signals``. Manual layout presets use ``layout: manual`` with a ``panels`` list instead. Raises: ValueError: If a plot descriptor is invalid. """ for i, plot in enumerate(self.plots): layout = str(plot.get("layout", "")).strip().lower() if layout == "manual": panels = plot.get("panels") if not isinstance(panels, list) or not panels: raise ValueError( f"Plot #{i} (layout: manual) must define a non-empty 'panels' list" ) for j, panel in enumerate(panels): if not isinstance(panel, dict): raise ValueError( f"Plot #{i} panel #{j} must be a mapping" ) selection = panel.get("selection") if isinstance(selection, dict) and selection: continue signals = panel.get("signals") if isinstance(signals, list) and signals: continue raise ValueError( f"Plot #{i} panel #{j} must define 'selection' or 'signals'" ) continue if "signals" not in plot: raise ValueError( f"Plot #{i} is missing required field 'signals'" ) if not isinstance(plot["signals"], list): raise ValueError( f"'signals' in plot #{i} must be a list" )