完善MECMAS21参数配置与分组交互
This commit is contained in:
1 parent
32f21f08ff
commit
be54070855
28 files changed
+3418
-202
No files matched your search
@@ -72,7 +72,125 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(mass.port_1.x, 0.1)
|
||||
self.assertAlmostEqual(mass.port_2.v, 0.2)
|
||||
|
||||
def test_mecmas21_uses_amesim_one_two_friction_encoding(self) -> None:
|
||||
disabled = AmesimMecmas21(
|
||||
"mass_disabled",
|
||||
self.medium,
|
||||
useFriction=1.0,
|
||||
fcoul=5.0,
|
||||
v0=2.0,
|
||||
)
|
||||
enabled = AmesimMecmas21(
|
||||
"mass_enabled",
|
||||
self.medium,
|
||||
useFriction=2.0,
|
||||
fcoul=5.0,
|
||||
v0=2.0,
|
||||
)
|
||||
|
||||
self.assertFalse(disabled.use_friction)
|
||||
self.assertEqual(disabled._dry_friction_force(), 0.0)
|
||||
self.assertTrue(enabled.use_friction)
|
||||
self.assertEqual(enabled._dry_friction_force(), -5.0)
|
||||
|
||||
def test_mecmas21_choice_metadata_preserves_amesim_codes(self) -> None:
|
||||
definitions = {
|
||||
definition.name: definition
|
||||
for definition in AmesimMecmas21.PARAMETERS
|
||||
}
|
||||
expected_options = {
|
||||
"useFriction": ((1.0, "否"), (2.0, "是")),
|
||||
"stoptype": (
|
||||
(1.0, "理想限位"),
|
||||
(2.0, "弹性限位"),
|
||||
(3.0, "恢复碰撞"),
|
||||
(4.0, "无限位"),
|
||||
),
|
||||
"discContactOption": (
|
||||
(1.0, "允许负接触力"),
|
||||
(2.0, "不允许负接触力"),
|
||||
),
|
||||
"strib": ((1.0, "否"), (2.0, "是")),
|
||||
"frictionType": ((1.0, "简单"), (2.0, "高级")),
|
||||
}
|
||||
|
||||
for name, options in expected_options.items():
|
||||
with self.subTest(name=name):
|
||||
definition = definitions[name]
|
||||
self.assertEqual(definition.editor, "choice")
|
||||
self.assertEqual(
|
||||
tuple((option.value, option.label) for option in definition.options),
|
||||
options,
|
||||
)
|
||||
self.assertIn("尚未实现", definitions["frictionType"].description)
|
||||
self.assertIn("尚未实现", definitions["strib"].description)
|
||||
self.assertEqual(definitions["theta"].label, "倾角(度)")
|
||||
self.assertIn("+90°", definitions["theta"].description)
|
||||
self.assertIn("-90°", definitions["theta"].description)
|
||||
self.assertIn("重力分量", definitions["theta"].description)
|
||||
self.assertEqual(AmesimMecmas21.MODEL_VERSION, "0.2.0")
|
||||
|
||||
def test_mecmas21_visibility_matches_amesim_parameter_dependencies(self) -> None:
|
||||
definitions = {
|
||||
definition.name: definition
|
||||
for definition in AmesimMecmas21.PARAMETERS
|
||||
}
|
||||
expected_visibility = {
|
||||
"fstick": (
|
||||
("stoptype", (1.0, 2.0, 4.0)),
|
||||
("useFriction", (2.0,)),
|
||||
),
|
||||
"fcoul": (
|
||||
("stoptype", (1.0, 2.0, 4.0)),
|
||||
("useFriction", (2.0,)),
|
||||
),
|
||||
"rvisc": (("useFriction", (2.0,)),),
|
||||
"wind": (("useFriction", (2.0,)),),
|
||||
"dvel": (
|
||||
("stoptype", (1.0, 2.0, 4.0)),
|
||||
("useFriction", (2.0,)),
|
||||
("frictionType", (2.0,)),
|
||||
),
|
||||
"restdvel": (("stoptype", (3.0,)),),
|
||||
"restcoeff": (("stoptype", (3.0,)),),
|
||||
"astrib": (
|
||||
("stoptype", (1.0, 2.0, 4.0)),
|
||||
("useFriction", (2.0,)),
|
||||
("frictionType", (2.0,)),
|
||||
("strib", (2.0,)),
|
||||
),
|
||||
"xmin": (("stoptype", (1.0, 2.0, 3.0)),),
|
||||
"Kbmin": (("stoptype", (2.0,)),),
|
||||
"Dbmin": (("stoptype", (2.0,)),),
|
||||
"Pdmin": (("stoptype", (2.0,)),),
|
||||
"xmax": (("stoptype", (1.0, 2.0, 3.0)),),
|
||||
"Kbmax": (("stoptype", (2.0,)),),
|
||||
"Dbmax": (("stoptype", (2.0,)),),
|
||||
"Pdmax": (("stoptype", (2.0,)),),
|
||||
"discContactOption": (("stoptype", (2.0,)),),
|
||||
"strib": (
|
||||
("stoptype", (1.0, 2.0, 4.0)),
|
||||
("useFriction", (2.0,)),
|
||||
("frictionType", (2.0,)),
|
||||
),
|
||||
"frictionType": (
|
||||
("stoptype", (1.0, 2.0, 4.0)),
|
||||
("useFriction", (2.0,)),
|
||||
),
|
||||
}
|
||||
|
||||
for name, expected in expected_visibility.items():
|
||||
with self.subTest(name=name):
|
||||
self.assertEqual(
|
||||
tuple(
|
||||
(condition.parameter, condition.values)
|
||||
for condition in definitions[name].visible_when
|
||||
),
|
||||
expected,
|
||||
)
|
||||
for name in ("mass", "theta", "useFriction", "stoptype", "v0", "x0"):
|
||||
with self.subTest(always_visible=name):
|
||||
self.assertEqual(definitions[name].visible_when, ())
|
||||
|
||||
def test_lmechn1_ties_active_port_kinematics_and_balances_force(self) -> None:
|
||||
node = AmesimLmechn1("node_1", self.medium, v1=2.0, sum=1.0)
|
||||
@@ -136,14 +254,32 @@ class AmesimMechanicalPublicComponentTests(unittest.TestCase):
|
||||
self.assertAlmostEqual(contact.gap, 0.0005)
|
||||
self.assertAlmostEqual(contact.contact_force, 0.0)
|
||||
|
||||
def test_mecmas21_registry_rejects_fractional_integer_options(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "useFriction must be an integer"):
|
||||
def test_mecmas21_registry_rejects_fractional_choice_value(self) -> None:
|
||||
with self.assertRaisesRegex(ValueError, "must be one of"):
|
||||
COMPONENT_MODEL_REGISTRY["amesim_mecmas21"].create(
|
||||
"mass_1",
|
||||
self.medium,
|
||||
{"useFriction": 0.5},
|
||||
)
|
||||
|
||||
def test_mecmas21_registry_rejects_unknown_integer_options(self) -> None:
|
||||
invalid_options = {
|
||||
"useFriction": 0.0,
|
||||
"stoptype": 0.0,
|
||||
"discContactOption": 3.0,
|
||||
"strib": 0.0,
|
||||
"frictionType": 3.0,
|
||||
}
|
||||
|
||||
for name, value in invalid_options.items():
|
||||
with self.subTest(name=name):
|
||||
with self.assertRaisesRegex(ValueError, "must be one of"):
|
||||
COMPONENT_MODEL_REGISTRY["amesim_mecmas21"].create(
|
||||
"mass_1",
|
||||
self.medium,
|
||||
{name: value},
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -1,11 +1,13 @@
|
||||
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
|
||||
@@ -61,9 +63,61 @@ 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(
|
||||
@@ -87,6 +141,7 @@ def signal_force_mass_project() -> ReactFlowProjectPayload:
|
||||
parameters["mass"] = 2.0
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-mechanical-signal-force-smoke",
|
||||
amesimParameterEncodingVersion=1,
|
||||
nodes=[
|
||||
component_node(
|
||||
"force_signal",
|
||||
@@ -151,6 +206,7 @@ def elastic_contact_project() -> ReactFlowProjectPayload:
|
||||
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(
|
||||
@@ -196,6 +252,7 @@ def force_node_mass_project() -> ReactFlowProjectPayload:
|
||||
]
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-mechanical-node-smoke",
|
||||
amesimParameterEncodingVersion=1,
|
||||
nodes=[
|
||||
component_node(
|
||||
"step_1",
|
||||
@@ -231,6 +288,229 @@ def force_node_mass_project() -> ReactFlowProjectPayload:
|
||||
|
||||
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())
|
||||
|
||||
@@ -46,6 +46,7 @@ def _mass_node(name: str) -> dict[str, object]:
|
||||
def pnrp17_coupled_project() -> ReactFlowProjectPayload:
|
||||
return ReactFlowProjectPayload(
|
||||
name="amesim-pnrp17-coupled-smoke",
|
||||
amesimParameterEncodingVersion=1,
|
||||
nodes=[
|
||||
component_node(
|
||||
"chamber_1",
|
||||
|
||||
@@ -164,8 +164,193 @@ class ComponentCatalogTests(unittest.TestCase):
|
||||
}
|
||||
self.assertEqual(components["amesim_mecmas21"]["category"]["id"], "mechanical")
|
||||
self.assertEqual([port["name"] for port in components["amesim_mecmas21"]["ports"]], ["port_2", "port_1"])
|
||||
self.assertEqual(components["amesim_mecmas21"]["modelVersion"], "0.2.0")
|
||||
self.assertEqual(
|
||||
components["amesim_mecmas21"]["parameterGroups"],
|
||||
[
|
||||
{
|
||||
"id": "friction",
|
||||
"label": "摩擦",
|
||||
"parameters": [
|
||||
"frictionType",
|
||||
"strib",
|
||||
"astrib",
|
||||
"fstick",
|
||||
"fcoul",
|
||||
"rvisc",
|
||||
"wind",
|
||||
"dvel",
|
||||
],
|
||||
"order": 10,
|
||||
"defaultExpanded": False,
|
||||
},
|
||||
{
|
||||
"id": "endstops",
|
||||
"label": "限位",
|
||||
"parameters": [
|
||||
"discContactOption",
|
||||
"xmax",
|
||||
"Kbmax",
|
||||
"Dbmax",
|
||||
"Pdmax",
|
||||
"xmin",
|
||||
"Kbmin",
|
||||
"Dbmin",
|
||||
"Pdmin",
|
||||
"restcoeff",
|
||||
"restdvel",
|
||||
],
|
||||
"order": 20,
|
||||
"defaultExpanded": False,
|
||||
},
|
||||
],
|
||||
)
|
||||
self.assertNotIn("parameterGroups", components["amesim_f000"])
|
||||
self.assertEqual(mecmas_parameters["mass"]["unit"], "kg")
|
||||
self.assertEqual(mecmas_parameters["Kbmin"]["unit"], "N/m")
|
||||
self.assertEqual(mecmas_parameters["theta"]["label"], "倾角(度)")
|
||||
self.assertIn("+90°", mecmas_parameters["theta"]["description"])
|
||||
self.assertIn("-90°", mecmas_parameters["theta"]["description"])
|
||||
self.assertEqual(
|
||||
{
|
||||
name: (
|
||||
mecmas_parameters[name]["editor"],
|
||||
[
|
||||
(option["value"], option["label"])
|
||||
for option in mecmas_parameters[name]["options"]
|
||||
],
|
||||
)
|
||||
for name in (
|
||||
"useFriction",
|
||||
"stoptype",
|
||||
"discContactOption",
|
||||
"strib",
|
||||
"frictionType",
|
||||
)
|
||||
},
|
||||
{
|
||||
"useFriction": ("choice", [(1.0, "否"), (2.0, "是")]),
|
||||
"stoptype": (
|
||||
"choice",
|
||||
[
|
||||
(1.0, "理想限位"),
|
||||
(2.0, "弹性限位"),
|
||||
(3.0, "恢复碰撞"),
|
||||
(4.0, "无限位"),
|
||||
],
|
||||
),
|
||||
"discContactOption": (
|
||||
"choice",
|
||||
[
|
||||
(1.0, "允许负接触力"),
|
||||
(2.0, "不允许负接触力"),
|
||||
],
|
||||
),
|
||||
"strib": ("choice", [(1.0, "否"), (2.0, "是")]),
|
||||
"frictionType": (
|
||||
"choice",
|
||||
[(1.0, "简单"), (2.0, "高级")],
|
||||
),
|
||||
},
|
||||
)
|
||||
self.assertEqual(
|
||||
{
|
||||
name: mecmas_parameters[name]["visibleWhen"]
|
||||
for name in (
|
||||
"fstick",
|
||||
"fcoul",
|
||||
"rvisc",
|
||||
"wind",
|
||||
"dvel",
|
||||
"restdvel",
|
||||
"restcoeff",
|
||||
"astrib",
|
||||
"xmin",
|
||||
"Kbmin",
|
||||
"Dbmin",
|
||||
"Pdmin",
|
||||
"xmax",
|
||||
"Kbmax",
|
||||
"Dbmax",
|
||||
"Pdmax",
|
||||
"discContactOption",
|
||||
"strib",
|
||||
"frictionType",
|
||||
)
|
||||
},
|
||||
{
|
||||
"fstick": [
|
||||
{"parameter": "stoptype", "values": [1.0, 2.0, 4.0]},
|
||||
{"parameter": "useFriction", "values": [2.0]},
|
||||
],
|
||||
"fcoul": [
|
||||
{"parameter": "stoptype", "values": [1.0, 2.0, 4.0]},
|
||||
{"parameter": "useFriction", "values": [2.0]},
|
||||
],
|
||||
"rvisc": [
|
||||
{"parameter": "useFriction", "values": [2.0]},
|
||||
],
|
||||
"wind": [
|
||||
{"parameter": "useFriction", "values": [2.0]},
|
||||
],
|
||||
"dvel": [
|
||||
{"parameter": "stoptype", "values": [1.0, 2.0, 4.0]},
|
||||
{"parameter": "useFriction", "values": [2.0]},
|
||||
{"parameter": "frictionType", "values": [2.0]},
|
||||
],
|
||||
"restdvel": [
|
||||
{"parameter": "stoptype", "values": [3.0]},
|
||||
],
|
||||
"restcoeff": [
|
||||
{"parameter": "stoptype", "values": [3.0]},
|
||||
],
|
||||
"astrib": [
|
||||
{"parameter": "stoptype", "values": [1.0, 2.0, 4.0]},
|
||||
{"parameter": "useFriction", "values": [2.0]},
|
||||
{"parameter": "frictionType", "values": [2.0]},
|
||||
{"parameter": "strib", "values": [2.0]},
|
||||
],
|
||||
"xmin": [
|
||||
{"parameter": "stoptype", "values": [1.0, 2.0, 3.0]},
|
||||
],
|
||||
"Kbmin": [
|
||||
{"parameter": "stoptype", "values": [2.0]},
|
||||
],
|
||||
"Dbmin": [
|
||||
{"parameter": "stoptype", "values": [2.0]},
|
||||
],
|
||||
"Pdmin": [
|
||||
{"parameter": "stoptype", "values": [2.0]},
|
||||
],
|
||||
"xmax": [
|
||||
{"parameter": "stoptype", "values": [1.0, 2.0, 3.0]},
|
||||
],
|
||||
"Kbmax": [
|
||||
{"parameter": "stoptype", "values": [2.0]},
|
||||
],
|
||||
"Dbmax": [
|
||||
{"parameter": "stoptype", "values": [2.0]},
|
||||
],
|
||||
"Pdmax": [
|
||||
{"parameter": "stoptype", "values": [2.0]},
|
||||
],
|
||||
"discContactOption": [
|
||||
{"parameter": "stoptype", "values": [2.0]},
|
||||
],
|
||||
"strib": [
|
||||
{"parameter": "stoptype", "values": [1.0, 2.0, 4.0]},
|
||||
{"parameter": "useFriction", "values": [2.0]},
|
||||
{"parameter": "frictionType", "values": [2.0]},
|
||||
],
|
||||
"frictionType": [
|
||||
{"parameter": "stoptype", "values": [1.0, 2.0, 4.0]},
|
||||
{"parameter": "useFriction", "values": [2.0]},
|
||||
],
|
||||
},
|
||||
)
|
||||
for name in ("mass", "theta", "useFriction", "stoptype", "v0", "x0"):
|
||||
with self.subTest(always_visible=name):
|
||||
self.assertNotIn("visibleWhen", mecmas_parameters[name])
|
||||
lstp_parameters = {
|
||||
parameter["name"]: parameter
|
||||
for parameter in components["amesim_lstp00a"]["parameters"]
|
||||
@@ -317,6 +502,7 @@ class ComponentCatalogTests(unittest.TestCase):
|
||||
self.assertIn("modelVersion", schema["$defs"]["component"]["required"])
|
||||
self.assertIn("version", schema["$defs"]["library"]["required"])
|
||||
parameter_schema = schema["$defs"]["parameter"]
|
||||
parameter_group_schema = schema["$defs"]["parameterGroup"]
|
||||
self.assertEqual(
|
||||
parameter_schema["properties"]["description"],
|
||||
{"type": "string", "minLength": 1},
|
||||
@@ -324,6 +510,21 @@ class ComponentCatalogTests(unittest.TestCase):
|
||||
self.assertNotIn("description", parameter_schema["required"])
|
||||
self.assertFalse(parameter_schema["additionalProperties"])
|
||||
self.assertNotIn("unknownField", parameter_schema["properties"])
|
||||
self.assertEqual(
|
||||
parameter_group_schema["required"],
|
||||
["id", "label", "parameters", "order", "defaultExpanded"],
|
||||
)
|
||||
self.assertTrue(
|
||||
parameter_group_schema["properties"]["parameters"]["uniqueItems"]
|
||||
)
|
||||
self.assertIn(
|
||||
"parameterGroups",
|
||||
schema["$defs"]["component"]["properties"],
|
||||
)
|
||||
self.assertNotIn(
|
||||
"parameterGroups",
|
||||
schema["$defs"]["component"]["required"],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -5,8 +5,16 @@ import unittest
|
||||
from app.simulation.components.experimental.library import LIBRARY
|
||||
from app.simulation.components.amesim.library import LIBRARY as AMESIM_LIBRARY
|
||||
from app.simulation.core.base import AlgebraicComponent
|
||||
from app.simulation.core.catalog import ComponentDisplaySpec, PortDisplaySpec
|
||||
from app.simulation.core.metadata import ParameterDefinition, ParameterOption
|
||||
from app.simulation.core.catalog import (
|
||||
ComponentDisplaySpec,
|
||||
ParameterGroupDisplaySpec,
|
||||
PortDisplaySpec,
|
||||
)
|
||||
from app.simulation.core.metadata import (
|
||||
ParameterCondition,
|
||||
ParameterDefinition,
|
||||
ParameterOption,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition
|
||||
from app.simulation.registry import (
|
||||
@@ -144,6 +152,166 @@ class ComponentRegistryTests(unittest.TestCase):
|
||||
library=LIBRARY,
|
||||
)
|
||||
|
||||
def test_parameter_groups_are_strictly_validated(self) -> None:
|
||||
parameters = (
|
||||
ParameterDefinition("mode", 1.0, label="模式"),
|
||||
ParameterDefinition("gain", 2.0, label="增益"),
|
||||
ParameterDefinition("offset", 0.0, label="偏置"),
|
||||
)
|
||||
|
||||
def validate_groups(parameter_groups: object) -> None:
|
||||
class GroupedComponent(AlgebraicComponent):
|
||||
MODEL_TYPE = "grouped_component"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = ()
|
||||
PARAMETERS = parameters
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="参数分组测试组件",
|
||||
library_id="experimental",
|
||||
category_id="flow",
|
||||
symbol="generic",
|
||||
ports=(),
|
||||
parameter_groups=parameter_groups, # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, *, name, medium, parameters):
|
||||
del medium, parameters
|
||||
return cls(name)
|
||||
|
||||
validate_component_model_class(GroupedComponent, library=LIBRARY)
|
||||
|
||||
valid_groups = (
|
||||
ParameterGroupDisplaySpec(
|
||||
id="advanced",
|
||||
label="高级参数",
|
||||
parameters=("mode", "gain"),
|
||||
order=10,
|
||||
),
|
||||
)
|
||||
validate_groups(valid_groups)
|
||||
self.assertEqual(
|
||||
valid_groups[0].as_catalog_dict(),
|
||||
{
|
||||
"id": "advanced",
|
||||
"label": "高级参数",
|
||||
"parameters": ["mode", "gain"],
|
||||
"order": 10,
|
||||
"defaultExpanded": False,
|
||||
},
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "must be a tuple"):
|
||||
validate_groups(list(valid_groups))
|
||||
with self.assertRaisesRegex(ValueError, "ParameterGroupDisplaySpec"):
|
||||
validate_groups((object(),))
|
||||
with self.assertRaisesRegex(ValueError, "must start with a lowercase"):
|
||||
validate_groups(
|
||||
(
|
||||
ParameterGroupDisplaySpec(
|
||||
id="Advanced",
|
||||
label="高级参数",
|
||||
parameters=("mode",),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "label must not be empty"):
|
||||
validate_groups(
|
||||
(
|
||||
ParameterGroupDisplaySpec(
|
||||
id="advanced",
|
||||
label=" ",
|
||||
parameters=("mode",),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "order must be an integer"):
|
||||
validate_groups(
|
||||
(
|
||||
ParameterGroupDisplaySpec(
|
||||
id="advanced",
|
||||
label="高级参数",
|
||||
parameters=("mode",),
|
||||
order=1.5, # type: ignore[arg-type]
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "must be a boolean"):
|
||||
validate_groups(
|
||||
(
|
||||
ParameterGroupDisplaySpec(
|
||||
id="advanced",
|
||||
label="高级参数",
|
||||
parameters=("mode",),
|
||||
default_expanded=1, # type: ignore[arg-type]
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "non-empty tuple"):
|
||||
validate_groups(
|
||||
(
|
||||
ParameterGroupDisplaySpec(
|
||||
id="advanced",
|
||||
label="高级参数",
|
||||
parameters=(),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "unknown parameters: missing"):
|
||||
validate_groups(
|
||||
(
|
||||
ParameterGroupDisplaySpec(
|
||||
id="advanced",
|
||||
label="高级参数",
|
||||
parameters=("missing",),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "contains duplicate names: mode"):
|
||||
validate_groups(
|
||||
(
|
||||
ParameterGroupDisplaySpec(
|
||||
id="advanced",
|
||||
label="高级参数",
|
||||
parameters=("mode", "mode"),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "contains duplicate names: advanced"):
|
||||
validate_groups(
|
||||
(
|
||||
ParameterGroupDisplaySpec(
|
||||
id="advanced",
|
||||
label="高级参数",
|
||||
parameters=("mode",),
|
||||
),
|
||||
ParameterGroupDisplaySpec(
|
||||
id="advanced",
|
||||
label="其他参数",
|
||||
parameters=("gain",),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"assign parameters to multiple groups: mode",
|
||||
):
|
||||
validate_groups(
|
||||
(
|
||||
ParameterGroupDisplaySpec(
|
||||
id="advanced",
|
||||
label="高级参数",
|
||||
parameters=("mode", "gain"),
|
||||
),
|
||||
ParameterGroupDisplaySpec(
|
||||
id="secondary",
|
||||
label="其他参数",
|
||||
parameters=("mode", "offset"),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def test_property_model_parameter_options_are_strictly_validated(self) -> None:
|
||||
def validate_parameter(parameter: ParameterDefinition) -> None:
|
||||
class PropertyModelComponent(AlgebraicComponent):
|
||||
@@ -235,6 +403,251 @@ class ComponentRegistryTests(unittest.TestCase):
|
||||
)
|
||||
)
|
||||
|
||||
def test_choice_parameters_and_visibility_conditions_are_strictly_validated(
|
||||
self,
|
||||
) -> None:
|
||||
def validate_parameters(
|
||||
parameters: tuple[ParameterDefinition, ...],
|
||||
) -> None:
|
||||
class ChoiceComponent(AlgebraicComponent):
|
||||
MODEL_TYPE = "choice_component"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = ()
|
||||
PARAMETERS = parameters
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="离散参数测试组件",
|
||||
library_id="experimental",
|
||||
category_id="flow",
|
||||
symbol="generic",
|
||||
ports=(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, *, name, medium, parameters):
|
||||
del medium, parameters
|
||||
return cls(name)
|
||||
|
||||
validate_component_model_class(ChoiceComponent, library=LIBRARY)
|
||||
|
||||
def choice_parameter(
|
||||
name: str,
|
||||
default: float = 0.0,
|
||||
*,
|
||||
visible_when: tuple[ParameterCondition, ...] = (),
|
||||
) -> ParameterDefinition:
|
||||
return ParameterDefinition(
|
||||
name,
|
||||
default,
|
||||
label=name,
|
||||
editor="choice",
|
||||
options=(
|
||||
ParameterOption(0.0, "关闭"),
|
||||
ParameterOption(1.0, "开启"),
|
||||
),
|
||||
visible_when=visible_when,
|
||||
)
|
||||
|
||||
controller = choice_parameter("mode", 1.0)
|
||||
dependent = ParameterDefinition(
|
||||
"gain",
|
||||
2.0,
|
||||
label="增益",
|
||||
visible_when=(ParameterCondition("mode", (0.0, 1.0)),),
|
||||
)
|
||||
validate_parameters((controller, dependent))
|
||||
self.assertEqual(
|
||||
dependent.as_interface_dict()["visibleWhen"],
|
||||
[{"parameter": "mode", "values": [0.0, 1.0]}],
|
||||
)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "choice editor must declare options"):
|
||||
validate_parameters(
|
||||
(
|
||||
ParameterDefinition(
|
||||
"mode",
|
||||
0.0,
|
||||
label="模式",
|
||||
editor="choice",
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"options require the 'choice' or 'amesimGasPropertyModel' editor",
|
||||
):
|
||||
validate_parameters(
|
||||
(
|
||||
ParameterDefinition(
|
||||
"mode",
|
||||
0.0,
|
||||
label="模式",
|
||||
options=(ParameterOption(0.0, "关闭"),),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "unknown parameter 'missing'"):
|
||||
validate_parameters(
|
||||
(
|
||||
controller,
|
||||
ParameterDefinition(
|
||||
"gain",
|
||||
2.0,
|
||||
label="增益",
|
||||
visible_when=(
|
||||
ParameterCondition("missing", (1.0,)),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "cannot reference itself"):
|
||||
validate_parameters(
|
||||
(
|
||||
choice_parameter(
|
||||
"mode",
|
||||
visible_when=(ParameterCondition("mode", (1.0,)),),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"visibility controller 'continuous' must declare options",
|
||||
):
|
||||
validate_parameters(
|
||||
(
|
||||
ParameterDefinition("continuous", 0.0, label="连续量"),
|
||||
ParameterDefinition(
|
||||
"gain",
|
||||
2.0,
|
||||
label="增益",
|
||||
visible_when=(
|
||||
ParameterCondition("continuous", (0.0,)),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(
|
||||
ValueError,
|
||||
"duplicate visibility controller 'mode'",
|
||||
):
|
||||
validate_parameters(
|
||||
(
|
||||
controller,
|
||||
ParameterDefinition(
|
||||
"gain",
|
||||
2.0,
|
||||
label="增益",
|
||||
visible_when=(
|
||||
ParameterCondition("mode", (0.0,)),
|
||||
ParameterCondition("mode", (1.0,)),
|
||||
),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "unsupported: 2"):
|
||||
validate_parameters(
|
||||
(
|
||||
controller,
|
||||
ParameterDefinition(
|
||||
"gain",
|
||||
2.0,
|
||||
label="增益",
|
||||
visible_when=(ParameterCondition("mode", (2.0,)),),
|
||||
),
|
||||
)
|
||||
)
|
||||
with self.assertRaisesRegex(ValueError, "contain a cycle"):
|
||||
validate_parameters(
|
||||
(
|
||||
choice_parameter(
|
||||
"first",
|
||||
visible_when=(ParameterCondition("second", (1.0,)),),
|
||||
),
|
||||
choice_parameter(
|
||||
"second",
|
||||
visible_when=(ParameterCondition("first", (1.0,)),),
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
def test_visibility_condition_value_shape_is_strictly_validated(self) -> None:
|
||||
def validate_condition(condition: object) -> None:
|
||||
class VisibilityComponent(AlgebraicComponent):
|
||||
MODEL_TYPE = "visibility_component"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = ()
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"mode",
|
||||
0.0,
|
||||
label="模式",
|
||||
editor="choice",
|
||||
options=(ParameterOption(0.0, "关闭"),),
|
||||
),
|
||||
ParameterDefinition(
|
||||
"gain",
|
||||
1.0,
|
||||
label="增益",
|
||||
visible_when=(condition,), # type: ignore[arg-type]
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="可见性测试组件",
|
||||
library_id="experimental",
|
||||
category_id="flow",
|
||||
symbol="generic",
|
||||
ports=(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, *, name, medium, parameters):
|
||||
del medium, parameters
|
||||
return cls(name)
|
||||
|
||||
validate_component_model_class(VisibilityComponent, library=LIBRARY)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "must use ParameterCondition"):
|
||||
validate_condition(object())
|
||||
with self.assertRaisesRegex(ValueError, "must use a non-empty tuple"):
|
||||
validate_condition(ParameterCondition("mode", ()))
|
||||
with self.assertRaisesRegex(ValueError, "must be finite numbers"):
|
||||
validate_condition(ParameterCondition("mode", (float("nan"),)))
|
||||
with self.assertRaisesRegex(ValueError, "contains duplicate values"):
|
||||
validate_condition(ParameterCondition("mode", (0.0, 0.0)))
|
||||
|
||||
malformed_visibility = ParameterDefinition(
|
||||
"gain",
|
||||
1.0,
|
||||
label="增益",
|
||||
visible_when=[], # type: ignore[arg-type]
|
||||
)
|
||||
|
||||
class MalformedVisibilityComponent(AlgebraicComponent):
|
||||
MODEL_TYPE = "malformed_visibility_component"
|
||||
MODEL_VERSION = "1.0.0"
|
||||
PORTS = ()
|
||||
PARAMETERS = (malformed_visibility,)
|
||||
RESULT_VARIABLES = ()
|
||||
DISPLAY = ComponentDisplaySpec(
|
||||
label="错误可见性测试组件",
|
||||
library_id="experimental",
|
||||
category_id="flow",
|
||||
symbol="generic",
|
||||
ports=(),
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, *, name, medium, parameters):
|
||||
del medium, parameters
|
||||
return cls(name)
|
||||
|
||||
with self.assertRaisesRegex(ValueError, "visible_when must use a tuple"):
|
||||
validate_component_model_class(
|
||||
MalformedVisibilityComponent,
|
||||
library=LIBRARY,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -143,7 +143,7 @@ class ContactSolverCausalizationTests(unittest.TestCase):
|
||||
"mass",
|
||||
medium,
|
||||
mass=1.0,
|
||||
useFriction=0.0,
|
||||
useFriction=1.0,
|
||||
stoptype=4.0,
|
||||
x0=1.0e9,
|
||||
v0=0.0,
|
||||
@@ -189,7 +189,7 @@ class ContactSolverCausalizationTests(unittest.TestCase):
|
||||
"mass",
|
||||
medium,
|
||||
mass=1.0,
|
||||
useFriction=0.0,
|
||||
useFriction=1.0,
|
||||
stoptype=4.0,
|
||||
x0=-0.08,
|
||||
v0=2.0,
|
||||
|
||||
@@ -113,7 +113,7 @@ def _single_mass_system(
|
||||
"mass",
|
||||
medium,
|
||||
mass=2.0,
|
||||
useFriction=0.0,
|
||||
useFriction=1.0,
|
||||
stoptype=stoptype,
|
||||
x0=x0,
|
||||
v0=0.0,
|
||||
@@ -189,7 +189,7 @@ class MechanicalSolverCausalizationTests(unittest.TestCase):
|
||||
"mass",
|
||||
medium,
|
||||
mass=2.0,
|
||||
useFriction=0.0,
|
||||
useFriction=1.0,
|
||||
x0=1.0e9,
|
||||
)
|
||||
zero = AmesimF000("zero")
|
||||
@@ -214,7 +214,7 @@ class MechanicalSolverCausalizationTests(unittest.TestCase):
|
||||
medium = IdealGasMedium()
|
||||
parameters = {
|
||||
"mass": 2.0,
|
||||
"useFriction": 0.0,
|
||||
"useFriction": 1.0,
|
||||
"stoptype": 2.0,
|
||||
"x0": 0.1,
|
||||
"xmax": 0.0,
|
||||
@@ -249,7 +249,7 @@ class MechanicalSolverCausalizationTests(unittest.TestCase):
|
||||
"large_mass",
|
||||
medium,
|
||||
mass=90_000.0,
|
||||
useFriction=0.0,
|
||||
useFriction=1.0,
|
||||
)
|
||||
zero = AmesimF000("large_zero")
|
||||
local_force = _AnchoredMechanicalForce("local_force", 40.0)
|
||||
@@ -280,7 +280,7 @@ class MechanicalSolverCausalizationTests(unittest.TestCase):
|
||||
"first_mass",
|
||||
medium,
|
||||
mass=2.0,
|
||||
useFriction=0.0,
|
||||
useFriction=1.0,
|
||||
x0=0.25,
|
||||
v0=0.5,
|
||||
)
|
||||
@@ -288,7 +288,7 @@ class MechanicalSolverCausalizationTests(unittest.TestCase):
|
||||
"second_mass",
|
||||
medium,
|
||||
mass=3.0,
|
||||
useFriction=0.0,
|
||||
useFriction=1.0,
|
||||
x0=0.25,
|
||||
v0=0.5,
|
||||
)
|
||||
@@ -549,7 +549,7 @@ class MechanicalSolverCausalizationTests(unittest.TestCase):
|
||||
stoptype=1.0,
|
||||
xmin=-1.0,
|
||||
xmax=0.0,
|
||||
useFriction=0.0,
|
||||
useFriction=1.0,
|
||||
)
|
||||
restitution = AmesimMecmas21(
|
||||
"restitution",
|
||||
@@ -559,7 +559,7 @@ class MechanicalSolverCausalizationTests(unittest.TestCase):
|
||||
xmax=0.0,
|
||||
restdvel=0.1,
|
||||
restcoeff=0.8,
|
||||
useFriction=0.0,
|
||||
useFriction=1.0,
|
||||
)
|
||||
group = MechanicalConstraintGroup((plastic, restitution))
|
||||
|
||||
|
||||
@@ -267,7 +267,7 @@ class MediumReferenceCatalogContractTests(unittest.TestCase):
|
||||
|
||||
self.assertEqual(
|
||||
schema["$defs"]["parameter"]["properties"]["editor"]["enum"],
|
||||
["amesimGasReference", "amesimGasPropertyModel"],
|
||||
["amesimGasReference", "amesimGasPropertyModel", "choice"],
|
||||
)
|
||||
self.assertEqual(
|
||||
schema["$defs"]["parameter"]["properties"]["options"]["items"][
|
||||
|
||||
@@ -35,6 +35,11 @@ class ReactFlowProjectMediumReferenceVersionTests(unittest.TestCase):
|
||||
project = ReactFlowProjectPayload(name="legacy-project")
|
||||
|
||||
self.assertIsNone(project.mediumReferenceVersion)
|
||||
self.assertIsNone(project.amesimParameterEncodingVersion)
|
||||
self.assertNotIn(
|
||||
"amesimParameterEncodingVersion",
|
||||
pydantic_to_jsonable(project),
|
||||
)
|
||||
|
||||
def test_xml_export_adds_default_property_model_to_legacy_medium_node(self) -> None:
|
||||
project = ReactFlowProjectPayload(
|
||||
|
||||
@@ -88,6 +88,7 @@ class SystemXmlProtocolTests(unittest.TestCase):
|
||||
self.assertEqual(root.attrib["schemaVersion"], "2")
|
||||
self.assertEqual(root.attrib["unitSystem"], "SI")
|
||||
self.assertEqual(root.attrib["mediumReferenceVersion"], "1")
|
||||
self.assertEqual(root.attrib["amesimParameterEncodingVersion"], "1")
|
||||
self.assertEqual(
|
||||
[child.tag for child in root],
|
||||
["Simulation", "Components", "Connections"],
|
||||
|
||||
@@ -190,7 +190,7 @@ class TestMqlMechanicalAssemblyTests(unittest.TestCase):
|
||||
self.assertEqual(mass.reset_velocity_threshold_m_s, 1.0e-6)
|
||||
self.assertEqual(mass.rest_coeff, 0.65)
|
||||
self.assertEqual(mass.stribeck_constant_m_s, 1.0e-3)
|
||||
self.assertTrue(mass.use_friction)
|
||||
self.assertFalse(mass.use_friction)
|
||||
self.assertEqual(mass.stop_type, 4)
|
||||
self.assertIn("v1@mass_friction_endstops_10", mass.data_paths)
|
||||
self.assertIn("x1@mass_friction_endstops_10", mass.data_paths)
|
||||
|
||||
Reference in new issue
Block a user