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
+22
-216
@@ -23,8 +23,7 @@ from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
|
||||
from app.simulation.performance import performance_span, profile_phase, profile_run
|
||||
from app.simulation.property_cache import property_cache_run
|
||||
from app.simulation.solvers.solver import SolverActivityTracker
|
||||
from app.simulation.config import SolverActivityTracker
|
||||
from app.system_xml import (
|
||||
SystemXmlDocument,
|
||||
SystemXmlValidationReport,
|
||||
@@ -41,8 +40,15 @@ if TYPE_CHECKING:
|
||||
|
||||
@asynccontextmanager
|
||||
async def _app_lifespan(application: FastAPI) -> AsyncIterator[None]:
|
||||
import logging
|
||||
|
||||
from app.simulation.backends import numeric_engine_name
|
||||
from app.simulation.warmup import warm_up_simulation_runtime
|
||||
|
||||
application.state.simulation_numeric_engine = numeric_engine_name()
|
||||
logging.getLogger("uvicorn.error").info(
|
||||
"Simulation numeric engine: %s", application.state.simulation_numeric_engine,
|
||||
)
|
||||
application.state.simulation_warmup = warm_up_simulation_runtime().as_dict()
|
||||
yield
|
||||
|
||||
@@ -688,26 +694,15 @@ def run_system_xml_simulation(
|
||||
cancel_check: Callable[[], bool] | None = None,
|
||||
activity_tracker: SolverActivityTracker | None = None,
|
||||
) -> dict[str, object]:
|
||||
with property_cache_run() as property_cache:
|
||||
with profile_run() as trace:
|
||||
result = _run_system_xml_simulation_profiled(
|
||||
xml_bytes,
|
||||
progress_callback,
|
||||
cancel_check,
|
||||
activity_tracker,
|
||||
)
|
||||
with profile_run() as trace:
|
||||
result = _run_system_xml_simulation_profiled(
|
||||
xml_bytes,
|
||||
progress_callback,
|
||||
cancel_check,
|
||||
activity_tracker,
|
||||
)
|
||||
|
||||
performance = trace.snapshot()
|
||||
if performance.get("mode") == "audit" and property_cache is not None:
|
||||
cache_info = property_cache.info()
|
||||
performance["propertyCache"] = {
|
||||
"hits": cache_info.hits,
|
||||
"misses": cache_info.misses,
|
||||
"maxEntriesPerCache": cache_info.max_entries_per_cache,
|
||||
"cacheCount": cache_info.cache_count,
|
||||
"currentEntries": cache_info.current_entries,
|
||||
"evictions": cache_info.evictions,
|
||||
}
|
||||
if performance.get("mode") != "off":
|
||||
diagnostics = dict(result.get("diagnostics", {}))
|
||||
diagnostics["performance"] = performance
|
||||
@@ -721,13 +716,8 @@ def _run_system_xml_simulation_profiled(
|
||||
cancel_check: Callable[[], bool] | None = None,
|
||||
activity_tracker: SolverActivityTracker | None = None,
|
||||
) -> dict[str, object]:
|
||||
from app.simulation.solvers.algebraic import AlgebraicSolveError
|
||||
from app.simulation.solvers.solver import SolveIVPConfig
|
||||
from app.simulation.solvers.stream import StreamSolveError
|
||||
from app.simulation.systems.generic import (
|
||||
GenericFluidSystem,
|
||||
SimulationPreparationError,
|
||||
)
|
||||
from app.simulation.backends import simulate_network
|
||||
from app.simulation.results import SimulationPreparationError
|
||||
|
||||
def emit(
|
||||
progress: int,
|
||||
@@ -766,20 +756,9 @@ def _run_system_xml_simulation_profiled(
|
||||
)
|
||||
|
||||
try:
|
||||
system = GenericFluidSystem(network)
|
||||
result = system.simulate(
|
||||
SolveIVPConfig(
|
||||
t_start=document.simulation.t_start,
|
||||
t_stop=document.simulation.t_stop,
|
||||
method=document.simulation.method,
|
||||
# The pressure-flow closure is solved to a scaled 1e-7
|
||||
# residual. State-specific mechanical absolute tolerances now
|
||||
# keep ideal-stop Jacobian perturbations stable, so the outer
|
||||
# integrator can use its canonical 1e-6 relative accuracy.
|
||||
rtol=1.0e-6,
|
||||
max_step=document.simulation.max_step,
|
||||
),
|
||||
sample_step=document.simulation.sample_step,
|
||||
result = simulate_network(
|
||||
network,
|
||||
document.simulation,
|
||||
progress_callback=report_system_progress,
|
||||
cancel_check=cancel_check,
|
||||
activity_tracker=activity_tracker,
|
||||
@@ -799,41 +778,6 @@ def _run_system_xml_simulation_profiled(
|
||||
],
|
||||
},
|
||||
) from exc
|
||||
except AlgebraicSolveError as exc:
|
||||
algebraic_diagnostics = exc.diagnostics.as_dict()
|
||||
algebraic_diagnostics["scopeKind"] = exc.scope_kind
|
||||
algebraic_diagnostics["scopeComponents"] = list(exc.scope_components)
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail={
|
||||
"message": str(exc),
|
||||
"issues": [
|
||||
{
|
||||
"severity": "error",
|
||||
"layer": "simulation",
|
||||
"code": "PRESSURE_FLOW_SOLVE_FAILED",
|
||||
"message": str(exc),
|
||||
}
|
||||
],
|
||||
"diagnostics": algebraic_diagnostics,
|
||||
},
|
||||
) from exc
|
||||
except StreamSolveError as exc:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
detail={
|
||||
"message": str(exc),
|
||||
"issues": [
|
||||
{
|
||||
"severity": "error",
|
||||
"layer": "simulation",
|
||||
"code": "STREAM_SOLVE_FAILED",
|
||||
"message": str(exc),
|
||||
}
|
||||
],
|
||||
"diagnostics": exc.diagnostics.as_dict(),
|
||||
},
|
||||
) from exc
|
||||
except (RuntimeError, ValueError) as exc:
|
||||
raise HTTPException(
|
||||
status_code=422,
|
||||
@@ -1524,143 +1468,15 @@ def sanitize_project_id(project_id: str) -> str:
|
||||
|
||||
|
||||
def run_reactflow_testmodel(project: ReactFlowProjectPayload) -> dict[str, object]:
|
||||
from app.simulation.examples.testmodel.run import (
|
||||
TestModelExecutionConfig,
|
||||
TestModelRunConfig,
|
||||
TestModelSamplingConfig,
|
||||
run_testmodel,
|
||||
)
|
||||
from app.simulation.examples.testmodel.system import (
|
||||
BranchConfig,
|
||||
CylinderConfig,
|
||||
OrificeConfig,
|
||||
PipeConfig,
|
||||
TankConfig,
|
||||
TestModelConfig,
|
||||
)
|
||||
from app.simulation.solvers.solver import SolveIVPConfig
|
||||
|
||||
validate_reactflow_execution_contract(project)
|
||||
|
||||
nodes_by_type: dict[str, list[ReactFlowNodePayload]] = {}
|
||||
for node in project.nodes:
|
||||
nodes_by_type.setdefault(node.data.modelType, []).append(node)
|
||||
|
||||
cylinder = first_node(nodes_by_type, "cylinder")
|
||||
tank = first_node(nodes_by_type, "tank")
|
||||
orifices = nodes_by_type.get("orifice", [])
|
||||
pipes = nodes_by_type.get("pipe", [])
|
||||
|
||||
model_config = TestModelConfig(
|
||||
cylinder=CylinderConfig(
|
||||
volume=parameter_float(cylinder, "volume", 0.01),
|
||||
p0=parameter_float(cylinder, "p0", 35e6),
|
||||
T0=parameter_float(cylinder, "T0", 300.0),
|
||||
),
|
||||
upper_branch=BranchConfig(
|
||||
orifice=OrificeConfig(K=parameter_float(node_at(orifices, 0), "K", 1e-5)),
|
||||
pipe=pipe_config_from_node(node_at(pipes, 0), PipeConfig),
|
||||
),
|
||||
lower_branch=BranchConfig(
|
||||
orifice=OrificeConfig(K=parameter_float(node_at(orifices, 1), "K", 1e-5)),
|
||||
pipe=pipe_config_from_node(node_at(pipes, 1), PipeConfig),
|
||||
),
|
||||
tank=TankConfig(
|
||||
volume=parameter_float(tank, "volume", 0.1),
|
||||
p0=parameter_float(tank, "p0", 1e5),
|
||||
T0=parameter_float(tank, "T0", 300.0),
|
||||
),
|
||||
)
|
||||
run_config = TestModelRunConfig(
|
||||
model=model_config,
|
||||
solver=SolveIVPConfig(
|
||||
t_start=project.simulation.t_start,
|
||||
t_stop=project.simulation.t_stop,
|
||||
method=project.simulation.method,
|
||||
max_step=project.simulation.max_step,
|
||||
),
|
||||
sampling=TestModelSamplingConfig(step=project.simulation.step),
|
||||
execution=TestModelExecutionConfig(use_modelica_reference_if_available=False),
|
||||
)
|
||||
result = run_testmodel(run_config=run_config)
|
||||
series_keys = ("time", "mytank.p", "mytank.T", "mycylinder.p", "mycylinder.T")
|
||||
series = {
|
||||
key: [float(value) for value in result.series[key]]
|
||||
for key in series_keys
|
||||
if key in result.series
|
||||
}
|
||||
|
||||
return {
|
||||
"success": bool(result.solution.success),
|
||||
"message": str(result.solution.message),
|
||||
"usedModelicaReference": result.used_modelica_reference,
|
||||
"final": {
|
||||
"time": series["time"][-1],
|
||||
"tankPressure": series["mytank.p"][-1],
|
||||
"tankTemperature": series["mytank.T"][-1],
|
||||
"cylinderPressure": series["mycylinder.p"][-1],
|
||||
"cylinderTemperature": series["mycylinder.T"][-1],
|
||||
},
|
||||
"series": series,
|
||||
"artifacts": {
|
||||
"primaryCsv": str(result.artifacts.primary_csv_path),
|
||||
"temperatureCsv": str(result.artifacts.temperature_csv_path),
|
||||
"temperatureSvg": str(result.artifacts.temperature_svg_path),
|
||||
"runReport": str(result.artifacts.run_report_path),
|
||||
},
|
||||
"networkSummary": result.system.network.summary(),
|
||||
}
|
||||
raise HTTPException(status_code=410, detail="The legacy Python example runner has been removed. Export System XML and use /api/system-xml/simulate.")
|
||||
|
||||
|
||||
def run_reactflow_test_mql(project: ReactFlowProjectPayload) -> dict[str, object]:
|
||||
from app.simulation.examples.test_mql.run import run_test_mql
|
||||
from app.simulation.examples.test_mql.system import TestMqlRunConfig
|
||||
|
||||
validate_reactflow_execution_contract(project)
|
||||
|
||||
run_config = TestMqlRunConfig(
|
||||
t_start=project.simulation.t_start,
|
||||
t_stop=project.simulation.t_stop,
|
||||
sample_step=project.simulation.step,
|
||||
)
|
||||
result = run_test_mql(run_config=run_config)
|
||||
series = {
|
||||
key: [float(value) for value in values]
|
||||
for key, values in result.result.series.items()
|
||||
}
|
||||
final = {key: values[-1] for key, values in series.items() if values}
|
||||
snapshot = result.system.snapshot()
|
||||
return {
|
||||
"success": True,
|
||||
"message": "test_mql fixed-topology simulation completed.",
|
||||
"model": {
|
||||
"name": snapshot.model_name,
|
||||
"componentCount": snapshot.component_count,
|
||||
"connectionCount": snapshot.connection_count,
|
||||
"continuousStateCount": snapshot.continuous_state_count,
|
||||
"discreteStateCount": snapshot.discrete_state_count,
|
||||
},
|
||||
"final": final,
|
||||
"series": series,
|
||||
"artifacts": {
|
||||
"summary": str(result.summary_path),
|
||||
},
|
||||
}
|
||||
raise HTTPException(status_code=410, detail="The legacy Python example runner has been removed. Export System XML and use /api/system-xml/simulate.")
|
||||
|
||||
|
||||
def first_node(
|
||||
nodes_by_type: dict[str, list[ReactFlowNodePayload]],
|
||||
model_type: str,
|
||||
) -> ReactFlowNodePayload | None:
|
||||
nodes = nodes_by_type.get(model_type, [])
|
||||
return nodes[0] if nodes else None
|
||||
|
||||
|
||||
def node_at(
|
||||
nodes: list[ReactFlowNodePayload],
|
||||
index: int,
|
||||
) -> ReactFlowNodePayload | None:
|
||||
return nodes[index] if index < len(nodes) else None
|
||||
|
||||
|
||||
def parameter_float(
|
||||
@@ -1675,13 +1491,3 @@ def parameter_float(
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError(f"Parameter '{name}' on component '{node.id}' must be numeric.")
|
||||
|
||||
|
||||
def pipe_config_from_node(node: ReactFlowNodePayload | None, pipe_config_type):
|
||||
return pipe_config_type(
|
||||
length=parameter_float(node, "length", 5.0),
|
||||
diameter=parameter_float(node, "diameter", 0.02),
|
||||
lambda_darcy=parameter_float(node, "lambda_darcy", 0.02),
|
||||
p0=parameter_float(node, "p0", 1e5),
|
||||
T0=parameter_float(node, "T0", 300.0),
|
||||
)
|
||||
Reference in new issue
Block a user