"""Simulation options and progress data; no numerical solver implementation.""" from __future__ import annotations from dataclasses import dataclass from typing import Sequence @dataclass(frozen=True) class SolverActivitySnapshot: """Low-cost, additive view of work inside an integration task. ``accepted_time`` deliberately changes only after an accepted solver step. Trial evaluations may continue to advance ``activity_sequence`` and ``current_trial_time`` while that public progress value stays fixed. """ activity_sequence: int activity_kind: str current_trial_time: float | None rhs_call_count: int accepted_step_sequence: int accepted_time: float | None solver_step_sequence: int jacobian_evaluation_count: int thermofluid_closure_count: int def as_dict(self) -> dict[str, object]: return { "activitySequence": self.activity_sequence, "activityKind": self.activity_kind, "currentTrialTime": self.current_trial_time, "rhsCallCount": self.rhs_call_count, "acceptedStepSequence": self.accepted_step_sequence, "acceptedTime": self.accepted_time, "solverStepSequence": self.solver_step_sequence, "jacobianEvaluationCount": self.jacobian_evaluation_count, "thermofluidClosureCount": self.thermofluid_closure_count, } class SolverActivityTracker: """Single-writer activity telemetry for a solver worker. The solver thread is the only writer and the stream thread only snapshots scalar attributes. The sequence is published last, so a reader never treats partially published fields as a newer completed activity update. The tracker receives aggregate counters from the independent C worker. """ __slots__ = ( "_accepted_step_sequence", "_accepted_time", "_activity_kind", "_activity_sequence", "_current_trial_time", "_jacobian_evaluation_count", "_rhs_call_count", "_solver_step_sequence", "_thermofluid_closure_count", ) def __init__(self) -> None: self._activity_sequence = 0 self._activity_kind = "idle" self._current_trial_time: float | None = None self._rhs_call_count = 0 self._accepted_step_sequence = 0 self._accepted_time: float | None = None self._solver_step_sequence = 0 self._jacobian_evaluation_count = 0 self._thermofluid_closure_count = 0 def _publish(self, kind: str, time: float | None = None) -> None: self._activity_kind = kind if time is not None: self._current_trial_time = float(time) self._activity_sequence += 1 def start_integration(self, time: float) -> None: self._accepted_time = float(time) self._publish("solver_initialization", time) def record_phase(self, kind: str, time: float | None = None) -> None: self._publish(kind, time) def record_solver_step(self, time: float) -> None: self._solver_step_sequence += 1 self._publish("solver_step", time) def record_rhs(self, time: float) -> None: self._rhs_call_count += 1 self._publish("rhs", time) def record_native_progress(self, time: float, rhs_count: int, accepted_count: int) -> None: """Publish aggregate counters from an isolated C worker without per-RHS callbacks.""" self._rhs_call_count = max(self._rhs_call_count, rhs_count) self._accepted_step_sequence = max(self._accepted_step_sequence, accepted_count) self._accepted_time = max(self._accepted_time or time, time) self._publish("native_solver", time) def record_jacobian(self, time: float) -> None: self._jacobian_evaluation_count += 1 self._publish("jacobian", time) def record_thermofluid_closure(self, time: float) -> None: self._thermofluid_closure_count += 1 self._publish("thermofluid_closure", time) def record_accepted_step(self, time: float) -> None: accepted_time = float(time) if ( self._accepted_time is not None and accepted_time <= self._accepted_time ): return self._accepted_step_sequence += 1 self._accepted_time = accepted_time self._publish("accepted_step", accepted_time) def snapshot(self) -> SolverActivitySnapshot: # ``activity_sequence`` is read last because writers publish it last. activity_kind = self._activity_kind current_trial_time = self._current_trial_time rhs_call_count = self._rhs_call_count accepted_step_sequence = self._accepted_step_sequence accepted_time = self._accepted_time solver_step_sequence = self._solver_step_sequence jacobian_evaluation_count = self._jacobian_evaluation_count thermofluid_closure_count = self._thermofluid_closure_count activity_sequence = self._activity_sequence return SolverActivitySnapshot( activity_sequence=activity_sequence, activity_kind=activity_kind, current_trial_time=current_trial_time, rhs_call_count=rhs_call_count, accepted_step_sequence=accepted_step_sequence, accepted_time=accepted_time, solver_step_sequence=solver_step_sequence, jacobian_evaluation_count=jacobian_evaluation_count, thermofluid_closure_count=thermofluid_closure_count, ) @dataclass(frozen=True) class SolveIVPConfig: t_start: float = 0.0 t_stop: float = 20.0 method: str = "BDF" rtol: float = 1e-6 atol: float | Sequence[float] = 1e-8 max_step: float = 1e-3 first_step: float | None = None