完善建模交互、组件图标与系统协议
This commit is contained in:
1 parent
456c29b3b6
commit
6572defaa4
66 files changed
+10067
-4163
No files matched your search
@@ -7,7 +7,7 @@ from app.main import (
|
||||
ReactFlowProjectPayload,
|
||||
build_reactflow_system_xml,
|
||||
compile_reactflow_network,
|
||||
reactflow_project_storage_data,
|
||||
compile_system_xml_network,
|
||||
run_system_xml_simulation,
|
||||
)
|
||||
from app.simulation.systems.generic import GenericFluidSystem
|
||||
@@ -64,23 +64,17 @@ def mechanical_port(name: str, side: str) -> dict[str, str]:
|
||||
return physical_port(name, "bidirectional", side, domain="mechanical")
|
||||
|
||||
|
||||
def mecmas_parameter_encoding_project(
|
||||
def mecmas_project(
|
||||
use_friction: float,
|
||||
strib: float,
|
||||
*,
|
||||
encoding_version: int | None,
|
||||
) -> ReactFlowProjectPayload:
|
||||
parameters = {
|
||||
**MECMAS21_DEFAULTS,
|
||||
"useFriction": use_friction,
|
||||
"strib": strib,
|
||||
}
|
||||
version = (
|
||||
{"amesimParameterEncodingVersion": encoding_version}
|
||||
if encoding_version is not None
|
||||
else {}
|
||||
)
|
||||
return ReactFlowProjectPayload(
|
||||
projectSchemaVersion=1,
|
||||
name="amesim-mecmas21-parameter-encoding",
|
||||
nodes=[
|
||||
component_node(
|
||||
@@ -101,7 +95,6 @@ def mecmas_parameter_encoding_project(
|
||||
"max_step": 0.01,
|
||||
"method": "BDF",
|
||||
},
|
||||
**version,
|
||||
)
|
||||
|
||||
|
||||
@@ -117,8 +110,8 @@ def mecmas_xml_parameter_values(xml: bytes) -> tuple[ET.Element, dict[str, float
|
||||
|
||||
def zero_force_mass_project() -> ReactFlowProjectPayload:
|
||||
return ReactFlowProjectPayload(
|
||||
projectSchemaVersion=1,
|
||||
name="amesim-mechanical-zero-force-smoke",
|
||||
amesimParameterEncodingVersion=1,
|
||||
nodes=[
|
||||
component_node("zero_left", "amesim_f000", [mechanical_port("port_1", "right")]),
|
||||
component_node(
|
||||
@@ -141,8 +134,8 @@ def signal_force_mass_project() -> ReactFlowProjectPayload:
|
||||
parameters = dict(MECMAS21_DEFAULTS)
|
||||
parameters["mass"] = 2.0
|
||||
return ReactFlowProjectPayload(
|
||||
projectSchemaVersion=1,
|
||||
name="amesim-mechanical-signal-force-smoke",
|
||||
amesimParameterEncodingVersion=1,
|
||||
nodes=[
|
||||
component_node(
|
||||
"force_signal",
|
||||
@@ -206,8 +199,8 @@ def elastic_contact_project() -> ReactFlowProjectPayload:
|
||||
right_parameters = dict(MECMAS21_DEFAULTS)
|
||||
right_parameters["x0"] = 0.0
|
||||
return ReactFlowProjectPayload(
|
||||
projectSchemaVersion=1,
|
||||
name="amesim-mechanical-elastic-contact-smoke",
|
||||
amesimParameterEncodingVersion=1,
|
||||
nodes=[
|
||||
component_node("zero_left", "amesim_f000", [mechanical_port("port_1", "right")]),
|
||||
component_node(
|
||||
@@ -240,20 +233,20 @@ def elastic_contact_project() -> ReactFlowProjectPayload:
|
||||
)
|
||||
|
||||
|
||||
def force_node_mass_project() -> ReactFlowProjectPayload:
|
||||
node_ports = [mechanical_port(f"port_{index}", "left") for index in range(1, 9)]
|
||||
node_ports.append(mechanical_port("port_9", "right"))
|
||||
def force_node_mass_project(right_port_count: int = 8) -> ReactFlowProjectPayload:
|
||||
node_ports = [mechanical_port(f"port_{index}", "right") for index in range(1, 21)]
|
||||
node_ports.append(mechanical_port("port_21", "left"))
|
||||
zero_nodes = [
|
||||
component_node(f"zero_{index}", "amesim_f000", [mechanical_port("port_1", "right")])
|
||||
for index in range(2, 9)
|
||||
for index in range(2, right_port_count + 1)
|
||||
]
|
||||
zero_edges = [
|
||||
physical_edge(f"edge-zero-{index}", f"zero_{index}", "port_1", "node_1", f"port_{index}")
|
||||
for index in range(2, 9)
|
||||
for index in range(2, right_port_count + 1)
|
||||
]
|
||||
return ReactFlowProjectPayload(
|
||||
projectSchemaVersion=1,
|
||||
name="amesim-mechanical-node-smoke",
|
||||
amesimParameterEncodingVersion=1,
|
||||
nodes=[
|
||||
component_node(
|
||||
"step_1",
|
||||
@@ -266,7 +259,12 @@ def force_node_mass_project() -> ReactFlowProjectPayload:
|
||||
"amesim_forc",
|
||||
[signal_port("res", "input", "left"), mechanical_port("port_2", "right")],
|
||||
),
|
||||
component_node("node_1", "amesim_lmechn1", node_ports, {"v1": 8.0, "sum": 1.0}),
|
||||
component_node(
|
||||
"node_1",
|
||||
"amesim_lmechn1",
|
||||
node_ports,
|
||||
{"v1": float(right_port_count), "sum": 1.0},
|
||||
),
|
||||
*zero_nodes,
|
||||
component_node(
|
||||
"mass_1",
|
||||
@@ -280,7 +278,13 @@ def force_node_mass_project() -> ReactFlowProjectPayload:
|
||||
signal_edge("signal-1", "step_1", "out", "force_1", "res"),
|
||||
physical_edge("edge-force", "force_1", "port_2", "node_1", "port_1"),
|
||||
*zero_edges,
|
||||
physical_edge("edge-mass", "node_1", "port_9", "mass_1", "port_1"),
|
||||
physical_edge(
|
||||
"edge-mass",
|
||||
"node_1",
|
||||
f"port_{right_port_count + 1}",
|
||||
"mass_1",
|
||||
"port_1",
|
||||
),
|
||||
physical_edge("edge-right", "mass_1", "port_2", "zero_right", "port_1"),
|
||||
],
|
||||
simulation={"t_start": 0.0, "t_stop": 0.02, "step": 0.01, "max_step": 0.005, "method": "BDF"},
|
||||
@@ -299,168 +303,66 @@ class AmesimMechanicalXmlTests(unittest.TestCase):
|
||||
self.assertEqual(len(tolerances), len(system.initial_state_vector()))
|
||||
|
||||
|
||||
def test_sparse_legacy_reactflow_mecmas_defaults_are_canonicalized(
|
||||
self,
|
||||
) -> None:
|
||||
project = mecmas_parameter_encoding_project(
|
||||
1.0,
|
||||
1.0,
|
||||
encoding_version=None,
|
||||
)
|
||||
def test_sparse_reactflow_mecmas_parameters_use_registered_defaults(self) -> None:
|
||||
project = mecmas_project(1.0, 1.0)
|
||||
project.nodes[0].data.parameters.pop("useFriction")
|
||||
project.nodes[0].data.parameters.pop("strib")
|
||||
|
||||
network = compile_reactflow_network(project)
|
||||
mass = network.components["mass_1"]
|
||||
self.assertEqual(mass.parameter_values["useFriction"], 2.0)
|
||||
self.assertEqual(mass.parameter_values["strib"], 2.0)
|
||||
self.assertEqual(mass.parameter_values["strib"], 1.0)
|
||||
self.assertTrue(mass.use_friction)
|
||||
|
||||
xml = build_reactflow_system_xml(project)
|
||||
root, parameters = mecmas_xml_parameter_values(xml)
|
||||
self.assertEqual(root.get("amesimParameterEncodingVersion"), "1")
|
||||
_, parameters = mecmas_xml_parameter_values(xml)
|
||||
self.assertEqual(parameters["useFriction"], 2.0)
|
||||
self.assertEqual(parameters["strib"], 2.0)
|
||||
self.assertEqual(parameters["strib"], 1.0)
|
||||
|
||||
report = validate_system_xml_document(xml)
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
assert report.document is not None
|
||||
normalized_project = ReactFlowProjectPayload(
|
||||
**report.document.as_project_data()
|
||||
)
|
||||
normalized_mass = compile_reactflow_network(normalized_project).components[
|
||||
"mass_1"
|
||||
]
|
||||
normalized_mass = compile_system_xml_network(report.document).components["mass_1"]
|
||||
self.assertEqual(normalized_mass.parameter_values["useFriction"], 2.0)
|
||||
self.assertEqual(normalized_mass.parameter_values["strib"], 2.0)
|
||||
self.assertEqual(normalized_mass.parameter_values["strib"], 1.0)
|
||||
self.assertTrue(normalized_mass.use_friction)
|
||||
|
||||
stored_data = reactflow_project_storage_data(project)
|
||||
self.assertEqual(stored_data["amesimParameterEncodingVersion"], 1)
|
||||
stored_parameters = stored_data["nodes"][0]["data"]["parameters"]
|
||||
self.assertEqual(stored_parameters["useFriction"], 2.0)
|
||||
self.assertEqual(stored_parameters["strib"], 2.0)
|
||||
stored_mass = compile_reactflow_network(
|
||||
ReactFlowProjectPayload(**stored_data)
|
||||
).components["mass_1"]
|
||||
self.assertTrue(stored_mass.use_friction)
|
||||
def test_removed_zero_based_mecmas_code_is_rejected(self) -> None:
|
||||
project = mecmas_project(0.0, 0.0)
|
||||
|
||||
def test_legacy_reactflow_mecmas_codes_compile_and_export_as_canonical(
|
||||
self,
|
||||
) -> None:
|
||||
for legacy_value, canonical_value in ((0.0, 1.0), (1.0, 2.0)):
|
||||
with self.subTest(legacy_value=legacy_value):
|
||||
project = mecmas_parameter_encoding_project(
|
||||
legacy_value,
|
||||
legacy_value,
|
||||
encoding_version=None,
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "must be one of 1, 2"):
|
||||
compile_reactflow_network(project)
|
||||
with self.assertRaisesRegex(ValueError, "must be one of 1, 2"):
|
||||
build_reactflow_system_xml(project)
|
||||
|
||||
network = compile_reactflow_network(project)
|
||||
mass = network.components["mass_1"]
|
||||
self.assertEqual(
|
||||
mass.parameter_values["useFriction"],
|
||||
canonical_value,
|
||||
)
|
||||
self.assertEqual(
|
||||
mass.parameter_values["strib"],
|
||||
canonical_value,
|
||||
)
|
||||
self.assertEqual(
|
||||
mass.use_friction,
|
||||
canonical_value == 2.0,
|
||||
)
|
||||
|
||||
root, parameters = mecmas_xml_parameter_values(
|
||||
build_reactflow_system_xml(project)
|
||||
)
|
||||
self.assertEqual(
|
||||
root.get("amesimParameterEncodingVersion"),
|
||||
"1",
|
||||
)
|
||||
self.assertEqual(
|
||||
parameters["useFriction"],
|
||||
canonical_value,
|
||||
)
|
||||
self.assertEqual(parameters["strib"], canonical_value)
|
||||
|
||||
def test_versioned_reactflow_mecmas_codes_are_not_reinterpreted(self) -> None:
|
||||
project = mecmas_parameter_encoding_project(
|
||||
1.0,
|
||||
2.0,
|
||||
encoding_version=1,
|
||||
)
|
||||
def test_current_mecmas_codes_are_not_reinterpreted(self) -> None:
|
||||
project = mecmas_project(1.0, 2.0)
|
||||
|
||||
network = compile_reactflow_network(project)
|
||||
mass = network.components["mass_1"]
|
||||
self.assertEqual(mass.parameter_values["useFriction"], 1.0)
|
||||
self.assertEqual(mass.parameter_values["strib"], 2.0)
|
||||
|
||||
def test_legacy_system_xml_mecmas_codes_are_migrated_with_warning(self) -> None:
|
||||
for legacy_value, canonical_value in ((0.0, 1.0), (1.0, 2.0)):
|
||||
with self.subTest(legacy_value=legacy_value):
|
||||
root, _ = mecmas_xml_parameter_values(
|
||||
build_reactflow_system_xml(
|
||||
mecmas_parameter_encoding_project(
|
||||
1.0,
|
||||
1.0,
|
||||
encoding_version=1,
|
||||
)
|
||||
)
|
||||
)
|
||||
del root.attrib["amesimParameterEncodingVersion"]
|
||||
component = root.find("./Components/Component[@id='mass_1']")
|
||||
assert component is not None
|
||||
for parameter in component.findall("Parameter"):
|
||||
if parameter.get("name") in {"useFriction", "strib"}:
|
||||
parameter.set("value", str(legacy_value))
|
||||
|
||||
report = validate_system_xml_document(
|
||||
ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||
)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
migration_issues = [
|
||||
issue
|
||||
for issue in report.issues
|
||||
if issue.code == "AMESIM_PARAMETER_ENCODING_MIGRATED"
|
||||
]
|
||||
self.assertEqual(len(migration_issues), 1)
|
||||
self.assertTrue(
|
||||
all(issue.severity == "warning" for issue in migration_issues)
|
||||
)
|
||||
assert report.document is not None
|
||||
migrated = {
|
||||
parameter.name: parameter.value
|
||||
for parameter in report.document.components[0].parameters
|
||||
}
|
||||
self.assertEqual(
|
||||
migrated["useFriction"],
|
||||
canonical_value,
|
||||
)
|
||||
self.assertEqual(migrated["strib"], canonical_value)
|
||||
self.assertEqual(
|
||||
report.document.amesim_parameter_encoding_version,
|
||||
"1",
|
||||
)
|
||||
self.assertEqual(
|
||||
report.document.as_project_data()[
|
||||
"amesimParameterEncodingVersion"
|
||||
],
|
||||
1,
|
||||
)
|
||||
|
||||
def test_sparse_legacy_system_xml_mecmas_defaults_are_migrated(self) -> None:
|
||||
def test_v3_rejects_removed_system_xml_encoding_marker(self) -> None:
|
||||
root, _ = mecmas_xml_parameter_values(
|
||||
build_reactflow_system_xml(
|
||||
mecmas_parameter_encoding_project(
|
||||
1.0,
|
||||
1.0,
|
||||
encoding_version=1,
|
||||
)
|
||||
)
|
||||
build_reactflow_system_xml(mecmas_project(1.0, 1.0))
|
||||
)
|
||||
root.set("amesimParameterEncodingVersion", "1")
|
||||
|
||||
report = validate_system_xml_document(
|
||||
ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||
)
|
||||
|
||||
self.assertFalse(report.valid)
|
||||
self.assertEqual(
|
||||
{issue.code for issue in report.issues},
|
||||
{"XSD_VALIDATION_ERROR"},
|
||||
)
|
||||
|
||||
def test_v3_requires_complete_mecmas_parameter_set(self) -> None:
|
||||
root, _ = mecmas_xml_parameter_values(
|
||||
build_reactflow_system_xml(mecmas_project(1.0, 1.0))
|
||||
)
|
||||
del root.attrib["amesimParameterEncodingVersion"]
|
||||
component = root.find("./Components/Component[@id='mass_1']")
|
||||
assert component is not None
|
||||
for parameter in list(component.findall("Parameter")):
|
||||
@@ -471,49 +373,20 @@ class AmesimMechanicalXmlTests(unittest.TestCase):
|
||||
ET.tostring(root, encoding="utf-8", xml_declaration=True)
|
||||
)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
migration_issues = [
|
||||
issue
|
||||
for issue in report.issues
|
||||
if issue.code == "AMESIM_PARAMETER_ENCODING_MIGRATED"
|
||||
]
|
||||
self.assertEqual(len(migration_issues), 1)
|
||||
self.assertEqual(migration_issues[0].severity, "warning")
|
||||
assert report.document is not None
|
||||
self.assertEqual(report.document.amesim_parameter_encoding_version, "1")
|
||||
parameters = {
|
||||
parameter.name: parameter.value
|
||||
for parameter in report.document.components[0].parameters
|
||||
}
|
||||
self.assertEqual(parameters["useFriction"], 2.0)
|
||||
self.assertEqual(parameters["strib"], 2.0)
|
||||
|
||||
normalized_project = ReactFlowProjectPayload(
|
||||
**report.document.as_project_data()
|
||||
self.assertFalse(report.valid)
|
||||
self.assertEqual(
|
||||
[issue.code for issue in report.issues].count(
|
||||
"PARAMETER_REQUIRED_MISSING"
|
||||
),
|
||||
2,
|
||||
)
|
||||
normalized_mass = compile_reactflow_network(normalized_project).components[
|
||||
"mass_1"
|
||||
]
|
||||
self.assertEqual(normalized_mass.parameter_values["useFriction"], 2.0)
|
||||
self.assertEqual(normalized_mass.parameter_values["strib"], 2.0)
|
||||
self.assertTrue(normalized_mass.use_friction)
|
||||
|
||||
def test_versioned_system_xml_mecmas_codes_are_not_migrated(self) -> None:
|
||||
xml = build_reactflow_system_xml(
|
||||
mecmas_parameter_encoding_project(
|
||||
1.0,
|
||||
1.0,
|
||||
encoding_version=1,
|
||||
)
|
||||
)
|
||||
def test_system_xml_mecmas_codes_are_preserved(self) -> None:
|
||||
xml = build_reactflow_system_xml(mecmas_project(1.0, 1.0))
|
||||
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
self.assertNotIn(
|
||||
"AMESIM_PARAMETER_ENCODING_MIGRATED",
|
||||
{issue.code for issue in report.issues},
|
||||
)
|
||||
assert report.document is not None
|
||||
parameters = {
|
||||
parameter.name: parameter.value
|
||||
@@ -539,6 +412,19 @@ class AmesimMechanicalXmlTests(unittest.TestCase):
|
||||
self.assertGreater(result["series"]["mass_1.v"][-1], 0.0)
|
||||
self.assertGreater(result["series"]["mass_1.x"][-1], 0.0)
|
||||
|
||||
def test_force_node_with_two_right_ports_ignores_reserved_catalog_ports(self) -> None:
|
||||
project = force_node_mass_project(2)
|
||||
xml = build_reactflow_system_xml(project)
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
self.assertTrue(report.valid, report.as_dict())
|
||||
network = compile_reactflow_network(project)
|
||||
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
|
||||
result = run_system_xml_simulation(xml)
|
||||
|
||||
self.assertTrue(result["success"], result["message"])
|
||||
self.assertAlmostEqual(result["series"]["mass_1.a"][0], 5.0)
|
||||
|
||||
def test_elastic_contact_project_compiles_and_simulates(self) -> None:
|
||||
xml = build_reactflow_system_xml(elastic_contact_project())
|
||||
report = validate_system_xml_document(xml)
|
||||
|
||||
Reference in new issue
Block a user