fix: align mechanical dynamic port validation
This commit is contained in:
1 parent
6a064892e2
commit
4e0b9fd8cc
7 files changed
+105
-7
No files matched your search
+1
-1
@@ -1089,7 +1089,7 @@ def build_reactflow_system_xml(project: ReactFlowProjectPayload) -> bytes:
|
|||||||
"modelVersion": node.data.modelVersion or "",
|
"modelVersion": node.data.modelVersion or "",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
for port_definition in component_spec.ports:
|
for port_definition in component_spec.active_ports(parameter_values):
|
||||||
port_key = (node.id, port_definition.name)
|
port_key = (node.id, port_definition.name)
|
||||||
if port_key in port_index:
|
if port_key in port_index:
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
|
|||||||
@@ -1129,14 +1129,29 @@ class AmesimLmechn1(AlgebraicComponent):
|
|||||||
raise ValueError(f"LMECHN1 {integer_name} must be an integer.")
|
raise ValueError(f"LMECHN1 {integer_name} must be an integer.")
|
||||||
return cls(name=name, medium=medium, v1=parameters["v1"], sum=parameters["sum"])
|
return cls(name=name, medium=medium, v1=parameters["v1"], sum=parameters["sum"])
|
||||||
|
|
||||||
@property
|
@classmethod
|
||||||
def active_ports(self) -> tuple[str, ...]:
|
def active_port_definitions_for_parameters(
|
||||||
return tuple(f"port_{index}" for index in range(1, self.v1 + 2))
|
cls,
|
||||||
|
parameters: Mapping[str, float],
|
||||||
|
) -> tuple[PortDefinition, ...]:
|
||||||
|
right_port_count = float(parameters["v1"])
|
||||||
|
if not right_port_count.is_integer():
|
||||||
|
raise ValueError("LMECHN1 v1 must be an integer.")
|
||||||
|
count = int(right_port_count)
|
||||||
|
if count < 1 or count > 20:
|
||||||
|
raise ValueError("LMECHN1 v1 must be between 1 and 20.")
|
||||||
|
return cls.PORTS[: count + 1]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_port_definitions(self) -> tuple[PortDefinition, ...]:
|
def active_port_definitions(self) -> tuple[PortDefinition, ...]:
|
||||||
return self.PORTS[: self.v1 + 1]
|
return self.PORTS[: self.v1 + 1]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def active_ports(self) -> tuple[str, ...]:
|
||||||
|
return tuple(
|
||||||
|
definition.name for definition in self.active_port_definitions
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def reference_port_name(self) -> str:
|
def reference_port_name(self) -> str:
|
||||||
return f"port_{self.v1 + 1}"
|
return f"port_{self.v1 + 1}"
|
||||||
|
|||||||
@@ -44,6 +44,15 @@ class Component(ABC):
|
|||||||
if port.definition is not None
|
if port.definition is not None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def active_port_definitions_for_parameters(
|
||||||
|
cls,
|
||||||
|
parameters: Mapping[str, float],
|
||||||
|
) -> tuple[PortDefinition, ...]:
|
||||||
|
"""Declared ports enabled by one normalized parameter set."""
|
||||||
|
|
||||||
|
return cls.PORTS
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def active_port_definitions(self) -> tuple[PortDefinition, ...]:
|
def active_port_definitions(self) -> tuple[PortDefinition, ...]:
|
||||||
"""Instance ports that participate in execution and result reporting."""
|
"""Instance ports that participate in execution and result reporting."""
|
||||||
|
|||||||
@@ -73,6 +73,16 @@ class ComponentModelSpec:
|
|||||||
def parameter_by_name(self) -> dict[str, ParameterDefinition]:
|
def parameter_by_name(self) -> dict[str, ParameterDefinition]:
|
||||||
return {parameter.name: parameter for parameter in self.parameters}
|
return {parameter.name: parameter for parameter in self.parameters}
|
||||||
|
|
||||||
|
def active_ports(
|
||||||
|
self,
|
||||||
|
values: Mapping[str, float],
|
||||||
|
) -> tuple[PortDefinition, ...]:
|
||||||
|
resolved = {
|
||||||
|
parameter.name: values.get(parameter.name, parameter.default)
|
||||||
|
for parameter in self.parameters
|
||||||
|
}
|
||||||
|
return self.component_class.active_port_definitions_for_parameters(resolved)
|
||||||
|
|
||||||
def as_catalog_dict(self) -> dict[str, object]:
|
def as_catalog_dict(self) -> dict[str, object]:
|
||||||
category = self.library.category_by_id[self.display.category_id]
|
category = self.library.category_by_id[self.display.category_id]
|
||||||
display_ports = self.display.port_by_name
|
display_ports = self.display.port_by_name
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ class SimulationNetwork:
|
|||||||
f"Cannot connect component {endpoint_a.component} to itself."
|
f"Cannot connect component {endpoint_a.component} to itself."
|
||||||
)
|
)
|
||||||
|
|
||||||
first_port = self._port_for(endpoint_a)
|
first_port = self._active_port_for(endpoint_a)
|
||||||
second_port = self._port_for(endpoint_b)
|
second_port = self._active_port_for(endpoint_b)
|
||||||
first_definition = first_port.definition
|
first_definition = first_port.definition
|
||||||
second_definition = second_port.definition
|
second_definition = second_port.definition
|
||||||
if first_definition is None or second_definition is None:
|
if first_definition is None or second_definition is None:
|
||||||
@@ -168,6 +168,20 @@ class SimulationNetwork:
|
|||||||
raise ValueError(f"Unknown component: {endpoint.component}.") from exc
|
raise ValueError(f"Unknown component: {endpoint.component}.") from exc
|
||||||
return component.get_port(endpoint.port)
|
return component.get_port(endpoint.port)
|
||||||
|
|
||||||
|
def _active_port_for(self, endpoint: Endpoint) -> PortState:
|
||||||
|
try:
|
||||||
|
component = self.components[endpoint.component]
|
||||||
|
except KeyError as exc:
|
||||||
|
raise ValueError(f"Unknown component: {endpoint.component}.") from exc
|
||||||
|
active_names = {
|
||||||
|
definition.name for definition in component.active_port_definitions
|
||||||
|
}
|
||||||
|
if endpoint.port not in active_names:
|
||||||
|
raise ValueError(
|
||||||
|
f"Port {endpoint} is not active for the component's current parameters."
|
||||||
|
)
|
||||||
|
return component.get_port(endpoint.port)
|
||||||
|
|
||||||
def connection_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
def connection_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||||
"""Evaluate connector equations that have a direct scalar residual.
|
"""Evaluate connector equations that have a direct scalar residual.
|
||||||
|
|
||||||
|
|||||||
+27
-1
@@ -538,6 +538,21 @@ def _registered_port(
|
|||||||
return None
|
return None
|
||||||
return next((port for port in spec.ports if port.name == port_name), None)
|
return next((port for port in spec.ports if port.name == port_name), None)
|
||||||
|
|
||||||
|
def _active_registered_ports(
|
||||||
|
component: SystemXmlComponent,
|
||||||
|
) -> tuple[PortDefinition, ...]:
|
||||||
|
spec = COMPONENT_MODEL_REGISTRY.get(component.model_type)
|
||||||
|
if spec is None:
|
||||||
|
return ()
|
||||||
|
values = {
|
||||||
|
parameter.name: parameter.value
|
||||||
|
for parameter in component.parameters
|
||||||
|
}
|
||||||
|
try:
|
||||||
|
return spec.active_ports(values)
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
return ()
|
||||||
|
|
||||||
|
|
||||||
def _validate_connections(
|
def _validate_connections(
|
||||||
document: SystemXmlDocument,
|
document: SystemXmlDocument,
|
||||||
@@ -615,6 +630,17 @@ def _validate_connections(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
continue
|
continue
|
||||||
|
if port not in _active_registered_ports(component):
|
||||||
|
issues.append(
|
||||||
|
_semantic_issue(
|
||||||
|
"ENDPOINT_PORT_INACTIVE",
|
||||||
|
f"Connection {connection.id} references inactive port "
|
||||||
|
f"{endpoint.component}.{endpoint.port} for its parameters.",
|
||||||
|
endpoint_path,
|
||||||
|
endpoint.line,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
continue
|
||||||
resolved_endpoints.append((endpoint, port))
|
resolved_endpoints.append((endpoint, port))
|
||||||
referenced_ports.add(endpoint.key)
|
referenced_ports.add(endpoint.key)
|
||||||
if port.kind == "physical":
|
if port.kind == "physical":
|
||||||
@@ -696,7 +722,7 @@ def _validate_connections(
|
|||||||
spec = COMPONENT_MODEL_REGISTRY.get(component.model_type)
|
spec = COMPONENT_MODEL_REGISTRY.get(component.model_type)
|
||||||
if spec is None:
|
if spec is None:
|
||||||
continue
|
continue
|
||||||
for port in spec.ports:
|
for port in _active_registered_ports(component):
|
||||||
if (component.id, port.name) not in referenced_ports:
|
if (component.id, port.name) not in referenced_ports:
|
||||||
issues.append(
|
issues.append(
|
||||||
_semantic_issue(
|
_semantic_issue(
|
||||||
|
|||||||
@@ -427,6 +427,30 @@ class AmesimMechanicalXmlTests(unittest.TestCase):
|
|||||||
self.assertNotIn("node_1.port_21.f", result["series"])
|
self.assertNotIn("node_1.port_21.f", result["series"])
|
||||||
self.assertAlmostEqual(result["series"]["mass_1.a"][0], 5.0)
|
self.assertAlmostEqual(result["series"]["mass_1.a"][0], 5.0)
|
||||||
|
|
||||||
|
def test_force_node_rejects_connection_to_inactive_reserved_port(self) -> None:
|
||||||
|
project = force_node_mass_project(2)
|
||||||
|
edge = next(
|
||||||
|
edge
|
||||||
|
for edge in project.edges
|
||||||
|
if edge.id == "edge-mass"
|
||||||
|
)
|
||||||
|
edge.sourceHandle = "port_9"
|
||||||
|
|
||||||
|
with self.assertRaisesRegex(ValueError, "missing endpoint node_1.port_9"):
|
||||||
|
build_reactflow_system_xml(project)
|
||||||
|
with self.assertRaisesRegex(ValueError, "port_9 is not active"):
|
||||||
|
compile_reactflow_network(project)
|
||||||
|
|
||||||
|
xml = build_reactflow_system_xml(force_node_mass_project(2))
|
||||||
|
root = ET.fromstring(xml)
|
||||||
|
endpoint = root.find("./Connections/Connection[@id='edge-mass']/Endpoint[@component='node_1']")
|
||||||
|
assert endpoint is not None
|
||||||
|
endpoint.set("port", "port_9")
|
||||||
|
report = validate_system_xml_document(ET.tostring(root, encoding="utf-8"))
|
||||||
|
|
||||||
|
self.assertFalse(report.valid)
|
||||||
|
self.assertIn("ENDPOINT_PORT_INACTIVE", {issue.code for issue in report.issues})
|
||||||
|
|
||||||
def test_elastic_contact_project_compiles_and_simulates(self) -> None:
|
def test_elastic_contact_project_compiles_and_simulates(self) -> None:
|
||||||
xml = build_reactflow_system_xml(elastic_contact_project())
|
xml = build_reactflow_system_xml(elastic_contact_project())
|
||||||
report = validate_system_xml_document(xml)
|
report = validate_system_xml_document(xml)
|
||||||
|
|||||||
Reference in new issue
Block a user