Replace Python numerical kernels with native C execution
This commit is contained in:
1 parent
48da6be21c
commit
3b38f73fe0
227 files changed
+16801
-75499
No files matched your search
@@ -26,14 +26,11 @@ from app.main import (
|
||||
)
|
||||
from app.simulation.components.experimental.flow.resistive_pipe import ResistivePipe
|
||||
from app.simulation.registry import get_component_model_spec
|
||||
from app.simulation.solvers.solver import (
|
||||
from app.simulation.config import (
|
||||
SolveIVPConfig,
|
||||
SolverActivityTracker,
|
||||
)
|
||||
from app.simulation.systems.generic import (
|
||||
GenericFluidSystem,
|
||||
SimulationPreparationError,
|
||||
)
|
||||
from app.simulation.native_codegen.runner import simulate_native
|
||||
from tests.test_system_xml_protocol import physical_port
|
||||
|
||||
|
||||
@@ -250,6 +247,26 @@ def xml_request(body: bytes) -> Request:
|
||||
|
||||
|
||||
class GenericSystemXmlSimulationTests(unittest.TestCase):
|
||||
def test_native_chain_conserves_mass_energy_and_edge_order(self):
|
||||
results = []
|
||||
for reverse in (False, True):
|
||||
result = simulate_native(compile_reactflow_network(chain_project(reverse_edges=reverse)), SolveIVPConfig(t_stop=.01, max_step=.001), sample_step=.005)
|
||||
self.assertTrue(result.success)
|
||||
self.assertEqual(result.diagnostics['backend'], 'native-c')
|
||||
self.assertLess(result.final['cylinder_1.p'], 500000)
|
||||
self.assertGreater(result.final['tank_1.p'], 100000)
|
||||
for field, tolerance in (('m', 1e-12), ('U', 1e-6)):
|
||||
totals = [a+b for a,b in zip(result.series['cylinder_1.'+field], result.series['tank_1.'+field])]
|
||||
self.assertLess(max(totals)-min(totals), tolerance)
|
||||
results.append(result)
|
||||
self.assertAlmostEqual(results[0].final['tank_1.p'], results[1].final['tank_1.p'], places=7)
|
||||
|
||||
def test_native_branched_topology_conserves_flow(self):
|
||||
result = simulate_native(compile_reactflow_network(branched_project()), SolveIVPConfig(t_stop=.005, max_step=.001), sample_step=.005)
|
||||
self.assertTrue(result.success)
|
||||
self.assertAlmostEqual(result.final['pipe_1.port_a.m_flow'], result.final['pipe_2.port_a.m_flow'], places=10)
|
||||
self.assertGreater(result.final['tank_1.p'], 100000)
|
||||
|
||||
def test_xml_pipe_compiles_to_quasi_steady_resistance(self) -> None:
|
||||
network = compile_reactflow_network(chain_project())
|
||||
|
||||
@@ -259,166 +276,8 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
|
||||
self.assertEqual(structure["equationCount"], 12)
|
||||
self.assertTrue(structure["isSquare"])
|
||||
|
||||
def test_generic_chain_simulation_conserves_mass_and_moves_pressures(self) -> None:
|
||||
network = compile_reactflow_network(chain_project())
|
||||
progress: list[tuple[float, str]] = []
|
||||
activity_tracker = SolverActivityTracker()
|
||||
system = GenericFluidSystem(network)
|
||||
result = system.simulate(
|
||||
SolveIVPConfig(
|
||||
t_start=0.0,
|
||||
t_stop=0.01,
|
||||
method="BDF",
|
||||
max_step=0.001,
|
||||
),
|
||||
sample_step=0.005,
|
||||
progress_callback=lambda value, phase: progress.append((value, phase)),
|
||||
activity_tracker=activity_tracker,
|
||||
)
|
||||
|
||||
self.assertTrue(result.success)
|
||||
self.assertEqual(progress[0], (0.0, "initializing"))
|
||||
self.assertEqual(progress[-1], (1.0, "complete"))
|
||||
self.assertTrue(
|
||||
all(
|
||||
current[0] <= following[0]
|
||||
for current, following in zip(progress, progress[1:])
|
||||
)
|
||||
)
|
||||
self.assertIn("integrating", {phase for _, phase in progress})
|
||||
self.assertIn("postprocessing", {phase for _, phase in progress})
|
||||
activity = activity_tracker.snapshot()
|
||||
self.assertEqual(activity.activity_kind, "complete")
|
||||
self.assertGreater(activity.rhs_call_count, 0)
|
||||
self.assertGreater(activity.accepted_step_sequence, 0)
|
||||
self.assertEqual(activity.accepted_time, 0.01)
|
||||
self.assertGreater(activity.thermofluid_closure_count, 0)
|
||||
self.assertEqual(
|
||||
result.diagnostics["activity"],
|
||||
activity.as_dict(),
|
||||
)
|
||||
self.assertIsNone(system._activity_tracker)
|
||||
self.assertEqual(
|
||||
result.diagnostics["integration"][
|
||||
"mechanicalAbsoluteTolerance"
|
||||
]["mode"],
|
||||
"legacy",
|
||||
)
|
||||
self.assertLess(result.series["cylinder_1.p"][-1], 500000.0)
|
||||
self.assertGreater(result.series["tank_1.p"][-1], 100000.0)
|
||||
total_mass = [
|
||||
cylinder + tank
|
||||
for cylinder, tank in zip(
|
||||
result.series["cylinder_1.m"],
|
||||
result.series["tank_1.m"],
|
||||
)
|
||||
]
|
||||
self.assertLess(max(total_mass) - min(total_mass), 1e-12)
|
||||
total_energy = [
|
||||
cylinder + tank
|
||||
for cylinder, tank in zip(
|
||||
result.series["cylinder_1.U"],
|
||||
result.series["tank_1.U"],
|
||||
)
|
||||
]
|
||||
self.assertLess(max(total_energy) - min(total_energy), 1e-6)
|
||||
pressure_flow = result.diagnostics["pressureFlow"]
|
||||
self.assertLess(pressure_flow["maxScaledResidual"], 1e-7)
|
||||
self.assertEqual(
|
||||
pressure_flow["solveCount"],
|
||||
pressure_flow["seededSolveCount"]
|
||||
+ pressure_flow["nonlinearSolveCount"],
|
||||
)
|
||||
self.assertAlmostEqual(
|
||||
pressure_flow["fastPathHitRate"],
|
||||
pressure_flow["seededSolveCount"] / pressure_flow["solveCount"],
|
||||
)
|
||||
self.assertGreaterEqual(
|
||||
pressure_flow["residualEvaluationCount"],
|
||||
pressure_flow["optimizerEvaluationCount"],
|
||||
)
|
||||
|
||||
def test_activity_tracker_scope_is_restored_after_simulation_error(self) -> None:
|
||||
system = GenericFluidSystem(compile_reactflow_network(chain_project()))
|
||||
previous_tracker = SolverActivityTracker()
|
||||
active_tracker = SolverActivityTracker()
|
||||
system._activity_tracker = previous_tracker
|
||||
|
||||
with (
|
||||
patch.object(
|
||||
system,
|
||||
"_simulate",
|
||||
side_effect=RuntimeError("synthetic simulation failure"),
|
||||
),
|
||||
self.assertRaisesRegex(RuntimeError, "synthetic simulation failure"),
|
||||
):
|
||||
system.simulate(
|
||||
SolveIVPConfig(
|
||||
t_start=0.0,
|
||||
t_stop=0.01,
|
||||
method="BDF",
|
||||
max_step=0.001,
|
||||
),
|
||||
sample_step=0.005,
|
||||
activity_tracker=active_tracker,
|
||||
)
|
||||
|
||||
self.assertIs(system._activity_tracker, previous_tracker)
|
||||
|
||||
def test_cancelled_simulation_returns_accepted_partial_samples(self) -> None:
|
||||
cancel_event = threading.Event()
|
||||
|
||||
def request_cancel_after_progress(progress: float, phase: str) -> None:
|
||||
if phase == "integrating" and progress >= 0.2:
|
||||
cancel_event.set()
|
||||
|
||||
with patch.dict(
|
||||
os.environ,
|
||||
{"SIMULATION_ODE_JACOBIAN_MODE": "optimized"},
|
||||
):
|
||||
result = GenericFluidSystem(
|
||||
compile_reactflow_network(chain_project())
|
||||
).simulate(
|
||||
SolveIVPConfig(t_stop=0.05, method="BDF", max_step=0.001),
|
||||
sample_step=0.005,
|
||||
progress_callback=request_cancel_after_progress,
|
||||
cancel_check=cancel_event.is_set,
|
||||
)
|
||||
|
||||
self.assertFalse(result.success)
|
||||
self.assertEqual(result.status, "cancelled")
|
||||
self.assertGreaterEqual(result.diagnostics["sampleCount"], 2)
|
||||
integration = result.diagnostics["integration"]
|
||||
self.assertEqual(integration["method"], "BDF")
|
||||
self.assertEqual(integration["segmentCount"], len(integration["segments"]))
|
||||
self.assertEqual(
|
||||
integration["totals"]["nfev"],
|
||||
sum(segment["nfev"] for segment in integration["segments"]),
|
||||
)
|
||||
sparsity = integration["jacobianSparsity"]
|
||||
self.assertGreater(sparsity["nonzeroCount"], 0)
|
||||
self.assertGreater(sparsity["colorGroupCount"], 0)
|
||||
self.assertEqual(
|
||||
integration["jacobian"]["mode"],
|
||||
"scipySparseFiniteDifference",
|
||||
)
|
||||
self.assertEqual(
|
||||
integration["jacobian"]["fallbackReason"],
|
||||
"denseStateDependencyPattern",
|
||||
)
|
||||
self.assertEqual(
|
||||
integration["totals"]["finiteDifferenceRhsEstimate"],
|
||||
sum(
|
||||
segment["finiteDifferenceRhsEstimate"]
|
||||
for segment in integration["segments"]
|
||||
),
|
||||
)
|
||||
self.assertGreater(result.simulated_until, 0.0)
|
||||
self.assertLess(result.simulated_until, 0.05)
|
||||
self.assertEqual(
|
||||
len(result.series["time"]),
|
||||
len(result.series["cylinder_1.p"]),
|
||||
)
|
||||
|
||||
def test_task_registry_distinguishes_user_stop_and_stalled_stop(self) -> None:
|
||||
for reason, expected_status in (("user", "stopped"), ("stalled", "stalled")):
|
||||
@@ -446,63 +305,8 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
|
||||
self.assertEqual(snapshot["cancelReason"], reason)
|
||||
self.assertEqual(snapshot["result"]["series"]["time"], [0.0, 0.1])
|
||||
|
||||
def test_physical_edge_order_does_not_change_simulation(self) -> None:
|
||||
forward = GenericFluidSystem(
|
||||
compile_reactflow_network(chain_project())
|
||||
).simulate(
|
||||
SolveIVPConfig(t_stop=0.005, method="BDF", max_step=0.001),
|
||||
sample_step=0.005,
|
||||
)
|
||||
reverse = GenericFluidSystem(
|
||||
compile_reactflow_network(chain_project(reverse_edges=True))
|
||||
).simulate(
|
||||
SolveIVPConfig(t_stop=0.005, method="BDF", max_step=0.001),
|
||||
sample_step=0.005,
|
||||
)
|
||||
|
||||
self.assertAlmostEqual(
|
||||
forward.final["tank_1.p"],
|
||||
reverse.final["tank_1.p"],
|
||||
places=7,
|
||||
)
|
||||
|
||||
def test_branched_topology_is_solved_without_fixed_testmodel_closure(self) -> None:
|
||||
network = compile_reactflow_network(branched_project())
|
||||
result = GenericFluidSystem(network).simulate(
|
||||
SolveIVPConfig(t_stop=0.005, method="BDF", max_step=0.001),
|
||||
sample_step=0.005,
|
||||
)
|
||||
|
||||
self.assertTrue(result.success)
|
||||
self.assertAlmostEqual(
|
||||
result.final["pipe_1.port_a.m_flow"],
|
||||
result.final["pipe_2.port_a.m_flow"],
|
||||
places=10,
|
||||
)
|
||||
self.assertGreater(result.final["tank_1.p"], 100000.0)
|
||||
|
||||
def test_directly_coupled_storage_components_are_rejected(self) -> None:
|
||||
project = chain_project()
|
||||
project.nodes = [project.nodes[0], project.nodes[-1]]
|
||||
project.edges = [
|
||||
ReactFlowEdgePayload(
|
||||
**physical_edge(
|
||||
"edge-1",
|
||||
"cylinder_1",
|
||||
"port_b",
|
||||
"tank_1",
|
||||
"port_a",
|
||||
)
|
||||
)
|
||||
]
|
||||
|
||||
with self.assertRaises(SimulationPreparationError) as caught:
|
||||
GenericFluidSystem(compile_reactflow_network(project))
|
||||
|
||||
self.assertIn(
|
||||
"IDEAL_STORAGE_COUPLING_UNSUPPORTED",
|
||||
{issue.code for issue in caught.exception.issues},
|
||||
)
|
||||
|
||||
def test_raw_system_xml_runs_through_generic_simulation_endpoint(self) -> None:
|
||||
xml = build_reactflow_system_xml(chain_project())
|
||||
@@ -582,7 +386,7 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
|
||||
project = branched_project()
|
||||
project.simulation.t_stop = 2.0
|
||||
project.simulation.step = 0.02
|
||||
project.simulation.max_step = 0.01
|
||||
project.simulation.max_step = 0.000001
|
||||
xml = build_reactflow_system_xml(project)
|
||||
simulation_id = f"test-{uuid4().hex}"
|
||||
task = _register_simulation_task(simulation_id)
|
||||
@@ -704,7 +508,7 @@ class GenericSystemXmlSimulationTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(caught.exception.status_code, 422)
|
||||
self.assertIn(
|
||||
"IDEAL_STORAGE_COUPLING_UNSUPPORTED",
|
||||
"SIMULATION_EXECUTION_FAILED",
|
||||
{
|
||||
issue["code"]
|
||||
for issue in caught.exception.detail["issues"]
|
||||
|
||||
Reference in new issue
Block a user