Files
SystemSimulationApp/tests/test_amesim_mechanical_xml.py
T

581 lines
22 KiB
Python

from __future__ import annotations
import unittest
from xml.etree import ElementTree as ET
from app.main import (
ReactFlowProjectPayload,
build_reactflow_system_xml,
compile_reactflow_network,
reactflow_project_storage_data,
run_system_xml_simulation,
)
from app.system_xml import validate_system_xml_document
from tests.test_amesim_pnvo001_signal_xml import signal_edge, signal_port
from tests.test_generic_system_xml_simulation import component_node, physical_edge
from tests.test_system_xml_protocol import physical_port
LSTP00A_DEFAULTS = {
"na": 10.0,
"gap0": 0.0,
"kcont": 1000.0,
"G": 8.57e10,
"sdiam": 0.02,
"wdiam": 0.002,
"rcont": 0.0,
"Pdis": 1.0e-7,
"stiffmode": 1.0,
"discContactOption": 1.0,
}
MECMAS21_DEFAULTS = {
"mass": 2.0,
"fstick": 0.0,
"fcoul": 0.0,
"rvisc": 0.0,
"wind": 0.0,
"dvel": 1.0e-6,
"restdvel": 1.0e-6,
"restcoeff": 0.65,
"astrib": 1.0e-3,
"xmin": -1.0,
"Kbmin": 1.0e9,
"Dbmin": 1.0e4,
"Pdmin": 1.0e-4,
"xmax": 1.0,
"Kbmax": 1.0e9,
"Dbmax": 1.0e4,
"Pdmax": 1.0e-4,
"theta": 0.0,
"useFriction": 1.0,
"stoptype": 4.0,
"discContactOption": 1.0,
"strib": 1.0,
"frictionType": 1.0,
"v0": 0.0,
"x0": 0.0,
}
def mechanical_port(name: str, side: str) -> dict[str, str]:
return physical_port(name, "bidirectional", side, domain="mechanical")
def mecmas_parameter_encoding_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(
name="amesim-mecmas21-parameter-encoding",
nodes=[
component_node(
"mass_1",
"amesim_mecmas21",
[
mechanical_port("port_1", "left"),
mechanical_port("port_2", "right"),
],
parameters,
),
],
edges=[],
simulation={
"t_start": 0.0,
"t_stop": 0.02,
"step": 0.01,
"max_step": 0.01,
"method": "BDF",
},
**version,
)
def mecmas_xml_parameter_values(xml: bytes) -> tuple[ET.Element, dict[str, float]]:
root = ET.fromstring(xml)
component = root.find("./Components/Component[@id='mass_1']")
assert component is not None
return root, {
str(parameter.get("name")): float(str(parameter.get("value")))
for parameter in component.findall("Parameter")
}
def zero_force_mass_project() -> ReactFlowProjectPayload:
return ReactFlowProjectPayload(
name="amesim-mechanical-zero-force-smoke",
amesimParameterEncodingVersion=1,
nodes=[
component_node("zero_left", "amesim_f000", [mechanical_port("port_1", "right")]),
component_node(
"mass_1",
"amesim_mecmas21",
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
MECMAS21_DEFAULTS,
),
component_node("zero_right", "amesim_f000", [mechanical_port("port_1", "left")]),
],
edges=[
physical_edge("edge-1", "zero_left", "port_1", "mass_1", "port_1"),
physical_edge("edge-2", "mass_1", "port_2", "zero_right", "port_1"),
],
simulation={"t_start": 0.0, "t_stop": 0.02, "step": 0.01, "max_step": 0.01, "method": "BDF"},
)
def signal_force_mass_project() -> ReactFlowProjectPayload:
parameters = dict(MECMAS21_DEFAULTS)
parameters["mass"] = 2.0
return ReactFlowProjectPayload(
name="amesim-mechanical-signal-force-smoke",
amesimParameterEncodingVersion=1,
nodes=[
component_node(
"force_signal",
"amesim_ud00",
[signal_port("out", "output", "right")],
{
"tstart": 0.0,
"start1": 10.0,
"end1": 10.0,
"t1": 1.0,
"start2": 10.0,
"end2": 10.0,
"t2": 0.0,
"start3": 10.0,
"end3": 10.0,
"t3": 0.0,
"start4": 10.0,
"end4": 10.0,
"t4": 0.0,
"start5": 10.0,
"end5": 10.0,
"t5": 0.0,
"start6": 10.0,
"end6": 10.0,
"t6": 0.0,
"start7": 10.0,
"end7": 10.0,
"t7": 0.0,
"start8": 10.0,
"end8": 10.0,
"t8": 0.0,
"nstages": 1.0,
"iscyclic": 0.0,
},
),
component_node(
"force_1",
"amesim_forc",
[signal_port("res", "input", "left"), mechanical_port("port_2", "right")],
),
component_node(
"mass_1",
"amesim_mecmas21",
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
parameters,
),
component_node("zero_right", "amesim_f000", [mechanical_port("port_1", "left")]),
],
edges=[
signal_edge("signal-1", "force_signal", "out", "force_1", "res"),
physical_edge("edge-1", "force_1", "port_2", "mass_1", "port_1"),
physical_edge("edge-2", "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"},
)
def elastic_contact_project() -> ReactFlowProjectPayload:
left_parameters = dict(MECMAS21_DEFAULTS)
left_parameters["x0"] = 0.001
right_parameters = dict(MECMAS21_DEFAULTS)
right_parameters["x0"] = 0.0
return ReactFlowProjectPayload(
name="amesim-mechanical-elastic-contact-smoke",
amesimParameterEncodingVersion=1,
nodes=[
component_node("zero_left", "amesim_f000", [mechanical_port("port_1", "right")]),
component_node(
"mass_left",
"amesim_mecmas21",
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
left_parameters,
),
component_node(
"contact_1",
"amesim_lstp00a",
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
LSTP00A_DEFAULTS,
),
component_node(
"mass_right",
"amesim_mecmas21",
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
right_parameters,
),
component_node("zero_right", "amesim_f000", [mechanical_port("port_1", "left")]),
],
edges=[
physical_edge("edge-1", "zero_left", "port_1", "mass_left", "port_1"),
physical_edge("edge-2", "mass_left", "port_2", "contact_1", "port_1"),
physical_edge("edge-3", "contact_1", "port_2", "mass_right", "port_1"),
physical_edge("edge-4", "mass_right", "port_2", "zero_right", "port_1"),
],
simulation={"t_start": 0.0, "t_stop": 0.02, "step": 0.01, "max_step": 0.005, "method": "BDF"},
)
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"))
zero_nodes = [
component_node(f"zero_{index}", "amesim_f000", [mechanical_port("port_1", "right")])
for index in range(2, 9)
]
zero_edges = [
physical_edge(f"edge-zero-{index}", f"zero_{index}", "port_1", "node_1", f"port_{index}")
for index in range(2, 9)
]
return ReactFlowProjectPayload(
name="amesim-mechanical-node-smoke",
amesimParameterEncodingVersion=1,
nodes=[
component_node(
"step_1",
"amesim_step0",
[signal_port("out", "output", "right")],
{"initial": 10.0, "final": 10.0, "time": 0.0},
),
component_node(
"force_1",
"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}),
*zero_nodes,
component_node(
"mass_1",
"amesim_mecmas21",
[mechanical_port("port_1", "left"), mechanical_port("port_2", "right")],
MECMAS21_DEFAULTS,
),
component_node("zero_right", "amesim_f000", [mechanical_port("port_1", "left")]),
],
edges=[
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-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"},
)
class AmesimMechanicalXmlTests(unittest.TestCase):
def test_sparse_legacy_reactflow_mecmas_defaults_are_canonicalized(
self,
) -> None:
project = mecmas_parameter_encoding_project(
1.0,
1.0,
encoding_version=None,
)
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.assertTrue(mass.use_friction)
xml = build_reactflow_system_xml(project)
root, parameters = mecmas_xml_parameter_values(xml)
self.assertEqual(root.get("amesimParameterEncodingVersion"), "1")
self.assertEqual(parameters["useFriction"], 2.0)
self.assertEqual(parameters["strib"], 2.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"
]
self.assertEqual(normalized_mass.parameter_values["useFriction"], 2.0)
self.assertEqual(normalized_mass.parameter_values["strib"], 2.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_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,
)
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,
)
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:
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 list(component.findall("Parameter")):
if parameter.get("name") in {"useFriction", "strib"}:
component.remove(parameter)
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.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()
)
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,
)
)
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
for parameter in report.document.components[0].parameters
}
self.assertEqual(parameters["useFriction"], 1.0)
self.assertEqual(parameters["strib"], 1.0)
def test_force_node_mass_project_compiles_and_simulates(self) -> None:
xml = build_reactflow_system_xml(force_node_mass_project())
report = validate_system_xml_document(xml)
self.assertTrue(report.valid, report.as_dict())
network = compile_reactflow_network(force_node_mass_project())
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
result = run_system_xml_simulation(xml)
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["step_1.out.signal"], [10.0, 10.0, 10.0])
self.assertEqual(result["series"]["force_1.res.signal"], [10.0, 10.0, 10.0])
self.assertAlmostEqual(result["series"]["mass_1.a"][0], 5.0)
self.assertGreater(result["series"]["mass_1.v"][-1], 0.0)
self.assertGreater(result["series"]["mass_1.x"][-1], 0.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)
self.assertTrue(report.valid, report.as_dict())
network = compile_reactflow_network(elastic_contact_project())
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
result = run_system_xml_simulation(xml)
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["time"], [0.0, 0.01, 0.02])
self.assertAlmostEqual(result["series"]["contact_1.force"][0], 1.0)
self.assertAlmostEqual(result["series"]["mass_left.a"][0], -0.5)
self.assertAlmostEqual(result["series"]["mass_right.a"][0], 0.5)
self.assertLess(result["series"]["mass_left.x"][-1], 0.001)
self.assertGreater(result["series"]["mass_right.x"][-1], 0.0)
def test_zero_force_mechanical_project_compiles_and_simulates(self) -> None:
xml = build_reactflow_system_xml(zero_force_mass_project())
report = validate_system_xml_document(xml)
self.assertTrue(report.valid, report.as_dict())
network = compile_reactflow_network(zero_force_mass_project())
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
result = run_system_xml_simulation(xml)
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["time"], [0.0, 0.01, 0.02])
self.assertEqual(result["series"]["mass_1.v"], [0.0, 0.0, 0.0])
self.assertEqual(result["series"]["mass_1.x"], [0.0, 0.0, 0.0])
def test_signal_force_mechanical_project_compiles_and_simulates(self) -> None:
xml = build_reactflow_system_xml(signal_force_mass_project())
report = validate_system_xml_document(xml)
self.assertTrue(report.valid, report.as_dict())
network = compile_reactflow_network(signal_force_mass_project())
self.assertTrue(network.pressure_flow_structure_dict()["isSquare"])
result = run_system_xml_simulation(xml)
self.assertTrue(result["success"], result["message"])
self.assertEqual(result["series"]["force_signal.out.signal"], [10.0, 10.0, 10.0])
self.assertEqual(result["series"]["force_1.res.signal"], [10.0, 10.0, 10.0])
self.assertAlmostEqual(result["series"]["mass_1.a"][0], 5.0)
self.assertGreater(result["series"]["mass_1.v"][-1], 0.0)
self.assertGreater(result["series"]["mass_1.x"][-1], 0.0)
if __name__ == "__main__":
unittest.main()