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
@@ -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
|
||||
Reference in new issue
Block a user