整合求解器活动监控与步长回归证据
同步远端 PNL0003 诊断和大采样网格能力,语义合并活动感知的 60 秒真停滞判定与旧后端 15 分钟兼容兜底。 纳管热路径优化、15 单元运行证据、浏览器与 API 报告,并补充北京时间更新日志和遗留问题。
This commit is contained in:
1 parent
c19cf77aee
commit
e18399c022
46 files changed
+181589
-170
No files matched your search
@@ -15,6 +15,135 @@ DenseState = Callable[[float], list[float]]
|
||||
JacobianCallable = Callable[[float, object], object]
|
||||
|
||||
|
||||
@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.
|
||||
Passing no tracker to :func:`integrate_ode` is the zero-cost opt-out path.
|
||||
"""
|
||||
|
||||
__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_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 StateTransition:
|
||||
"""A state reset located inside an accepted integration step."""
|
||||
@@ -773,6 +902,7 @@ def _integrate_scipy_stepwise(
|
||||
state_transition_handler: StateTransitionHandler | None = None,
|
||||
jac_sparsity=None,
|
||||
jac: JacobianCallable | None = None,
|
||||
activity_tracker: SolverActivityTracker | None = None,
|
||||
) -> ODESolution:
|
||||
"""Initial stepwise integration path for breakpoints and state resets.
|
||||
|
||||
@@ -796,6 +926,18 @@ def _integrate_scipy_stepwise(
|
||||
if solver_type is None:
|
||||
raise ValueError(f"Unsupported integration method: {config.method}")
|
||||
implicit_jac = jac if config.method in {"BDF", "Radau"} else None
|
||||
solver_jac = implicit_jac
|
||||
if implicit_jac is not None and activity_tracker is not None:
|
||||
original_jacobian = implicit_jac
|
||||
|
||||
def activity_jacobian(time, state):
|
||||
activity_tracker.record_jacobian(float(time))
|
||||
try:
|
||||
return original_jacobian(time, state)
|
||||
finally:
|
||||
activity_tracker.record_phase("solver_step", float(time))
|
||||
|
||||
solver_jac = activity_jacobian
|
||||
|
||||
times = [float(config.t_start)]
|
||||
states = [[float(value)] for value in initial_state]
|
||||
@@ -886,8 +1028,8 @@ def _integrate_scipy_stepwise(
|
||||
"max_step": segment_max_step,
|
||||
}
|
||||
if config.method in {"BDF", "Radau"}:
|
||||
if implicit_jac is not None:
|
||||
solver_options["jac"] = implicit_jac
|
||||
if solver_jac is not None:
|
||||
solver_options["jac"] = solver_jac
|
||||
elif jac_sparsity is not None:
|
||||
solver_options["jac_sparsity"] = jac_sparsity
|
||||
requested_first_step = (
|
||||
@@ -909,6 +1051,11 @@ def _integrate_scipy_stepwise(
|
||||
start_segment = getattr(implicit_jac, "start_segment", None)
|
||||
if start_segment is not None:
|
||||
start_segment()
|
||||
if activity_tracker is not None:
|
||||
activity_tracker.record_phase(
|
||||
"solver_initialization",
|
||||
last_accepted_time,
|
||||
)
|
||||
solver = solver_type(
|
||||
cancellable_rhs,
|
||||
last_accepted_time,
|
||||
@@ -980,6 +1127,10 @@ def _integrate_scipy_stepwise(
|
||||
integration_end - last_accepted_time
|
||||
),
|
||||
)
|
||||
if activity_tracker is not None:
|
||||
activity_tracker.record_solver_step(
|
||||
last_accepted_time
|
||||
)
|
||||
step_message = solver.step()
|
||||
except IntegrationCancelled:
|
||||
status = "cancelled"
|
||||
@@ -1365,6 +1516,7 @@ def integrate_ode(
|
||||
jac_sparsity=None,
|
||||
jac: JacobianCallable | None = None,
|
||||
recoverable_trial_retries: bool = False,
|
||||
activity_tracker: SolverActivityTracker | None = None,
|
||||
):
|
||||
"""Integrate an ODE, optionally restarting at equation discontinuities.
|
||||
|
||||
@@ -1381,8 +1533,34 @@ def integrate_ode(
|
||||
into the stepwise path so a ``RecoverableTrialStateError`` can rebuild the
|
||||
solver from its last accepted state. It defaults to false to preserve the
|
||||
direct ``solve_ivp`` path for ordinary callers.
|
||||
|
||||
``activity_tracker`` is optional and additive. When omitted, the numerical
|
||||
call path and callback behavior are unchanged.
|
||||
"""
|
||||
|
||||
integration_rhs = rhs
|
||||
integration_accepted_step_callback = accepted_step_callback
|
||||
if activity_tracker is not None:
|
||||
activity_tracker.start_integration(config.t_start)
|
||||
original_rhs = rhs
|
||||
|
||||
def activity_rhs(time, state):
|
||||
numeric_time = float(time)
|
||||
activity_tracker.record_rhs(numeric_time)
|
||||
try:
|
||||
return original_rhs(time, state)
|
||||
finally:
|
||||
activity_tracker.record_phase("solver_step", numeric_time)
|
||||
|
||||
integration_rhs = activity_rhs
|
||||
|
||||
def activity_accepted_step(time: float) -> None:
|
||||
activity_tracker.record_accepted_step(float(time))
|
||||
if accepted_step_callback is not None:
|
||||
accepted_step_callback(float(time))
|
||||
|
||||
integration_accepted_step_callback = activity_accepted_step
|
||||
|
||||
if (
|
||||
state_transition_handler is not None
|
||||
and config.t_stop < config.t_start
|
||||
@@ -1406,22 +1584,22 @@ def integrate_ode(
|
||||
except ImportError:
|
||||
if normalized_breakpoints:
|
||||
return _runge_kutta_4_segmented(
|
||||
rhs,
|
||||
integration_rhs,
|
||||
initial_state,
|
||||
config,
|
||||
t_eval,
|
||||
normalized_breakpoints,
|
||||
cancel_check,
|
||||
accepted_step_callback,
|
||||
integration_accepted_step_callback,
|
||||
state_transition_handler,
|
||||
)
|
||||
return _runge_kutta_4(
|
||||
rhs,
|
||||
integration_rhs,
|
||||
initial_state,
|
||||
config,
|
||||
t_eval,
|
||||
cancel_check,
|
||||
accepted_step_callback,
|
||||
integration_accepted_step_callback,
|
||||
state_transition_handler,
|
||||
)
|
||||
|
||||
@@ -1432,25 +1610,26 @@ def integrate_ode(
|
||||
or recoverable_trial_retries
|
||||
):
|
||||
return _integrate_scipy_stepwise(
|
||||
rhs,
|
||||
integration_rhs,
|
||||
initial_state,
|
||||
config,
|
||||
t_eval,
|
||||
cancel_check or (lambda: False),
|
||||
accepted_step_callback,
|
||||
integration_accepted_step_callback,
|
||||
normalized_breakpoints,
|
||||
state_transition_handler,
|
||||
jac_sparsity,
|
||||
jac,
|
||||
activity_tracker,
|
||||
)
|
||||
|
||||
implicit_jac = jac if config.method in {"BDF", "Radau"} else None
|
||||
solve_rhs = rhs
|
||||
solve_rhs = integration_rhs
|
||||
if implicit_jac is not None:
|
||||
observer = getattr(implicit_jac, "observe", None)
|
||||
if observer is not None:
|
||||
def observed_rhs(time, state):
|
||||
derivative = rhs(time, state)
|
||||
derivative = integration_rhs(time, state)
|
||||
observer(float(time), state, derivative)
|
||||
return derivative
|
||||
|
||||
@@ -1459,6 +1638,19 @@ def integrate_ode(
|
||||
if start_segment is not None:
|
||||
start_segment()
|
||||
|
||||
solve_jac = implicit_jac
|
||||
if implicit_jac is not None and activity_tracker is not None:
|
||||
original_jacobian = implicit_jac
|
||||
|
||||
def activity_jacobian(time, state):
|
||||
activity_tracker.record_jacobian(float(time))
|
||||
try:
|
||||
return original_jacobian(time, state)
|
||||
finally:
|
||||
activity_tracker.record_phase("solver_step", float(time))
|
||||
|
||||
solve_jac = activity_jacobian
|
||||
|
||||
solve_options = {
|
||||
"fun": solve_rhs,
|
||||
"t_span": (config.t_start, config.t_stop),
|
||||
@@ -1471,8 +1663,11 @@ def integrate_ode(
|
||||
}
|
||||
if config.first_step is not None:
|
||||
solve_options["first_step"] = config.first_step
|
||||
if implicit_jac is not None:
|
||||
solve_options["jac"] = implicit_jac
|
||||
if solve_jac is not None:
|
||||
solve_options["jac"] = solve_jac
|
||||
elif jac_sparsity is not None and config.method in {"BDF", "Radau"}:
|
||||
solve_options["jac_sparsity"] = jac_sparsity
|
||||
return solve_ivp(**solve_options)
|
||||
direct_solution = solve_ivp(**solve_options)
|
||||
if activity_tracker is not None and len(direct_solution.t):
|
||||
activity_tracker.record_accepted_step(float(direct_solution.t[-1]))
|
||||
return direct_solution
|
||||
Reference in new issue
Block a user