旧版前端工程文件导入时版本对比查验、审阅与仿真时部分阻挡功能实现;前端参数输入格式统一规范
This commit is contained in:
1 parent
22579e51c9
commit
44b6ea74ab
32 files changed
+2087
-322
No files matched your search
+42
-16
@@ -20,7 +20,9 @@ from xml.etree import ElementTree as ET
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request, Response
|
||||
from fastapi.responses import FileResponse, HTMLResponse, StreamingResponse
|
||||
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
||||
from pydantic import BaseModel, ConfigDict, Field, StrictFloat, StrictStr, ValidationError
|
||||
|
||||
from app.project_parameters import prepare_project, version_warning
|
||||
|
||||
from app.simulation.performance import performance_span, profile_phase, profile_run
|
||||
from app.simulation.native_codegen.transport import NativeSeriesJson, serialize_result_parts
|
||||
@@ -147,8 +149,8 @@ class ReactFlowNodeData(BaseModel):
|
||||
componentType: str = "component"
|
||||
modelType: str = "component"
|
||||
# Optional at the storage boundary so an incompatible project can still be
|
||||
# opened and inspected. Every execution path requires an exact registry
|
||||
# match before defaults, equations, or ports are consumed.
|
||||
# opened and inspected. Input adapters warn and select the current model;
|
||||
# the normalized XML/numerical boundary still enforces an exact match.
|
||||
modelVersion: str | None = None
|
||||
ports: list[ReactFlowPortDefinition] = Field(default_factory=list)
|
||||
parameters: dict[str, Any] = Field(default_factory=dict)
|
||||
@@ -185,17 +187,17 @@ class ReactFlowEdgePayload(BaseModel):
|
||||
|
||||
|
||||
class ReactFlowSimulationConfig(BaseModel):
|
||||
t_start: float = 0.0
|
||||
t_stop: float = 2.0
|
||||
step: float = 0.1
|
||||
max_step: float = 0.005
|
||||
t_start: StrictFloat | StrictStr = 0.0
|
||||
t_stop: StrictFloat | StrictStr = 2.0
|
||||
step: StrictFloat | StrictStr = 0.1
|
||||
max_step: StrictFloat | StrictStr = 0.005
|
||||
method: str = "BDF"
|
||||
|
||||
|
||||
class ReactFlowProjectPayload(BaseModel):
|
||||
model_config = ConfigDict(extra="forbid")
|
||||
|
||||
projectSchemaVersion: Literal[1]
|
||||
projectSchemaVersion: Literal[1, 2]
|
||||
name: str = "untitled"
|
||||
nodes: list[ReactFlowNodePayload] = Field(default_factory=list)
|
||||
edges: list[ReactFlowEdgePayload] = Field(default_factory=list)
|
||||
@@ -339,10 +341,19 @@ def get_component_catalog() -> dict[str, object]:
|
||||
@app.post("/api/reactflow/system-xml")
|
||||
def export_reactflow_system_xml(payload: ReactFlowProjectPayload) -> Response:
|
||||
try:
|
||||
xml = build_reactflow_system_xml(payload)
|
||||
normalized, notices = prepare_project(payload)
|
||||
xml = build_reactflow_system_xml(normalized)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return Response(content=xml, media_type="application/xml")
|
||||
headers = {}
|
||||
if notices:
|
||||
warning = {**version_warning(notices), "totalCount": len(notices), "components": notices[:10]}
|
||||
encoded = quote(json.dumps(warning, ensure_ascii=False))
|
||||
while len(encoded) > 3800 and warning["components"]:
|
||||
warning["components"] = warning["components"][:-1]
|
||||
encoded = quote(json.dumps(warning, ensure_ascii=False))
|
||||
headers["X-Component-Version-Warnings"] = encoded
|
||||
return Response(content=xml, media_type="application/xml", headers=headers)
|
||||
|
||||
|
||||
@app.post("/api/simulation-results/csv")
|
||||
@@ -510,10 +521,11 @@ def simulate_reactflow_test_mql(payload: ReactFlowProjectPayload) -> dict[str, o
|
||||
@app.post("/api/reactflow/compile-model")
|
||||
def compile_reactflow_model(payload: ReactFlowProjectPayload) -> dict[str, object]:
|
||||
try:
|
||||
network = compile_reactflow_network(payload)
|
||||
normalized, notices = prepare_project(payload)
|
||||
network = compile_reactflow_network(normalized)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
return {"success": True, **network.as_interface_dict()}
|
||||
return {"success": True, **network.as_interface_dict(), "warnings": [version_warning(notices)] if notices else []}
|
||||
|
||||
|
||||
@app.post("/api/system-xml/validate")
|
||||
@@ -1025,7 +1037,7 @@ def validate_reactflow_component_contract(
|
||||
node: ReactFlowNodePayload,
|
||||
component_spec: "ComponentModelSpec",
|
||||
) -> dict[str, float]:
|
||||
"""Validate the persisted model contract before consuming current defaults."""
|
||||
"""Validate the normalized SI model contract before consuming current defaults."""
|
||||
|
||||
if (
|
||||
node.data.componentType != node.data.modelType
|
||||
@@ -1094,6 +1106,7 @@ def validate_reactflow_execution_contract(
|
||||
def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
|
||||
from app.simulation.registry import get_component_model_spec
|
||||
|
||||
validate_si_project(project)
|
||||
system_attributes = {
|
||||
"schemaVersion": SYSTEM_XML_SCHEMA_VERSION,
|
||||
"unitSystem": SYSTEM_XML_UNIT_SYSTEM,
|
||||
@@ -1300,6 +1313,7 @@ def _solver_model_from_reactflow(
|
||||
) -> SolverModelInput:
|
||||
from app.simulation.registry import get_component_model_spec
|
||||
|
||||
validate_si_project(project)
|
||||
components: list[SolverComponentInput] = []
|
||||
for node in project.nodes:
|
||||
spec = get_component_model_spec(node.data.modelType)
|
||||
@@ -1517,6 +1531,18 @@ def parameter_float(
|
||||
return default
|
||||
value = node.data.parameters.get(name, default)
|
||||
try:
|
||||
return float(value)
|
||||
except (TypeError, ValueError):
|
||||
raise ValueError(f"Parameter '{name}' on component '{node.id}' must be numeric.")
|
||||
if type(value) in (int, float) and isfinite(value):
|
||||
return float(value)
|
||||
except OverflowError:
|
||||
pass
|
||||
raise ValueError(f"Parameter '{name}' on component '{node.id}' must be a finite SI number; normalize expressions before execution.")
|
||||
|
||||
|
||||
def validate_si_project(project: ReactFlowProjectPayload) -> None:
|
||||
"""Guard the editor-independent numerical boundary, including time settings."""
|
||||
if project.projectSchemaVersion != 1:
|
||||
raise ValueError("Execution requires normalized SI data; preprocess project input first.")
|
||||
for name in ("t_start", "t_stop", "step", "max_step"):
|
||||
value = getattr(project.simulation, name)
|
||||
if type(value) not in (int, float) or not isfinite(value):
|
||||
raise ValueError(f"Simulation {name} must be a finite SI number.")
|
||||
@@ -0,0 +1,250 @@
|
||||
"""User-input adapter shared by HTTP and CLI; the numerical layer stays SI-only.
|
||||
|
||||
JSON v1 numbers (including decimal strings) were SI, but expressions used the
|
||||
selected unit. JSON v2 consistently uses the selected unit for both. Missing
|
||||
parameters use catalog defaults, which are always SI. Never infer a format from
|
||||
magnitudes or relabel a legacy project without converting its values.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import math
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
UNIT_TABLE = json.loads((Path(__file__).resolve().parent.parent / "schemas" / "parameter-units.json").read_text(encoding="utf-8"))
|
||||
DECIMAL = re.compile(r"[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?\Z", re.ASCII)
|
||||
TOKEN = re.compile(r"(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?|[A-Za-z_][A-Za-z_0-9]*|\*\*|[+*/^(),-]", re.ASCII)
|
||||
|
||||
|
||||
def finite(value: float) -> float:
|
||||
if not math.isfinite(value):
|
||||
raise ValueError("Parameter expression must produce a finite real number.")
|
||||
return value
|
||||
|
||||
|
||||
def numeric_literal(value: object) -> float | None:
|
||||
if type(value) in (int, float):
|
||||
try:
|
||||
return finite(float(value))
|
||||
except OverflowError as exc:
|
||||
raise ValueError("Parameter magnitude exceeds finite float range.") from exc
|
||||
if isinstance(value, str) and DECIMAL.fullmatch(value.strip()):
|
||||
return finite(float(value))
|
||||
return None
|
||||
|
||||
|
||||
def expression_value(source: str) -> float:
|
||||
"""Same bounded recursive-descent grammar as parameterExpression.ts; no eval."""
|
||||
source = source.strip().removeprefix("=").strip()
|
||||
if not source or len(source) > 512:
|
||||
raise ValueError("Parameter expression must contain 1..512 characters.")
|
||||
tokens: list[str] = []
|
||||
position = 0
|
||||
while position < len(source):
|
||||
if source[position].isspace():
|
||||
position += 1
|
||||
continue
|
||||
match = TOKEN.match(source, position)
|
||||
if match is None:
|
||||
raise ValueError(f"Unsupported expression character at {position + 1}.")
|
||||
tokens.append(match[0])
|
||||
position = match.end()
|
||||
if len(tokens) > 256:
|
||||
raise ValueError("Parameter expression exceeds 256 tokens.")
|
||||
tokens.append("")
|
||||
index = 0
|
||||
operations = 0
|
||||
|
||||
def current():
|
||||
return tokens[index]
|
||||
|
||||
def take():
|
||||
nonlocal index
|
||||
token = current()
|
||||
if token:
|
||||
index += 1
|
||||
return token
|
||||
|
||||
def operation():
|
||||
nonlocal operations
|
||||
operations += 1
|
||||
if operations > 256:
|
||||
raise ValueError("Parameter expression exceeds 256 operations.")
|
||||
|
||||
def depth_check(depth):
|
||||
if depth > 32:
|
||||
raise ValueError("Parameter expression exceeds 32 nesting levels.")
|
||||
|
||||
def additive(depth):
|
||||
value = multiplicative(depth)
|
||||
while current() in ("+", "-"):
|
||||
op = take()
|
||||
right = multiplicative(depth)
|
||||
operation()
|
||||
value = finite(value + right if op == "+" else value - right)
|
||||
return value
|
||||
|
||||
def multiplicative(depth):
|
||||
value = unary(depth)
|
||||
while current() in ("*", "/"):
|
||||
op = take()
|
||||
right = unary(depth)
|
||||
operation()
|
||||
value = finite(value * right if op == "*" else value / right)
|
||||
return value
|
||||
|
||||
def unary(depth):
|
||||
depth_check(depth)
|
||||
if current() in ("+", "-"):
|
||||
op = take()
|
||||
operation()
|
||||
value = unary(depth + 1)
|
||||
return value if op == "+" else -value
|
||||
return power(depth)
|
||||
|
||||
def power(depth):
|
||||
depth_check(depth)
|
||||
value = primary(depth)
|
||||
if current() in ("^", "**"):
|
||||
take()
|
||||
exponent = unary(depth + 1)
|
||||
operation()
|
||||
value = finite(math.pow(value, exponent))
|
||||
return value
|
||||
|
||||
def primary(depth):
|
||||
depth_check(depth)
|
||||
token = take()
|
||||
if token == "(":
|
||||
value = additive(depth + 1)
|
||||
if take() != ")":
|
||||
raise ValueError("Missing closing parenthesis.")
|
||||
return value
|
||||
if token and (token[0].isdigit() or token[0] == "."):
|
||||
return finite(float(token))
|
||||
name = token.lower()
|
||||
if token and (token[0].isalpha() or token[0] == "_"):
|
||||
if current() != "(":
|
||||
if name in ("pi", "e"):
|
||||
return math.pi if name == "pi" else math.e
|
||||
raise ValueError(f"Unknown identifier: {token}.")
|
||||
depth_check(depth + 1)
|
||||
take()
|
||||
args = []
|
||||
if current() != ")":
|
||||
while True:
|
||||
if len(args) >= 16:
|
||||
raise ValueError("Functions accept at most 16 arguments.")
|
||||
args.append(additive(depth + 1))
|
||||
if current() != ",":
|
||||
break
|
||||
take()
|
||||
if take() != ")":
|
||||
raise ValueError("Missing function closing parenthesis.")
|
||||
operation()
|
||||
functions = {"sqrt": math.sqrt, "abs": abs, "sin": math.sin,
|
||||
"cos": math.cos, "tan": math.tan, "asin": math.asin,
|
||||
"acos": math.acos, "atan": math.atan, "exp": math.exp,
|
||||
"ln": math.log, "log": math.log, "log10": math.log10,
|
||||
"pow": math.pow}
|
||||
if name in ("min", "max") and args:
|
||||
return finite((min if name == "min" else max)(args))
|
||||
if name not in functions or len(args) != (2 if name == "pow" else 1):
|
||||
raise ValueError(f"Unsupported function or argument count: {token}.")
|
||||
return finite(functions[name](*args))
|
||||
raise ValueError("Expected a number, constant or function.")
|
||||
|
||||
try:
|
||||
result = additive(0)
|
||||
if current():
|
||||
raise ValueError("Unexpected trailing expression content.")
|
||||
return finite(result)
|
||||
except (ArithmeticError, RecursionError) as exc:
|
||||
raise ValueError("Invalid arithmetic or expression domain.") from exc
|
||||
|
||||
|
||||
def unit_conversion(definition, unit: str) -> tuple[float, float]:
|
||||
options = UNIT_TABLE.get(definition.quantity, {}) if definition.unit else {}
|
||||
if unit in options:
|
||||
scale, offset, _ = options[unit]
|
||||
return scale, offset
|
||||
if unit == definition.unit:
|
||||
return 1.0, 0.0
|
||||
raise ValueError(f"Unsupported unit '{unit}' for {definition.name} ({definition.quantity}).")
|
||||
|
||||
|
||||
def prepare_project(project):
|
||||
"""Copy external input to a current-version, numeric SI execution project.
|
||||
|
||||
Returns consolidated version notices to the caller. Does not mutate saved
|
||||
data and does not weaken the strict XML/native model-version checks.
|
||||
"""
|
||||
from app.simulation.registry import get_component_model_spec
|
||||
|
||||
normalized = project.model_copy(deep=True)
|
||||
notices = []
|
||||
specs = {}
|
||||
for node in normalized.nodes:
|
||||
model = node.data
|
||||
spec = specs.get(model.modelType)
|
||||
if spec is None:
|
||||
spec = get_component_model_spec(model.modelType)
|
||||
specs[model.modelType] = spec
|
||||
if model.componentType != spec.model_type:
|
||||
raise ValueError(f"COMPONENT_MODEL_TYPE_MISMATCH: {node.id}.")
|
||||
if model.modelVersion != spec.model_version:
|
||||
notices.append({"componentId": node.id, "label": model.label or node.id,
|
||||
"storedVersion": model.modelVersion,
|
||||
"currentVersion": spec.model_version})
|
||||
for name, value in model.parameters.items():
|
||||
definition = spec.parameter_by_name.get(name)
|
||||
if definition is None:
|
||||
raise ValueError(f"Component '{node.id}' contains unsupported parameters: {name}.")
|
||||
try:
|
||||
scale, offset = unit_conversion(definition, model.parameterUnits.get(name, definition.unit))
|
||||
number = numeric_literal(value)
|
||||
is_expression = number is None
|
||||
if is_expression:
|
||||
if not isinstance(value, str) or definition.editor:
|
||||
raise ValueError("Expected a numeric value; discrete parameters cannot use expressions.")
|
||||
number = expression_value(value)
|
||||
if project.projectSchemaVersion == 2 or is_expression:
|
||||
number = finite(number * scale + offset)
|
||||
model.parameters[name] = number
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"{node.id}.{name}: {exc}") from exc
|
||||
# Explicit legacy migrations also used by the browser.
|
||||
if model.modelVersion == "0.1.0" and model.modelType == "amesim_forc":
|
||||
model.parameters.setdefault("direction", 1.0)
|
||||
if model.modelVersion == "0.1.0" and model.modelType == "amesim_lmechn1":
|
||||
count = model.parameters.get("v1")
|
||||
if count in range(1, 9):
|
||||
for edge in normalized.edges:
|
||||
if edge.source == node.id and edge.sourceHandle == "port_9":
|
||||
edge.sourceHandle = f"port_{int(count) + 1}"
|
||||
if edge.target == node.id and edge.targetHandle == "port_9":
|
||||
edge.targetHandle = f"port_{int(count) + 1}"
|
||||
model.parameters["sum"] = 1.0
|
||||
# Historical LMECHN1 exposed only nine ports; use its migrated contract.
|
||||
from app.main import ReactFlowPortDefinition
|
||||
model.ports = [ReactFlowPortDefinition(name=p.name, kind=p.kind, domain=p.domain,
|
||||
nominalRole=p.nominal_role, positiveFlowDirection=p.positive_flow_direction)
|
||||
for p in spec.ports]
|
||||
model.modelVersion = spec.model_version
|
||||
model.parameterUnits = {p.name: p.unit for p in spec.parameters}
|
||||
model.parameterScientificNotation = {}
|
||||
for name in ("t_start", "t_stop", "step", "max_step"):
|
||||
value = getattr(normalized.simulation, name)
|
||||
number = numeric_literal(value)
|
||||
if number is None:
|
||||
number = expression_value(value)
|
||||
setattr(normalized.simulation, name, number)
|
||||
normalized.projectSchemaVersion = 1 # Internal numeric SI contract, never a v2 wire payload.
|
||||
return normalized, notices
|
||||
|
||||
|
||||
def version_warning(notices):
|
||||
return {"code": "COMPONENT_MODEL_VERSION_WARNING",
|
||||
"message": "旧版或版本未知的组件将使用当前模型执行,可能仿真失败或结果与实际不符。",
|
||||
"components": notices}
|
||||
@@ -1,15 +0,0 @@
|
||||
# 元件开发示例
|
||||
|
||||
权威规则见 [组件模型建模规范](../../../docs/standard/component-model-authoring-spec-v1.md)。当前模型采用 Python 声明、C 数值实现。
|
||||
|
||||
以气瓶为例:
|
||||
|
||||
1. 在 [cylinder.py](experimental/storage/cylinder.py) 声明 `MODEL_TYPE`、`MODEL_VERSION`、`PORTS`、`PARAMETERS`、`RESULT_VARIABLES`、`DISPLAY` 和 `create()`。
|
||||
2. 构造函数调用 `set_parameter_values()`、`register_declared_port()`,保存介质选择和容积。不要在 Python 中计算密度、内能或状态导数。
|
||||
3. 通过 `EQUATIONS` 声明端口压力与气瓶状态之间的约束;只保存变量名和关系。
|
||||
气动端口同时声明 `computation`,说明温度、压力和质量/能量流率由谁提供;固定参考口还要声明其支路的参考来源,见 [端口供需合同](../../../docs/standard/port-computation-contract.md)。
|
||||
4. 在 [extended.py](../native_codegen/extended.py) 分配状态及输出位置,生成 `native_medium_init()` 初始化调用和气瓶质量/能量导数计算。
|
||||
5. 公共物性和数值公式由 [kernels.c](../../../native/components/kernels.c) 实现,积分和事件由 `native/runtime/` 处理。
|
||||
6. 加入组件库 `library.py` 及 C 版本白名单,验证目录/XML 合同、边界输入、逆流、守恒、RK45/BDF 和输出键。
|
||||
|
||||
新增模型的参考值应来自独立解析结果、外部可信结果或已有冻结基准;不恢复第二套 Python 数值实现。
|
||||
@@ -1,85 +1,20 @@
|
||||
"""CLI input adapter; XML stays the numerical backend's execution contract."""
|
||||
"""CLI input adapter shared with HTTP; numerical execution accepts SI XML only."""
|
||||
from __future__ import annotations
|
||||
|
||||
import ast
|
||||
from copy import deepcopy
|
||||
import json
|
||||
from math import isfinite
|
||||
import operator
|
||||
from pathlib import Path
|
||||
import warnings
|
||||
|
||||
from app.project_parameters import expression_value as arithmetic_value, prepare_project, version_warning
|
||||
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))
|
||||
normalized, notices = prepare_project(ReactFlowProjectPayload.model_validate(data))
|
||||
if notices:
|
||||
warnings.warn(json.dumps(version_warning(notices), ensure_ascii=False), UserWarning, stacklevel=2)
|
||||
return build_reactflow_system_xml(normalized)
|
||||
|
||||
|
||||
def load_input(path: Path):
|
||||
|
||||
Reference in new issue
Block a user