完善MECMAS21参数配置与分组交互

This commit is contained in:
ljz committed 2026-08-03 23:51:27 +08:00
1 parent 32f21f08ff
commit be54070855
28 files changed
+3418 -202

No files matched your search

+108 -5
View File
@@ -38,6 +38,8 @@ FRONTEND_DIST_DIR = Path(__file__).resolve().parent.parent / "frontend" / "dist"
PROJECT_STORAGE_DIR = Path(__file__).parent / "data" / "reactflow-projects"
SYSTEM_XML_SCHEMA_VERSION = "2"
SYSTEM_XML_UNIT_SYSTEM = "SI"
AMESIM_PARAMETER_ENCODING_VERSION = 1
MECMAS21_LEGACY_BINARY_PARAMETERS = ("useFriction", "strib")
SimulationProgressEmitter = Callable[
[int, str, str, float | None, float | None],
@@ -137,6 +139,15 @@ class ReactFlowSimulationConfig(BaseModel):
class ReactFlowProjectPayload(BaseModel):
name: str = "untitled"
mediumReferenceVersion: Literal[1] | None = None
# Missing/None is reserved for projects written before canonical AMESim
# option codes were introduced. Every current writer emits version 1.
amesimParameterEncodingVersion: Literal[1] | None = Field(
default=None,
description=(
"Version 1 is required for current writers; omission is accepted "
"only when importing legacy MECMAS21 0/1 option codes."
),
)
nodes: list[ReactFlowNodePayload] = Field(default_factory=list)
edges: list[ReactFlowEdgePayload] = Field(default_factory=list)
simulation: ReactFlowSimulationConfig = Field(default_factory=ReactFlowSimulationConfig)
@@ -168,8 +179,42 @@ class SimulationCancellationPayload(BaseModel):
def pydantic_to_jsonable(model: BaseModel) -> dict[str, Any]:
if hasattr(model, "model_dump"):
return model.model_dump(mode="json")
return model.dict()
return model.model_dump(mode="json", exclude_none=True)
return model.dict(exclude_none=True)
def reactflow_project_storage_data(
project: ReactFlowProjectPayload,
) -> dict[str, Any]:
"""Return a current, self-identifying project payload for persistence."""
data = pydantic_to_jsonable(project)
if project.amesimParameterEncodingVersion is None:
from app.simulation.registry import get_component_model_spec
stored_nodes = data["nodes"]
for index, node in enumerate(project.nodes):
if node.data.modelType != "amesim_mecmas21":
continue
parameter_definitions = get_component_model_spec(
node.data.modelType
).parameter_by_name
stored_parameters = stored_nodes[index]["data"].setdefault(
"parameters",
{},
)
for name in MECMAS21_LEGACY_BINARY_PARAMETERS:
stored_parameters[name] = canonical_amesim_parameter_value(
project,
node,
name,
node.data.parameters.get(
name,
parameter_definitions[name].default,
),
)
data["amesimParameterEncodingVersion"] = AMESIM_PARAMETER_ENCODING_VERSION
return data
@app.get("/")
@@ -367,7 +412,7 @@ def save_reactflow_project(
) -> dict[str, object]:
path = reactflow_project_path(project_id)
PROJECT_STORAGE_DIR.mkdir(parents=True, exist_ok=True)
data = pydantic_to_jsonable(payload)
data = reactflow_project_storage_data(payload)
data["name"] = payload.name or project_id
path.write_text(
json.dumps(data, ensure_ascii=False, indent=2),
@@ -907,6 +952,9 @@ def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
"schemaVersion": SYSTEM_XML_SCHEMA_VERSION,
"unitSystem": SYSTEM_XML_UNIT_SYSTEM,
"mediumReferenceVersion": "1",
"amesimParameterEncodingVersion": str(
AMESIM_PARAMETER_ENCODING_VERSION
),
},
)
ET.SubElement(
@@ -963,11 +1011,30 @@ def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
port_definition.positiveFlowDirection or "intoComponent"
)
ET.SubElement(component_node, "Port", port_attributes)
parameter_values = dict(node.data.parameters)
parameter_values = {
name: canonical_amesim_parameter_value(project, node, name, value)
for name, value in node.data.parameters.items()
}
try:
component_spec = get_component_model_spec(node.data.modelType)
except ValueError:
component_spec = None
if (
component_spec is not None
and project.amesimParameterEncodingVersion is None
and node.data.modelType == "amesim_mecmas21"
):
parameter_definitions = {
parameter.name: parameter for parameter in component_spec.parameters
}
for name in MECMAS21_LEGACY_BINARY_PARAMETERS:
if name not in parameter_values:
parameter_values[name] = canonical_amesim_parameter_value(
project,
node,
name,
parameter_definitions[name].default,
)
if (
component_spec is not None
and component_spec.display.role == "amesimGasMediumDefinition"
@@ -1140,7 +1207,12 @@ def compile_reactflow_network(
for node in project.nodes:
spec = get_component_model_spec(node.data.modelType)
parameter_values = {
parameter.name: parameter_float(node, parameter.name, parameter.default)
parameter.name: canonical_amesim_parameter_value(
project,
node,
parameter.name,
parameter_float(node, parameter.name, parameter.default),
)
for parameter in spec.parameters
}
unknown_parameters = set(node.data.parameters) - set(spec.parameter_by_name)
@@ -1436,6 +1508,37 @@ def parameter_float(
raise ValueError(f"Parameter '{name}' on component '{node.id}' must be numeric.")
def canonical_amesim_parameter_value(
project: ReactFlowProjectPayload,
node: ReactFlowNodePayload,
name: str,
value: Any,
) -> Any:
"""Translate legacy app-local AMESim codes to the canonical catalog codes.
Projects created before ``amesimParameterEncodingVersion`` used 0/1 for
MECMAS21 yes/no parameters. AMESim itself uses 1/2, so preserve the old
project meaning while all newly saved projects and System XML use the
canonical codes.
"""
if (
project.amesimParameterEncodingVersion is not None
or node.data.modelType != "amesim_mecmas21"
or name not in MECMAS21_LEGACY_BINARY_PARAMETERS
):
return value
try:
numeric_value = float(value)
except (TypeError, ValueError):
return value
if numeric_value == 0.0:
return 1.0
if numeric_value == 1.0:
return 2.0
return value
def pipe_config_from_node(node: ReactFlowNodePayload | None, pipe_config_type):
return pipe_config_type(
length=parameter_float(node, "length", 5.0),