feat: integrate AMESim media models and editor UI

This commit is contained in:
ljz committed 2026-07-31 23:36:58 +08:00
1 parent 3e4f466114
commit e7177ab03e
181 files changed
+7042 -25270

No files matched your search

+111 -7
View File
@@ -28,6 +28,7 @@ from app.system_xml import (
)
if TYPE_CHECKING:
from app.simulation.components.amesim.gases import AmesimGasRegistry
from app.simulation.core.ports import PortDefinition
from app.simulation.systems.network import SimulationNetwork
@@ -135,6 +136,7 @@ class ReactFlowSimulationConfig(BaseModel):
class ReactFlowProjectPayload(BaseModel):
name: str = "untitled"
mediumReferenceVersion: Literal[1] | None = None
nodes: list[ReactFlowNodePayload] = Field(default_factory=list)
edges: list[ReactFlowEdgePayload] = Field(default_factory=list)
simulation: ReactFlowSimulationConfig = Field(default_factory=ReactFlowSimulationConfig)
@@ -891,12 +893,15 @@ def _compile_xml_document_or_422(
def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
from app.simulation.registry import get_component_model_spec
system = ET.Element(
"System",
{
"name": project.name,
"schemaVersion": SYSTEM_XML_SCHEMA_VERSION,
"unitSystem": SYSTEM_XML_UNIT_SYSTEM,
"mediumReferenceVersion": "1",
},
)
ET.SubElement(
@@ -953,7 +958,20 @@ def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
port_definition.positiveFlowDirection or "intoComponent"
)
ET.SubElement(component_node, "Port", port_attributes)
for name, value in node.data.parameters.items():
parameter_values = dict(node.data.parameters)
try:
component_spec = get_component_model_spec(node.data.modelType)
except ValueError:
component_spec = None
if (
component_spec is not None
and component_spec.display.role == "amesimGasMediumDefinition"
):
for parameter in component_spec.parameters:
if parameter.editor == "amesimGasPropertyModel":
parameter_values.setdefault(parameter.name, parameter.default)
for name, value in parameter_values.items():
ET.SubElement(
component_node,
"Parameter",
@@ -1086,14 +1104,34 @@ def validate_compatible_ports(
)
def compile_reactflow_network(project: ReactFlowProjectPayload) -> "SimulationNetwork":
from app.simulation.core.medium import IdealGasMedium
def compile_reactflow_network(
project: ReactFlowProjectPayload,
*,
amesim_gas_registry: "AmesimGasRegistry | None" = None,
) -> "SimulationNetwork":
from app.simulation.components.amesim.gases import (
AMESIM_BUILTIN_AIR_GAS_INDEX,
default_amesim_gas_registry,
)
from app.simulation.components.amesim.media.properties import (
AmesimGasMediumDefinitionComponent,
)
from app.simulation.registry import get_component_model_spec
from app.simulation.systems.network import SimulationNetwork
medium = IdealGasMedium()
network = SimulationNetwork(name=project.name)
if amesim_gas_registry is None:
amesim_gas_registry = default_amesim_gas_registry()
else:
amesim_gas_registry = amesim_gas_registry.copy()
network = SimulationNetwork(name=project.name)
resolved_nodes = []
specs_by_component_id = {}
gas_indices_by_component_id = {}
# Phase 1: normalize every node and consume compile-time medium definitions.
# A definition node represents one complete property-method instance and is
# intentionally not added to the equation network.
for node in project.nodes:
spec = get_component_model_spec(node.data.modelType)
parameter_values = {
@@ -1107,7 +1145,73 @@ def compile_reactflow_network(project: ReactFlowProjectPayload) -> "SimulationNe
+ ", ".join(sorted(unknown_parameters))
+ "."
)
component = spec.create(node.id, medium, parameter_values)
if issubclass(
spec.component_class,
AmesimGasMediumDefinitionComponent,
):
definition_component = spec.create(
node.id,
amesim_gas_registry.resolve(AMESIM_BUILTIN_AIR_GAS_INDEX),
parameter_values,
)
assert isinstance(
definition_component,
AmesimGasMediumDefinitionComponent,
)
validate_component_port_interface(
node,
definition_component.port_definitions,
)
try:
amesim_gas_registry.register(
definition_component.gas_definition()
)
except ValueError as exc:
raise ValueError(
f"AMESim medium definition component '{node.id}' is invalid: "
f"{exc}"
) from exc
continue
resolved_nodes.append((node, spec, parameter_values))
specs_by_component_id[node.id] = spec
gas_indices_by_component_id[node.id] = (
parameter_values["gi"]
if spec.library.id == "amesim" and "gi" in spec.parameter_by_name
else None
)
# Phase 2: resolve one gas property model for each pneumatic circuit before
# any dynamic component initializes its thermodynamic state.
pneumatic_connections = []
for edge in project.edges:
source_spec = specs_by_component_id.get(edge.source)
target_spec = specs_by_component_id.get(edge.target)
if source_spec is None or target_spec is None:
continue
source_ports = {port.name: port for port in source_spec.ports}
target_ports = {port.name: port for port in target_spec.ports}
source_port = source_ports.get(edge.sourceHandle or "")
target_port = target_ports.get(edge.targetHandle or "")
if (
source_port is not None
and target_port is not None
and source_port.domain == "pneumatic"
and target_port.domain == "pneumatic"
):
pneumatic_connections.append((edge.source, edge.target))
media_by_component_id = amesim_gas_registry.resolve_network_media(
gas_indices_by_component_id,
pneumatic_connections,
)
for node, spec, parameter_values in resolved_nodes:
component = spec.create(
node.id,
media_by_component_id[node.id],
parameter_values,
)
validate_component_port_interface(node, component.port_definitions)
network.add_component(component)
@@ -1260,7 +1364,7 @@ def run_reactflow_testmodel(project: ReactFlowProjectPayload) -> dict[str, objec
def run_reactflow_test_mql(project: ReactFlowProjectPayload) -> dict[str, object]:
from app.simulation.examples.test_mql.run import run_test_mql
from PythonModels.systems.test_mql import TestMqlRunConfig
from app.simulation.examples.test_mql.system import TestMqlRunConfig
run_config = TestMqlRunConfig(
t_start=project.simulation.t_start,