整合求解器活动监控与步长回归证据
同步远端 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
@@ -1,5 +1,6 @@
|
||||
import math
|
||||
import sys
|
||||
import threading
|
||||
import types
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
@@ -8,6 +9,7 @@ from app.simulation.core.errors import RecoverableTrialStateError
|
||||
from app.simulation.solvers.solver import (
|
||||
IntegrationCancelled,
|
||||
SolveIVPConfig,
|
||||
SolverActivityTracker,
|
||||
StateTransition,
|
||||
integrate_ode,
|
||||
)
|
||||
@@ -44,6 +46,164 @@ def _counting_fixed_step_solver(
|
||||
|
||||
|
||||
class IntegrateOdeTests(unittest.TestCase):
|
||||
def test_activity_tracking_preserves_the_exact_trajectory(self) -> None:
|
||||
options = {
|
||||
"rhs": lambda time, state: [
|
||||
-0.5 * float(state[0]) + 0.1 * float(time)
|
||||
],
|
||||
"initial_state": [1.0],
|
||||
"config": SolveIVPConfig(
|
||||
t_start=0.0,
|
||||
t_stop=0.1,
|
||||
method="BDF",
|
||||
max_step=0.01,
|
||||
),
|
||||
"t_eval": [0.0, 0.025, 0.05, 0.075, 0.1],
|
||||
"cancel_check": lambda: False,
|
||||
}
|
||||
|
||||
baseline = integrate_ode(**options)
|
||||
tracked = integrate_ode(
|
||||
**options,
|
||||
activity_tracker=SolverActivityTracker(),
|
||||
)
|
||||
|
||||
self.assertTrue(baseline.success, baseline.message)
|
||||
self.assertTrue(tracked.success, tracked.message)
|
||||
self.assertEqual(tracked.t, baseline.t)
|
||||
self.assertEqual(tracked.y, baseline.y)
|
||||
self.assertEqual(tracked.solver_segments, baseline.solver_segments)
|
||||
|
||||
def test_activity_advances_while_accepted_time_is_unchanged(self) -> None:
|
||||
import numpy as np
|
||||
|
||||
rhs_started = threading.Event()
|
||||
release_rhs = threading.Event()
|
||||
tracker = SolverActivityTracker()
|
||||
results = []
|
||||
errors: list[BaseException] = []
|
||||
|
||||
class BlockingSolver:
|
||||
def __init__(self, fun, t0, y0, t_bound, **_kwargs):
|
||||
self.fun = fun
|
||||
self.t = float(t0)
|
||||
self.y = np.asarray(y0, dtype=float)
|
||||
self.t_bound = float(t_bound)
|
||||
self.status = "running"
|
||||
self.nfev = 0
|
||||
self.njev = 0
|
||||
self.nlu = 0
|
||||
|
||||
def step(self):
|
||||
self.fun(0.25, self.y)
|
||||
self.nfev += 1
|
||||
self.t = self.t_bound
|
||||
self.y = np.asarray([self.t], dtype=float)
|
||||
self.status = "finished"
|
||||
return None
|
||||
|
||||
def rhs(_time, _state):
|
||||
rhs_started.set()
|
||||
if not release_rhs.wait(timeout=2.0):
|
||||
raise TimeoutError("test did not release the blocking RHS")
|
||||
return [1.0]
|
||||
|
||||
def run_integration() -> None:
|
||||
try:
|
||||
results.append(
|
||||
integrate_ode(
|
||||
rhs=rhs,
|
||||
initial_state=[0.0],
|
||||
config=SolveIVPConfig(t_stop=1.0, method="BDF"),
|
||||
cancel_check=lambda: False,
|
||||
activity_tracker=tracker,
|
||||
)
|
||||
)
|
||||
except BaseException as exc: # pragma: no cover - test handoff
|
||||
errors.append(exc)
|
||||
|
||||
with patch("scipy.integrate.BDF", BlockingSolver):
|
||||
worker = threading.Thread(target=run_integration)
|
||||
worker.start()
|
||||
self.assertTrue(rhs_started.wait(timeout=2.0))
|
||||
during_rhs = tracker.snapshot()
|
||||
self.assertEqual(during_rhs.activity_kind, "rhs")
|
||||
self.assertGreater(during_rhs.activity_sequence, 0)
|
||||
self.assertEqual(during_rhs.current_trial_time, 0.25)
|
||||
self.assertEqual(during_rhs.rhs_call_count, 1)
|
||||
self.assertEqual(during_rhs.accepted_step_sequence, 0)
|
||||
self.assertEqual(during_rhs.accepted_time, 0.0)
|
||||
release_rhs.set()
|
||||
worker.join(timeout=2.0)
|
||||
|
||||
self.assertFalse(worker.is_alive())
|
||||
self.assertEqual(errors, [])
|
||||
self.assertTrue(results[0].success, results[0].message)
|
||||
completed = tracker.snapshot()
|
||||
self.assertEqual(completed.accepted_step_sequence, 1)
|
||||
self.assertEqual(completed.accepted_time, 1.0)
|
||||
self.assertEqual(completed.solver_step_sequence, 1)
|
||||
|
||||
def test_activity_distinguishes_blocking_jacobian_evaluation(self) -> None:
|
||||
import numpy as np
|
||||
|
||||
jacobian_started = threading.Event()
|
||||
release_jacobian = threading.Event()
|
||||
tracker = SolverActivityTracker()
|
||||
results = []
|
||||
|
||||
class BlockingSolver:
|
||||
def __init__(self, _fun, t0, y0, t_bound, **options):
|
||||
self.jac = options["jac"]
|
||||
self.t = float(t0)
|
||||
self.y = np.asarray(y0, dtype=float)
|
||||
self.t_bound = float(t_bound)
|
||||
self.status = "running"
|
||||
self.nfev = 0
|
||||
self.njev = 0
|
||||
self.nlu = 0
|
||||
|
||||
def step(self):
|
||||
self.jac(0.5, self.y)
|
||||
self.njev += 1
|
||||
self.t = self.t_bound
|
||||
self.status = "finished"
|
||||
return None
|
||||
|
||||
class BlockingJacobian:
|
||||
def __call__(self, _time, _state):
|
||||
jacobian_started.set()
|
||||
if not release_jacobian.wait(timeout=2.0):
|
||||
raise TimeoutError("test did not release the Jacobian")
|
||||
return np.asarray([[0.0]])
|
||||
|
||||
def run_integration() -> None:
|
||||
results.append(
|
||||
integrate_ode(
|
||||
rhs=lambda _time, _state: [0.0],
|
||||
initial_state=[0.0],
|
||||
config=SolveIVPConfig(t_stop=1.0, method="BDF"),
|
||||
cancel_check=lambda: False,
|
||||
jac=BlockingJacobian(),
|
||||
activity_tracker=tracker,
|
||||
)
|
||||
)
|
||||
|
||||
with patch("scipy.integrate.BDF", BlockingSolver):
|
||||
worker = threading.Thread(target=run_integration)
|
||||
worker.start()
|
||||
self.assertTrue(jacobian_started.wait(timeout=2.0))
|
||||
during_jacobian = tracker.snapshot()
|
||||
self.assertEqual(during_jacobian.activity_kind, "jacobian")
|
||||
self.assertEqual(during_jacobian.jacobian_evaluation_count, 1)
|
||||
self.assertEqual(during_jacobian.rhs_call_count, 0)
|
||||
self.assertEqual(during_jacobian.accepted_step_sequence, 0)
|
||||
release_jacobian.set()
|
||||
worker.join(timeout=2.0)
|
||||
|
||||
self.assertFalse(worker.is_alive())
|
||||
self.assertTrue(results[0].success, results[0].message)
|
||||
|
||||
def test_generic_solver_keeps_canonical_default_tolerance(self) -> None:
|
||||
self.assertEqual(SolveIVPConfig().atol, 1.0e-8)
|
||||
|
||||
|
||||
Reference in new issue
Block a user