119 lines
4.4 KiB
Python
119 lines
4.4 KiB
Python
from __future__ import annotations
|
|
|
|
import re
|
|
import tarfile
|
|
import xml.etree.ElementTree as ET
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TestMqlComponentContact:
|
|
component_a: str
|
|
port_a: str
|
|
component_b: str
|
|
port_b: str
|
|
|
|
def other_endpoint(self, component_alias: str) -> tuple[str, str]:
|
|
if component_alias == self.component_a:
|
|
return self.component_b, self.port_b
|
|
if component_alias == self.component_b:
|
|
return self.component_a, self.port_a
|
|
raise KeyError(component_alias)
|
|
|
|
def port_for(self, component_alias: str) -> str:
|
|
if component_alias == self.component_a:
|
|
return self.port_a
|
|
if component_alias == self.component_b:
|
|
return self.port_b
|
|
raise KeyError(component_alias)
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class TestMqlCirTopology:
|
|
component_contacts: tuple[TestMqlComponentContact, ...]
|
|
|
|
def contacts_for(self, component_alias: str) -> tuple[TestMqlComponentContact, ...]:
|
|
return tuple(
|
|
contact
|
|
for contact in self.component_contacts
|
|
if component_alias in (contact.component_a, contact.component_b)
|
|
)
|
|
|
|
|
|
def load_test_mql_cir_topology(
|
|
archive_path: str | Path,
|
|
*,
|
|
cir_member: str = "test_mql_.cir",
|
|
) -> TestMqlCirTopology:
|
|
with tarfile.open(archive_path) as archive:
|
|
cir_file = archive.extractfile(cir_member)
|
|
if cir_file is None:
|
|
raise ValueError(f"Missing AMESim circuit member: {cir_member}")
|
|
cir_text = cir_file.read().decode("latin1")
|
|
|
|
root = ET.fromstring(_topology_only_xml(cir_text))
|
|
components = root.findall(".//COMPS_LIST/COMP")
|
|
aliases = tuple(_required_text(component, "ALIAS") for component in components)
|
|
contacts: dict[
|
|
tuple[tuple[int, int], tuple[int, int]],
|
|
TestMqlComponentContact,
|
|
] = {}
|
|
directed_contacts: set[tuple[tuple[int, int], tuple[int, int]]] = set()
|
|
|
|
for component_index, component in enumerate(components):
|
|
ports = component.findall("./COMP_PORTS_LIST/COMP_PORT")
|
|
for port_index, port in enumerate(ports):
|
|
if port.findtext("PORT_CONNECT") != "1":
|
|
continue
|
|
for connection in port.findall("./CONNECT_LIST/CONNECT"):
|
|
target_index = int(_required_text(connection, "CONNECT_ENTITY_NUM"))
|
|
target_port_index = int(_required_text(connection, "CONNECT_ENTITY_PORT"))
|
|
if target_index < 0 or target_index >= len(components):
|
|
raise ValueError(f"Component contact references unknown entity {target_index}")
|
|
target_ports = components[target_index].findall("./COMP_PORTS_LIST/COMP_PORT")
|
|
if target_port_index < 0 or target_port_index >= len(target_ports):
|
|
raise ValueError(
|
|
f"Component contact references unknown port {target_port_index} "
|
|
f"on {aliases[target_index]}"
|
|
)
|
|
|
|
endpoint = (component_index, port_index)
|
|
target_endpoint = (target_index, target_port_index)
|
|
directed_contacts.add((endpoint, target_endpoint))
|
|
key = tuple(sorted((endpoint, target_endpoint)))
|
|
first, second = key
|
|
contacts[key] = TestMqlComponentContact(
|
|
component_a=aliases[first[0]],
|
|
port_a=f"port_{first[1] + 1}",
|
|
component_b=aliases[second[0]],
|
|
port_b=f"port_{second[1] + 1}",
|
|
)
|
|
|
|
for endpoint, target_endpoint in directed_contacts:
|
|
if (target_endpoint, endpoint) not in directed_contacts:
|
|
raise ValueError(
|
|
"AMESim component contact is not reciprocal: "
|
|
f"{endpoint} -> {target_endpoint}"
|
|
)
|
|
|
|
return TestMqlCirTopology(component_contacts=tuple(contacts.values()))
|
|
|
|
|
|
def _topology_only_xml(cir_text: str) -> str:
|
|
# AMESim expressions inside SUBMODEL contain unescaped && and <= operators.
|
|
# Topology lives outside those blocks, so omit them before XML parsing.
|
|
return re.sub(
|
|
r"<SUBMODEL>.*?</SUBMODEL>",
|
|
"<SUBMODEL />",
|
|
cir_text,
|
|
flags=re.DOTALL,
|
|
)
|
|
|
|
|
|
def _required_text(element: ET.Element, child_name: str) -> str:
|
|
value = element.findtext(child_name)
|
|
if value is None:
|
|
raise ValueError(f"Missing AMESim circuit element: {child_name}")
|
|
return value
|