ReactFlow前端拖拽模型与参数配置功能MVP
This commit is contained in:
1 parent
70c91ed019
commit
d3b99304fb
2001 files changed
+349044
-548
No files matched your search
+328
-283
@@ -1,330 +1,375 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
import json
|
||||
import os
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import zlib
|
||||
from pathlib import Path
|
||||
from typing import Iterable
|
||||
from urllib.parse import unquote, urljoin
|
||||
from typing import Any
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request, Response
|
||||
from fastapi.responses import HTMLResponse
|
||||
from pydantic import BaseModel
|
||||
from fastapi import FastAPI, HTTPException, Response
|
||||
from fastapi.responses import FileResponse, HTMLResponse
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
app = FastAPI(title="System Simulation App")
|
||||
INDEX_TEMPLATE = Path(__file__).parent / "static" / "index.html"
|
||||
DRAWIO_UPSTREAM_URL = (
|
||||
os.getenv("DRAWIO_UPSTREAM_URL")
|
||||
or os.getenv("DRAWIO_BASE_URL")
|
||||
or "http://127.0.0.1:8081/"
|
||||
)
|
||||
COMPONENT_LIBRARY = [
|
||||
{
|
||||
"title": "\u8d2e\u7bb1",
|
||||
"w": 80,
|
||||
"h": 80,
|
||||
"xml": (
|
||||
'<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/>'
|
||||
'<object id="tank" label="\u8d2e\u7bb1" componentType="tank" '
|
||||
'medium="N2" volume="1.0"><mxCell style="shape=cylinder;'
|
||||
"whiteSpace=wrap;html=1;boundedLbl=1;backgroundOutline=1;"
|
||||
'size=15;fillColor=#dae8fc;strokeColor=#6c8ebf;" vertex="1" '
|
||||
'parent="1"><mxGeometry width="80" height="80" as="geometry"/>'
|
||||
"</mxCell></object></root></mxGraphModel>"
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "\u9600\u95e8",
|
||||
"w": 72,
|
||||
"h": 56,
|
||||
"xml": (
|
||||
'<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/>'
|
||||
'<object id="valve" label="\u9600\u95e8" componentType="valve" '
|
||||
'valveType="manual" nominalDiameter="10"><mxCell style="rhombus;'
|
||||
'whiteSpace=wrap;html=1;fillColor=#ffe6cc;strokeColor=#d79b00;" '
|
||||
'vertex="1" parent="1"><mxGeometry width="72" height="56" '
|
||||
'as="geometry"/></mxCell></object></root></mxGraphModel>'
|
||||
),
|
||||
},
|
||||
{
|
||||
"title": "\u7ba1\u6bb5",
|
||||
"w": 96,
|
||||
"h": 36,
|
||||
"xml": (
|
||||
'<mxGraphModel><root><mxCell id="0"/><mxCell id="1" parent="0"/>'
|
||||
'<object id="pipe" label="\u7ba1\u6bb5" componentType="pipe" '
|
||||
'length="1.0" diameter="0.01"><mxCell style="rounded=1;'
|
||||
"whiteSpace=wrap;html=1;arcSize=12;fillColor=#d5e8d4;"
|
||||
'strokeColor=#82b366;" vertex="1" parent="1"><mxGeometry '
|
||||
'width="96" height="36" as="geometry"/></mxCell></object>'
|
||||
"</root></mxGraphModel>"
|
||||
),
|
||||
},
|
||||
]
|
||||
app = FastAPI(title="System Simulation ReactFlow App")
|
||||
FRONTEND_DIST_DIR = Path(__file__).resolve().parent.parent / "frontend" / "dist"
|
||||
PROJECT_STORAGE_DIR = Path(__file__).parent / "data" / "reactflow-projects"
|
||||
|
||||
|
||||
class DiagramPayload(BaseModel):
|
||||
xml: str
|
||||
class ReactFlowPosition(BaseModel):
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
|
||||
|
||||
class ReactFlowNodeData(BaseModel):
|
||||
label: str = ""
|
||||
componentType: str = "component"
|
||||
modelType: str = "component"
|
||||
ports: list[str] = Field(default_factory=list)
|
||||
parameters: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class ReactFlowNodePayload(BaseModel):
|
||||
id: str
|
||||
type: str | None = None
|
||||
position: ReactFlowPosition = Field(default_factory=ReactFlowPosition)
|
||||
data: ReactFlowNodeData
|
||||
|
||||
|
||||
class ReactFlowEdgePayload(BaseModel):
|
||||
id: str
|
||||
source: str
|
||||
target: str
|
||||
sourceHandle: str | None = None
|
||||
targetHandle: str | None = None
|
||||
|
||||
|
||||
class ReactFlowSimulationConfig(BaseModel):
|
||||
t_start: float = 0.0
|
||||
t_stop: float = 2.0
|
||||
step: float = 0.1
|
||||
max_step: float = 0.005
|
||||
method: str = "BDF"
|
||||
|
||||
|
||||
class ReactFlowProjectPayload(BaseModel):
|
||||
name: str = "untitled"
|
||||
nodes: list[ReactFlowNodePayload] = Field(default_factory=list)
|
||||
edges: list[ReactFlowEdgePayload] = Field(default_factory=list)
|
||||
simulation: ReactFlowSimulationConfig = Field(default_factory=ReactFlowSimulationConfig)
|
||||
|
||||
|
||||
def pydantic_to_jsonable(model: BaseModel) -> dict[str, Any]:
|
||||
if hasattr(model, "model_dump"):
|
||||
return model.model_dump(mode="json")
|
||||
return model.dict()
|
||||
|
||||
|
||||
@app.get("/")
|
||||
def index(request: Request) -> HTMLResponse:
|
||||
html = INDEX_TEMPLATE.read_text(encoding="utf-8")
|
||||
drawio_base_url = default_drawio_base_url(request)
|
||||
html = html.replace("__DRAWIO_BASE_URL__", json.dumps(drawio_base_url))
|
||||
return HTMLResponse(html, headers={"Cache-Control": "no-store"})
|
||||
def index() -> HTMLResponse:
|
||||
frontend_index = FRONTEND_DIST_DIR / "index.html"
|
||||
if frontend_index.exists():
|
||||
return HTMLResponse(
|
||||
frontend_index.read_text(encoding="utf-8"),
|
||||
headers={"Cache-Control": "no-store"},
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/component-library.drawiolib")
|
||||
def component_library() -> Response:
|
||||
library = ET.Element("mxlibrary", {"title": "System Components"})
|
||||
library.text = json.dumps(COMPONENT_LIBRARY, ensure_ascii=False)
|
||||
return Response(
|
||||
content=ET.tostring(library, encoding="utf-8"),
|
||||
media_type="application/xml",
|
||||
headers={
|
||||
"Access-Control-Allow-Origin": "*",
|
||||
"Cache-Control": "no-store",
|
||||
},
|
||||
return HTMLResponse(
|
||||
"""
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<title>System Simulation Flow</title>
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
||||
color: #1f2933;
|
||||
background: #f8fafc;
|
||||
}
|
||||
main {
|
||||
max-width: 720px;
|
||||
margin: 96px auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
code {
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
background: #e5e7eb;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<main>
|
||||
<h1>System Simulation Flow</h1>
|
||||
<p>ReactFlow 前端未构建。开发时请在 <code>frontend</code> 目录运行 <code>npm run dev</code>,然后打开 <code>http://127.0.0.1:5173</code>。</p>
|
||||
<p>如果要让 FastAPI 直接服务页面,请先运行 <code>npm run build</code> 生成 <code>frontend/dist</code>。</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
""",
|
||||
headers={"Cache-Control": "no-store"},
|
||||
)
|
||||
|
||||
|
||||
@app.api_route("/drawio", methods=["GET", "POST", "HEAD", "OPTIONS"])
|
||||
@app.api_route("/drawio/{path:path}", methods=["GET", "POST", "HEAD", "OPTIONS"])
|
||||
async def drawio_proxy(request: Request, path: str = "") -> Response:
|
||||
upstream_url = build_drawio_upstream_url(path, request.url.query)
|
||||
body = await request.body()
|
||||
headers = {
|
||||
key: value
|
||||
for key, value in request.headers.items()
|
||||
if key.lower()
|
||||
not in {
|
||||
"host",
|
||||
"connection",
|
||||
"content-length",
|
||||
"accept-encoding",
|
||||
}
|
||||
@app.get("/assets/{path:path}")
|
||||
def frontend_asset(path: str) -> FileResponse:
|
||||
asset_path = (FRONTEND_DIST_DIR / "assets" / path).resolve()
|
||||
assets_root = (FRONTEND_DIST_DIR / "assets").resolve()
|
||||
try:
|
||||
asset_path.relative_to(assets_root)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=404, detail="Frontend asset was not found.") from exc
|
||||
if not asset_path.exists():
|
||||
raise HTTPException(status_code=404, detail="Frontend asset was not found.")
|
||||
return FileResponse(asset_path)
|
||||
|
||||
|
||||
@app.post("/api/reactflow/system-xml")
|
||||
def export_reactflow_system_xml(payload: ReactFlowProjectPayload) -> Response:
|
||||
return Response(
|
||||
content=build_reactflow_system_xml(payload),
|
||||
media_type="application/xml",
|
||||
)
|
||||
|
||||
|
||||
@app.get("/api/reactflow/projects")
|
||||
def list_reactflow_projects() -> dict[str, object]:
|
||||
PROJECT_STORAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
projects = []
|
||||
for path in sorted(PROJECT_STORAGE_DIR.glob("*.json")):
|
||||
projects.append(
|
||||
{
|
||||
"id": path.stem,
|
||||
"updatedAt": datetime.fromtimestamp(
|
||||
path.stat().st_mtime,
|
||||
timezone.utc,
|
||||
).isoformat(),
|
||||
}
|
||||
)
|
||||
return {"projects": projects}
|
||||
|
||||
|
||||
@app.get("/api/reactflow/projects/{project_id}")
|
||||
def load_reactflow_project(project_id: str) -> dict[str, object]:
|
||||
path = reactflow_project_path(project_id)
|
||||
if not path.exists():
|
||||
raise HTTPException(status_code=404, detail="Project was not found.")
|
||||
return json.loads(path.read_text(encoding="utf-8"))
|
||||
|
||||
|
||||
@app.post("/api/reactflow/projects/{project_id}")
|
||||
def save_reactflow_project(
|
||||
project_id: str,
|
||||
payload: ReactFlowProjectPayload,
|
||||
) -> dict[str, object]:
|
||||
path = reactflow_project_path(project_id)
|
||||
PROJECT_STORAGE_DIR.mkdir(parents=True, exist_ok=True)
|
||||
data = pydantic_to_jsonable(payload)
|
||||
data["name"] = payload.name or project_id
|
||||
path.write_text(
|
||||
json.dumps(data, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
return {
|
||||
"id": path.stem,
|
||||
"path": str(path),
|
||||
"componentCount": len(payload.nodes),
|
||||
"connectionCount": len(payload.edges),
|
||||
}
|
||||
|
||||
upstream_request = urllib.request.Request(
|
||||
upstream_url,
|
||||
data=body if body else None,
|
||||
headers=headers,
|
||||
method=request.method,
|
||||
)
|
||||
|
||||
@app.post("/api/reactflow/simulate-testmodel")
|
||||
def simulate_reactflow_testmodel(payload: ReactFlowProjectPayload) -> dict[str, object]:
|
||||
try:
|
||||
with urllib.request.urlopen(upstream_request, timeout=30) as upstream_response:
|
||||
content = upstream_response.read()
|
||||
return Response(
|
||||
content=content,
|
||||
status_code=upstream_response.status,
|
||||
headers=filter_proxy_headers(upstream_response.headers),
|
||||
)
|
||||
except urllib.error.HTTPError as exc:
|
||||
return Response(
|
||||
content=exc.read(),
|
||||
status_code=exc.code,
|
||||
headers=filter_proxy_headers(exc.headers),
|
||||
)
|
||||
except urllib.error.URLError as exc:
|
||||
raise HTTPException(
|
||||
status_code=502,
|
||||
detail=f"Unable to reach draw.io upstream: {exc.reason}",
|
||||
) from exc
|
||||
|
||||
|
||||
@app.post("/api/system-xml")
|
||||
def export_system_xml(payload: DiagramPayload) -> Response:
|
||||
try:
|
||||
model = read_graph_model(payload.xml)
|
||||
system_xml = build_system_xml(model)
|
||||
result = run_reactflow_testmodel(payload)
|
||||
except ValueError as exc:
|
||||
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||
|
||||
return Response(content=system_xml, media_type="application/xml")
|
||||
return result
|
||||
|
||||
|
||||
def default_drawio_base_url(request: Request) -> str:
|
||||
hostname = request.url.hostname or "127.0.0.1"
|
||||
if ":" in hostname and not hostname.startswith("["):
|
||||
hostname = f"[{hostname}]"
|
||||
|
||||
port = f":{request.url.port}" if request.url.port else ""
|
||||
return f"{request.url.scheme}://{hostname}{port}/drawio/"
|
||||
|
||||
|
||||
def build_drawio_upstream_url(path: str, query: str) -> str:
|
||||
target = urljoin(DRAWIO_UPSTREAM_URL, path)
|
||||
if query:
|
||||
target = f"{target}?{query}"
|
||||
return target
|
||||
|
||||
|
||||
def filter_proxy_headers(headers) -> dict[str, str]:
|
||||
excluded = {
|
||||
"connection",
|
||||
"content-encoding",
|
||||
"content-length",
|
||||
"keep-alive",
|
||||
"proxy-authenticate",
|
||||
"proxy-authorization",
|
||||
"te",
|
||||
"trailer",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
}
|
||||
return {
|
||||
key: value
|
||||
for key, value in headers.items()
|
||||
if key.lower() not in excluded
|
||||
}
|
||||
|
||||
|
||||
def read_graph_model(xml_text: str) -> ET.Element:
|
||||
xml_text = xml_text.strip()
|
||||
if not xml_text:
|
||||
raise ValueError("Diagram XML is empty.")
|
||||
|
||||
try:
|
||||
root = ET.fromstring(xml_text)
|
||||
except ET.ParseError as exc:
|
||||
raise ValueError("Diagram XML is invalid.") from exc
|
||||
|
||||
if tag_name(root) == "mxGraphModel":
|
||||
return root
|
||||
|
||||
if tag_name(root) != "mxfile":
|
||||
raise ValueError("Expected mxGraphModel or mxfile XML.")
|
||||
|
||||
diagram = next((child for child in root if tag_name(child) == "diagram"), None)
|
||||
if diagram is None:
|
||||
raise ValueError("mxfile does not contain a diagram.")
|
||||
|
||||
embedded_model = next(
|
||||
(child for child in diagram if tag_name(child) == "mxGraphModel"),
|
||||
None,
|
||||
)
|
||||
if embedded_model is not None:
|
||||
return embedded_model
|
||||
|
||||
if not diagram.text:
|
||||
raise ValueError("diagram node is empty.")
|
||||
|
||||
return decode_diagram_payload(diagram.text.strip())
|
||||
|
||||
|
||||
def decode_diagram_payload(payload: str) -> ET.Element:
|
||||
if payload.startswith("<"):
|
||||
return parse_graph_model(payload)
|
||||
|
||||
if payload.startswith("%3C"):
|
||||
return parse_graph_model(unquote(payload))
|
||||
|
||||
try:
|
||||
padded = payload + ("=" * (-len(payload) % 4))
|
||||
compressed = base64.b64decode(padded)
|
||||
try:
|
||||
decoded = zlib.decompress(compressed, -15)
|
||||
except zlib.error:
|
||||
decoded = zlib.decompress(compressed)
|
||||
return parse_graph_model(unquote(decoded.decode("utf-8")))
|
||||
except Exception as exc: # noqa: BLE001 - keep API error small and stable.
|
||||
raise ValueError("Unable to decode draw.io diagram payload.") from exc
|
||||
|
||||
|
||||
def parse_graph_model(xml_text: str) -> ET.Element:
|
||||
try:
|
||||
model = ET.fromstring(xml_text)
|
||||
except ET.ParseError as exc:
|
||||
raise ValueError("Decoded diagram XML is invalid.") from exc
|
||||
|
||||
if tag_name(model) != "mxGraphModel":
|
||||
raise ValueError("Decoded diagram is not an mxGraphModel.")
|
||||
|
||||
return model
|
||||
|
||||
|
||||
def build_system_xml(model: ET.Element) -> bytes:
|
||||
root = next((child for child in model if tag_name(child) == "root"), None)
|
||||
if root is None:
|
||||
raise ValueError("mxGraphModel does not contain a root node.")
|
||||
|
||||
system = ET.Element("System")
|
||||
def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
|
||||
system = ET.Element("System", {"name": project.name})
|
||||
components_node = ET.SubElement(system, "Components")
|
||||
connections_node = ET.SubElement(system, "Connections")
|
||||
|
||||
for component in read_components(root):
|
||||
for node in project.nodes:
|
||||
component_node = ET.SubElement(
|
||||
components_node,
|
||||
"Component",
|
||||
{
|
||||
"id": component["id"],
|
||||
"type": component["type"],
|
||||
"label": component["label"],
|
||||
"id": node.id,
|
||||
"name": node.data.label,
|
||||
"type": node.data.modelType,
|
||||
"componentType": node.data.componentType,
|
||||
"x": f"{node.position.x:g}",
|
||||
"y": f"{node.position.y:g}",
|
||||
},
|
||||
)
|
||||
for name, value in component["properties"].items():
|
||||
for port in node.data.ports:
|
||||
ET.SubElement(component_node, "Port", {"name": port})
|
||||
for name, value in node.data.parameters.items():
|
||||
ET.SubElement(
|
||||
component_node,
|
||||
"Property",
|
||||
{"name": name, "value": value},
|
||||
"Parameter",
|
||||
{"name": name, "value": str(value)},
|
||||
)
|
||||
|
||||
for edge in read_connections(root):
|
||||
ET.SubElement(connections_node, "Connection", edge)
|
||||
for edge in project.edges:
|
||||
ET.SubElement(
|
||||
connections_node,
|
||||
"Connection",
|
||||
{
|
||||
"id": edge.id,
|
||||
"source": edge.source,
|
||||
"sourcePort": edge.sourceHandle or "",
|
||||
"target": edge.target,
|
||||
"targetPort": edge.targetHandle or "",
|
||||
},
|
||||
)
|
||||
|
||||
ET.indent(system, space=" ")
|
||||
return ET.tostring(system, encoding="utf-8", xml_declaration=True)
|
||||
|
||||
|
||||
def read_components(root: ET.Element) -> Iterable[dict]:
|
||||
for element in root:
|
||||
if tag_name(element) != "object":
|
||||
continue
|
||||
|
||||
cell = next((child for child in element if tag_name(child) == "mxCell"), None)
|
||||
if cell is None or cell.attrib.get("vertex") != "1":
|
||||
continue
|
||||
|
||||
component_id = element.attrib.get("id") or cell.attrib.get("id")
|
||||
if not component_id:
|
||||
continue
|
||||
|
||||
properties = {
|
||||
key: value
|
||||
for key, value in element.attrib.items()
|
||||
if key not in {"id", "label", "componentType"}
|
||||
}
|
||||
|
||||
yield {
|
||||
"id": component_id,
|
||||
"type": element.attrib.get("componentType", "component"),
|
||||
"label": element.attrib.get("label", ""),
|
||||
"properties": properties,
|
||||
}
|
||||
def reactflow_project_path(project_id: str) -> Path:
|
||||
safe_id = sanitize_project_id(project_id)
|
||||
return PROJECT_STORAGE_DIR / f"{safe_id}.json"
|
||||
|
||||
|
||||
def read_connections(root: ET.Element) -> Iterable[dict[str, str]]:
|
||||
for cell in root.iter():
|
||||
if tag_name(cell) != "mxCell" or cell.attrib.get("edge") != "1":
|
||||
continue
|
||||
|
||||
source = cell.attrib.get("source")
|
||||
target = cell.attrib.get("target")
|
||||
if not source or not target:
|
||||
continue
|
||||
|
||||
edge = {"source": source, "target": target}
|
||||
if "id" in cell.attrib:
|
||||
edge["id"] = cell.attrib["id"]
|
||||
yield edge
|
||||
def sanitize_project_id(project_id: str) -> str:
|
||||
safe_id = "".join(
|
||||
character
|
||||
for character in project_id.strip()
|
||||
if character.isalnum() or character in {"-", "_"}
|
||||
)
|
||||
if not safe_id:
|
||||
raise HTTPException(status_code=400, detail="Project id cannot be empty.")
|
||||
return safe_id[:80]
|
||||
|
||||
|
||||
def tag_name(element: ET.Element) -> str:
|
||||
return element.tag.rsplit("}", 1)[-1]
|
||||
def run_reactflow_testmodel(project: ReactFlowProjectPayload) -> dict[str, object]:
|
||||
from PythonModels.core.solver import SolveIVPConfig
|
||||
from PythonModels.scripts.run_testmodel import (
|
||||
TestModelExecutionConfig,
|
||||
TestModelRunConfig,
|
||||
TestModelSamplingConfig,
|
||||
run_testmodel,
|
||||
)
|
||||
from PythonModels.systems.testmodel import (
|
||||
BranchConfig,
|
||||
CylinderConfig,
|
||||
OrificeConfig,
|
||||
PipeConfig,
|
||||
TankConfig,
|
||||
TestModelConfig,
|
||||
)
|
||||
|
||||
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(),
|
||||
}
|
||||
|
||||
|
||||
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(
|
||||
node: ReactFlowNodePayload | None,
|
||||
name: str,
|
||||
default: float,
|
||||
) -> float:
|
||||
if node is None:
|
||||
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.")
|
||||
|
||||
|
||||
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