Replace Python numerical kernels with native C execution

This commit is contained in:
ljz committed 2026-09-10 01:12:18 +08:00
1 parent 48da6be21c
commit 3b38f73fe0
227 files changed
+16801 -75499

No files matched your search

+90
View File
@@ -0,0 +1,90 @@
"""CLI input adapter; XML stays the numerical backend's execution contract."""
from __future__ import annotations
import ast
from copy import deepcopy
import json
from math import isfinite
import operator
from pathlib import Path
from app.system_xml import validate_system_xml_document
# Matches the editor's displayed-unit conversions. Stored numeric values are
# already SI; only arithmetic expressions are evaluated in the selected unit.
_SCALES = {
"area": {"m2": 1, "cm2": 1e-4, "mm2": 1e-6},
"length": {"m": 1, "cm": .01, "mm": .001},
"pressure": {"Pa": 1, "kPa": 1e3, "MPa": 1e6, "bar": 1e5},
"volume": {"m3": 1, "L": .001, "mL": 1e-6},
}
_OPERATIONS = {ast.Add: operator.add, ast.Sub: operator.sub, ast.Mult: operator.mul,
ast.Div: operator.truediv, ast.Pow: operator.pow}
def arithmetic_value(source: str) -> float:
"""Bounded arithmetic only: never evaluate code, names or function calls."""
source = source.strip().removeprefix("=").strip()
if len(source) > 512:
raise ValueError("Parameter expression exceeds 512 characters.")
tree = ast.parse(source, mode="eval")
if sum(1 for _ in ast.walk(tree)) > 256:
raise ValueError("Parameter expression is too complex.")
def visit(node, depth=0):
if depth > 32:
raise ValueError("Parameter expression is nested too deeply.")
if isinstance(node, ast.Constant) and type(node.value) in (int, float):
result = float(node.value)
elif isinstance(node, ast.UnaryOp) and type(node.op) in (ast.UAdd, ast.USub):
result = visit(node.operand, depth+1) * (-1 if isinstance(node.op, ast.USub) else 1)
elif isinstance(node, ast.BinOp) and type(node.op) in _OPERATIONS:
a, b = visit(node.left, depth+1), visit(node.right, depth+1)
if isinstance(node.op, ast.Pow) and abs(b) > 16:
raise ValueError("CLI parameter exponents must be between -16 and 16.")
result = _OPERATIONS[type(node.op)](a, b)
else:
raise ValueError("CLI supports arithmetic expressions only; export complex expressions as XML from the editor.")
if not isinstance(result, (int, float)) or not isfinite(result):
raise ValueError("Parameter expression must produce a finite real number.")
return result
return float(visit(tree.body))
def project_xml(data: dict) -> bytes:
from app.main import ReactFlowProjectPayload, build_reactflow_system_xml
from app.simulation.registry import get_component_model_spec
normalized = deepcopy(data)
for node in normalized.get("nodes", []):
model = node["data"]
spec = get_component_model_spec(model["modelType"])
for name, value in list(model.get("parameters", {}).items()):
if not isinstance(value, str):
continue
try:
number = float(value)
except ValueError:
definition = spec.parameter_by_name[name]
if definition.editor:
raise ValueError(f"{node['id']}.{name}: a discrete parameter cannot be an expression.")
number = arithmetic_value(value)
unit = model.get("parameterUnits", {}).get(name, definition.unit)
if definition.quantity in _SCALES:
if unit not in _SCALES[definition.quantity]:
raise ValueError(f"Unsupported parameter unit: {unit}.")
number *= _SCALES[definition.quantity][unit]
elif definition.quantity == "temperature" and unit == "degC":
number += 273.15
elif unit != definition.unit:
raise ValueError(f"Unsupported parameter unit: {unit}.")
if not isfinite(number):
raise ValueError(f"{node['id']}.{name}: parameter must be finite.")
model["parameters"][name] = number
return build_reactflow_system_xml(ReactFlowProjectPayload.model_validate(normalized))
def load_input(path: Path):
xml = project_xml(json.loads(path.read_text(encoding="utf-8"))) if path.suffix.lower() == ".json" else path.read_bytes()
report = validate_system_xml_document(xml)
if not report.valid or report.document is None:
raise ValueError(f"Invalid simulation XML: {report.as_dict()}")
return xml, report.document