规范仿真模型库并完善前端交互
归档仿真模型并补充组件目录、建模规范与校验。 完善控制台、默认节点、视图适配及前端自动化测试。
This commit is contained in:
1 parent
f7f1078911
commit
bbc88a6bbb
71 files changed
+4574
-806
No files matched your search
@@ -0,0 +1,717 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping, Sequence
|
||||
from dataclasses import dataclass
|
||||
from importlib import import_module
|
||||
from math import isfinite
|
||||
import re
|
||||
from typing import cast
|
||||
|
||||
from app.simulation.core.base import Component
|
||||
from app.simulation.core.catalog import (
|
||||
ComponentCategorySpec,
|
||||
ComponentDisplaySpec,
|
||||
ComponentLibrarySpec,
|
||||
PortDisplaySpec,
|
||||
)
|
||||
from app.simulation.core.metadata import (
|
||||
SI_UNIT_BY_QUANTITY,
|
||||
ParameterDefinition,
|
||||
ResultVariableDefinition,
|
||||
)
|
||||
from app.simulation.core.medium import IdealGasMedium
|
||||
from app.simulation.core.ports import PortDefinition, PortVariableDefinition
|
||||
|
||||
|
||||
ParameterSpec = ParameterDefinition
|
||||
|
||||
ENABLED_COMPONENT_LIBRARIES = (
|
||||
"app.simulation.components.experimental.library:LIBRARY",
|
||||
)
|
||||
|
||||
_MACHINE_ID_PATTERN = re.compile(r"[a-z][a-z0-9_]*")
|
||||
_MEMBER_ID_PATTERN = re.compile(r"[A-Za-z][A-Za-z0-9_]*")
|
||||
_SEMANTIC_VERSION_PATTERN = re.compile(r"\d+\.\d+\.\d+")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class ComponentModelSpec:
|
||||
"""Validated registry entry backed by one component implementation class."""
|
||||
|
||||
component_class: type[Component]
|
||||
library: ComponentLibrarySpec
|
||||
|
||||
@property
|
||||
def model_type(self) -> str:
|
||||
return cast(str, self.component_class.MODEL_TYPE)
|
||||
|
||||
@property
|
||||
def model_version(self) -> str:
|
||||
return cast(str, self.component_class.MODEL_VERSION)
|
||||
|
||||
@property
|
||||
def ports(self) -> tuple[PortDefinition, ...]:
|
||||
return self.component_class.PORTS
|
||||
|
||||
@property
|
||||
def parameters(self) -> tuple[ParameterDefinition, ...]:
|
||||
return self.component_class.PARAMETERS
|
||||
|
||||
@property
|
||||
def result_variables(self) -> tuple[ResultVariableDefinition, ...]:
|
||||
return self.component_class.RESULT_VARIABLES
|
||||
|
||||
@property
|
||||
def display(self) -> ComponentDisplaySpec:
|
||||
return cast(ComponentDisplaySpec, self.component_class.DISPLAY)
|
||||
|
||||
@property
|
||||
def parameter_by_name(self) -> dict[str, ParameterDefinition]:
|
||||
return {parameter.name: parameter for parameter in self.parameters}
|
||||
|
||||
def as_catalog_dict(self) -> dict[str, object]:
|
||||
category = self.library.category_by_id[self.display.category_id]
|
||||
display_ports = self.display.port_by_name
|
||||
ports: list[dict[str, object]] = []
|
||||
for port in self.ports:
|
||||
payload = port.as_interface_dict()
|
||||
payload["side"] = display_ports[port.name].side
|
||||
payload["order"] = display_ports[port.name].order
|
||||
ports.append(payload)
|
||||
ports.sort(key=lambda item: (int(item["order"]), str(item["name"])))
|
||||
|
||||
return {
|
||||
"type": self.model_type,
|
||||
"modelType": self.model_type,
|
||||
"modelVersion": self.model_version,
|
||||
"label": self.display.label,
|
||||
"symbol": self.display.symbol,
|
||||
"order": self.display.order,
|
||||
"category": category.as_catalog_dict(),
|
||||
"ports": ports,
|
||||
"parameters": [
|
||||
parameter.as_interface_dict() for parameter in self.parameters
|
||||
],
|
||||
}
|
||||
|
||||
def create(
|
||||
self,
|
||||
name: str,
|
||||
medium: IdealGasMedium,
|
||||
values: Mapping[str, float],
|
||||
) -> Component:
|
||||
unknown = sorted(set(values) - set(self.parameter_by_name))
|
||||
if unknown:
|
||||
raise ValueError(
|
||||
f"Component '{name}' contains unsupported parameters: "
|
||||
+ ", ".join(unknown)
|
||||
+ "."
|
||||
)
|
||||
|
||||
resolved = {
|
||||
parameter.name: values.get(parameter.name, parameter.default)
|
||||
for parameter in self.parameters
|
||||
}
|
||||
for parameter in self.parameters:
|
||||
message = parameter.validation_message(resolved[parameter.name])
|
||||
if message is not None:
|
||||
raise ValueError(
|
||||
f"Parameter '{parameter.name}' on component '{name}' {message}."
|
||||
)
|
||||
|
||||
component = self.component_class.create(
|
||||
name=name,
|
||||
medium=medium,
|
||||
parameters=resolved,
|
||||
)
|
||||
self._validate_instance(component, resolved)
|
||||
return component
|
||||
|
||||
def _validate_instance(
|
||||
self,
|
||||
component: Component,
|
||||
resolved_parameters: Mapping[str, float],
|
||||
) -> None:
|
||||
if not isinstance(component, self.component_class):
|
||||
raise ValueError(
|
||||
f"Component model '{self.model_type}' create() returned "
|
||||
f"{type(component).__name__}, expected {self.component_class.__name__}."
|
||||
)
|
||||
if component.model_type != self.model_type:
|
||||
raise ValueError(
|
||||
f"Component implementation '{self.model_type}' created instance "
|
||||
f"with model type '{component.model_type}'."
|
||||
)
|
||||
if component.port_definitions != self.ports:
|
||||
raise ValueError(
|
||||
f"Component implementation '{self.model_type}' does not match "
|
||||
"its declared ports."
|
||||
)
|
||||
if component.parameter_values != dict(resolved_parameters):
|
||||
raise ValueError(
|
||||
f"Component implementation '{self.model_type}' did not preserve "
|
||||
"its normalized parameters."
|
||||
)
|
||||
|
||||
|
||||
def _load_reference(reference: str) -> object:
|
||||
try:
|
||||
module_name, attribute_name = reference.rsplit(":", maxsplit=1)
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
f"Component reference '{reference}' must use 'module:attribute'."
|
||||
) from exc
|
||||
if not module_name or not attribute_name:
|
||||
raise ValueError(
|
||||
f"Component reference '{reference}' must use 'module:attribute'."
|
||||
)
|
||||
|
||||
try:
|
||||
module = import_module(module_name)
|
||||
except Exception as exc:
|
||||
raise ValueError(
|
||||
f"Could not import component module '{module_name}' from '{reference}'."
|
||||
) from exc
|
||||
try:
|
||||
return getattr(module, attribute_name)
|
||||
except AttributeError as exc:
|
||||
raise ValueError(
|
||||
f"Component module '{module_name}' has no attribute '{attribute_name}'."
|
||||
) from exc
|
||||
|
||||
|
||||
def _validate_machine_id(value: object, *, field: str) -> str:
|
||||
if not isinstance(value, str) or _MACHINE_ID_PATTERN.fullmatch(value) is None:
|
||||
raise ValueError(
|
||||
f"{field} must start with a lowercase letter and contain only "
|
||||
"lowercase letters, digits, and underscores."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _validate_member_id(value: object, *, field: str) -> str:
|
||||
if not isinstance(value, str) or _MEMBER_ID_PATTERN.fullmatch(value) is None:
|
||||
raise ValueError(
|
||||
f"{field} must start with a letter and contain only letters, "
|
||||
"digits, and underscores."
|
||||
)
|
||||
return value
|
||||
|
||||
|
||||
def _validate_version(value: object, *, field: str) -> str:
|
||||
if (
|
||||
not isinstance(value, str)
|
||||
or _SEMANTIC_VERSION_PATTERN.fullmatch(value) is None
|
||||
):
|
||||
raise ValueError(f"{field} must use 'major.minor.patch'.")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_label(value: object, *, field: str) -> str:
|
||||
if not isinstance(value, str) or not value.strip():
|
||||
raise ValueError(f"{field} must not be empty.")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_order(value: object, *, field: str) -> int:
|
||||
if type(value) is not int:
|
||||
raise ValueError(f"{field} must be an integer.")
|
||||
return value
|
||||
|
||||
|
||||
def _validate_quantity_unit(
|
||||
quantity: object,
|
||||
unit: object,
|
||||
*,
|
||||
field: str,
|
||||
) -> None:
|
||||
if not isinstance(quantity, str) or not quantity:
|
||||
raise ValueError(f"{field} quantity must not be empty.")
|
||||
if not isinstance(unit, str):
|
||||
raise ValueError(f"{field} unit must be a string.")
|
||||
try:
|
||||
expected_unit = SI_UNIT_BY_QUANTITY[quantity]
|
||||
except KeyError as exc:
|
||||
raise ValueError(
|
||||
f"{field} uses unsupported quantity '{quantity}'."
|
||||
) from exc
|
||||
if unit != expected_unit:
|
||||
raise ValueError(
|
||||
f"{field} uses unit '{unit}' for quantity '{quantity}', "
|
||||
f"expected SI unit '{expected_unit}'."
|
||||
)
|
||||
|
||||
|
||||
def _validate_unique_names(
|
||||
names: Sequence[str],
|
||||
*,
|
||||
field: str,
|
||||
) -> None:
|
||||
duplicates = sorted({name for name in names if names.count(name) > 1})
|
||||
if duplicates:
|
||||
raise ValueError(f"{field} contains duplicate names: {', '.join(duplicates)}.")
|
||||
|
||||
|
||||
def _validate_category(
|
||||
category: ComponentCategorySpec,
|
||||
*,
|
||||
library_id: str,
|
||||
) -> None:
|
||||
if not isinstance(category, ComponentCategorySpec):
|
||||
raise ValueError(
|
||||
f"Library '{library_id}' categories must use ComponentCategorySpec."
|
||||
)
|
||||
_validate_machine_id(
|
||||
category.id,
|
||||
field=f"Library '{library_id}' category id",
|
||||
)
|
||||
_validate_label(
|
||||
category.label,
|
||||
field=f"Library '{library_id}' category '{category.id}' label",
|
||||
)
|
||||
_validate_order(
|
||||
category.order,
|
||||
field=f"Library '{library_id}' category '{category.id}' order",
|
||||
)
|
||||
|
||||
|
||||
def validate_component_library(library: ComponentLibrarySpec) -> None:
|
||||
if not isinstance(library, ComponentLibrarySpec):
|
||||
raise ValueError("Enabled component libraries must use ComponentLibrarySpec.")
|
||||
library_id = _validate_machine_id(library.id, field="Component library id")
|
||||
_validate_label(library.label, field=f"Library '{library_id}' label")
|
||||
_validate_version(library.version, field=f"Library '{library_id}' version")
|
||||
_validate_order(library.order, field=f"Library '{library_id}' order")
|
||||
if type(library.temporary) is not bool:
|
||||
raise ValueError(f"Library '{library_id}' temporary must be a boolean.")
|
||||
if (
|
||||
not isinstance(library.source_package, str)
|
||||
or not library.source_package.strip()
|
||||
):
|
||||
raise ValueError(f"Library '{library_id}' source package must not be empty.")
|
||||
if not library.categories:
|
||||
raise ValueError(f"Library '{library_id}' must declare at least one category.")
|
||||
if not library.models:
|
||||
raise ValueError(f"Library '{library_id}' must declare at least one model.")
|
||||
|
||||
for category in library.categories:
|
||||
_validate_category(category, library_id=library_id)
|
||||
category_ids = [category.id for category in library.categories]
|
||||
_validate_unique_names(
|
||||
category_ids,
|
||||
field=f"Library '{library_id}' categories",
|
||||
)
|
||||
|
||||
_validate_unique_names(
|
||||
list(library.models),
|
||||
field=f"Library '{library_id}' model references",
|
||||
)
|
||||
for reference in library.models:
|
||||
if not isinstance(reference, str) or ":" not in reference:
|
||||
raise ValueError(
|
||||
f"Library '{library_id}' contains invalid model reference "
|
||||
f"'{reference}'."
|
||||
)
|
||||
module_name = reference.rsplit(":", maxsplit=1)[0]
|
||||
if not (
|
||||
module_name == library.source_package
|
||||
or module_name.startswith(f"{library.source_package}.")
|
||||
):
|
||||
raise ValueError(
|
||||
f"Library '{library_id}' model '{reference}' is outside source "
|
||||
f"package '{library.source_package}'."
|
||||
)
|
||||
|
||||
|
||||
def _validate_port_variable(
|
||||
variable: PortVariableDefinition,
|
||||
*,
|
||||
model_type: str,
|
||||
port_name: str,
|
||||
) -> None:
|
||||
field = f"Component '{model_type}' port '{port_name}' variable"
|
||||
if not isinstance(variable, PortVariableDefinition):
|
||||
raise ValueError(f"{field} must use PortVariableDefinition.")
|
||||
_validate_member_id(variable.name, field=f"{field} name")
|
||||
_validate_label(variable.label or variable.name, field=f"{field} label")
|
||||
_validate_quantity_unit(
|
||||
variable.quantity or variable.name,
|
||||
variable.unit,
|
||||
field=f"{field} '{variable.name}'",
|
||||
)
|
||||
_validate_order(variable.order, field=f"{field} '{variable.name}' order")
|
||||
if variable.role not in {"effort", "flow", "stream", "signal"}:
|
||||
raise ValueError(f"{field} '{variable.name}' has invalid role.")
|
||||
if variable.connection_rule not in {
|
||||
"equal",
|
||||
"sumToZero",
|
||||
"streamMix",
|
||||
"directed",
|
||||
}:
|
||||
raise ValueError(
|
||||
f"{field} '{variable.name}' has invalid connection rule."
|
||||
)
|
||||
if type(variable.result_visible) is not bool:
|
||||
raise ValueError(
|
||||
f"{field} '{variable.name}' result_visible must be a boolean."
|
||||
)
|
||||
|
||||
|
||||
def _validate_port(port: PortDefinition, *, model_type: str) -> None:
|
||||
field = f"Component '{model_type}' port"
|
||||
if not isinstance(port, PortDefinition):
|
||||
raise ValueError(f"{field} declarations must use PortDefinition.")
|
||||
_validate_machine_id(port.name, field=f"{field} name")
|
||||
_validate_machine_id(port.domain, field=f"{field} '{port.name}' domain")
|
||||
if port.kind not in {"physical", "signal"}:
|
||||
raise ValueError(f"{field} '{port.name}' has invalid kind.")
|
||||
if port.nominal_role not in {
|
||||
"inlet",
|
||||
"outlet",
|
||||
"bidirectional",
|
||||
"input",
|
||||
"output",
|
||||
}:
|
||||
raise ValueError(f"{field} '{port.name}' has invalid nominal role.")
|
||||
if port.kind == "physical" and port.positive_flow_direction != "intoComponent":
|
||||
raise ValueError(
|
||||
f"{field} '{port.name}' must use positive flow into the component."
|
||||
)
|
||||
if port.kind == "physical" and port.nominal_role in {"input", "output"}:
|
||||
raise ValueError(
|
||||
f"{field} '{port.name}' uses a signal-only nominal role."
|
||||
)
|
||||
if port.kind == "signal" and port.nominal_role not in {"input", "output"}:
|
||||
raise ValueError(
|
||||
f"{field} '{port.name}' must use input or output as its nominal role."
|
||||
)
|
||||
|
||||
variable_names = [variable.name for variable in port.variables]
|
||||
_validate_unique_names(
|
||||
variable_names,
|
||||
field=f"{field} '{port.name}' variables",
|
||||
)
|
||||
for variable in port.variables:
|
||||
_validate_port_variable(
|
||||
variable,
|
||||
model_type=model_type,
|
||||
port_name=port.name,
|
||||
)
|
||||
|
||||
|
||||
def _validate_parameter(
|
||||
parameter: ParameterDefinition,
|
||||
*,
|
||||
model_type: str,
|
||||
) -> None:
|
||||
field = f"Component '{model_type}' parameter"
|
||||
if not isinstance(parameter, ParameterDefinition):
|
||||
raise ValueError(f"{field} declarations must use ParameterDefinition.")
|
||||
_validate_member_id(parameter.name, field=f"{field} name")
|
||||
_validate_label(parameter.label, field=f"{field} '{parameter.name}' label")
|
||||
_validate_quantity_unit(
|
||||
parameter.quantity,
|
||||
parameter.unit,
|
||||
field=f"{field} '{parameter.name}'",
|
||||
)
|
||||
if not isinstance(parameter.default, (int, float)) or not isfinite(
|
||||
parameter.default
|
||||
):
|
||||
raise ValueError(f"{field} '{parameter.name}' default must be finite.")
|
||||
for boundary_name, boundary in (
|
||||
("minimum", parameter.minimum),
|
||||
("maximum", parameter.maximum),
|
||||
):
|
||||
if boundary is not None and (
|
||||
not isinstance(boundary, (int, float)) or not isfinite(boundary)
|
||||
):
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' {boundary_name} must be finite."
|
||||
)
|
||||
if type(parameter.minimum_exclusive) is not bool:
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' minimum_exclusive must be a boolean."
|
||||
)
|
||||
if (
|
||||
parameter.minimum is not None
|
||||
and parameter.maximum is not None
|
||||
and parameter.minimum > parameter.maximum
|
||||
):
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' minimum exceeds its maximum."
|
||||
)
|
||||
message = parameter.validation_message(parameter.default)
|
||||
if message is not None:
|
||||
raise ValueError(
|
||||
f"{field} '{parameter.name}' default value {message}."
|
||||
)
|
||||
|
||||
|
||||
def _validate_result_variable(
|
||||
variable: ResultVariableDefinition,
|
||||
*,
|
||||
model_type: str,
|
||||
) -> None:
|
||||
field = f"Component '{model_type}' result variable"
|
||||
if not isinstance(variable, ResultVariableDefinition):
|
||||
raise ValueError(
|
||||
f"{field} declarations must use ResultVariableDefinition."
|
||||
)
|
||||
_validate_member_id(variable.name, field=f"{field} name")
|
||||
_validate_label(variable.label, field=f"{field} '{variable.name}' label")
|
||||
_validate_quantity_unit(
|
||||
variable.quantity,
|
||||
variable.unit,
|
||||
field=f"{field} '{variable.name}'",
|
||||
)
|
||||
_validate_machine_id(
|
||||
variable.category,
|
||||
field=f"{field} '{variable.name}' category",
|
||||
)
|
||||
_validate_order(variable.order, field=f"{field} '{variable.name}' order")
|
||||
if type(variable.visible) is not bool:
|
||||
raise ValueError(
|
||||
f"{field} '{variable.name}' visible must be a boolean."
|
||||
)
|
||||
|
||||
|
||||
def validate_component_model_class(
|
||||
component_class: type[Component],
|
||||
*,
|
||||
library: ComponentLibrarySpec,
|
||||
) -> None:
|
||||
if not isinstance(component_class, type) or not issubclass(
|
||||
component_class,
|
||||
Component,
|
||||
):
|
||||
raise ValueError(
|
||||
f"Library '{library.id}' model entries must be Component subclasses."
|
||||
)
|
||||
|
||||
required_declarations = (
|
||||
"MODEL_TYPE",
|
||||
"MODEL_VERSION",
|
||||
"PORTS",
|
||||
"PARAMETERS",
|
||||
"RESULT_VARIABLES",
|
||||
"DISPLAY",
|
||||
"create",
|
||||
)
|
||||
missing = [
|
||||
name for name in required_declarations if name not in component_class.__dict__
|
||||
]
|
||||
if missing:
|
||||
raise ValueError(
|
||||
f"Component class '{component_class.__name__}' must declare: "
|
||||
+ ", ".join(missing)
|
||||
+ "."
|
||||
)
|
||||
if not isinstance(component_class.__dict__["create"], classmethod):
|
||||
raise ValueError(
|
||||
f"Component class '{component_class.__name__}' create must be a classmethod."
|
||||
)
|
||||
|
||||
model_type = _validate_machine_id(
|
||||
component_class.MODEL_TYPE,
|
||||
field=f"Component class '{component_class.__name__}' model type",
|
||||
)
|
||||
_validate_version(
|
||||
component_class.MODEL_VERSION,
|
||||
field=f"Component '{model_type}' model version",
|
||||
)
|
||||
|
||||
display = component_class.DISPLAY
|
||||
if not isinstance(display, ComponentDisplaySpec):
|
||||
raise ValueError(
|
||||
f"Component '{model_type}' DISPLAY must use ComponentDisplaySpec."
|
||||
)
|
||||
_validate_label(display.label, field=f"Component '{model_type}' display label")
|
||||
_validate_machine_id(
|
||||
display.symbol,
|
||||
field=f"Component '{model_type}' display symbol",
|
||||
)
|
||||
_validate_order(display.order, field=f"Component '{model_type}' display order")
|
||||
if not isinstance(display.ports, tuple):
|
||||
raise ValueError(f"Component '{model_type}' DISPLAY ports must be a tuple.")
|
||||
if display.library_id != library.id:
|
||||
raise ValueError(
|
||||
f"Component '{model_type}' references library '{display.library_id}', "
|
||||
f"expected '{library.id}'."
|
||||
)
|
||||
if display.category_id not in library.category_by_id:
|
||||
raise ValueError(
|
||||
f"Component '{model_type}' references unknown category "
|
||||
f"'{display.category_id}' in library '{library.id}'."
|
||||
)
|
||||
|
||||
ports = component_class.PORTS
|
||||
parameters = component_class.PARAMETERS
|
||||
result_variables = component_class.RESULT_VARIABLES
|
||||
if not isinstance(ports, tuple):
|
||||
raise ValueError(f"Component '{model_type}' PORTS must be a tuple.")
|
||||
if not isinstance(parameters, tuple):
|
||||
raise ValueError(f"Component '{model_type}' PARAMETERS must be a tuple.")
|
||||
if not isinstance(result_variables, tuple):
|
||||
raise ValueError(
|
||||
f"Component '{model_type}' RESULT_VARIABLES must be a tuple."
|
||||
)
|
||||
|
||||
for port in ports:
|
||||
_validate_port(port, model_type=model_type)
|
||||
for parameter in parameters:
|
||||
_validate_parameter(parameter, model_type=model_type)
|
||||
for variable in result_variables:
|
||||
_validate_result_variable(variable, model_type=model_type)
|
||||
for port in display.ports:
|
||||
if not isinstance(port, PortDisplaySpec):
|
||||
raise ValueError(
|
||||
f"Component '{model_type}' display ports must use PortDisplaySpec."
|
||||
)
|
||||
_validate_machine_id(
|
||||
port.name,
|
||||
field=f"Component '{model_type}' display port name",
|
||||
)
|
||||
if port.side not in {"left", "right"}:
|
||||
raise ValueError(
|
||||
f"Component '{model_type}' display port '{port.name}' "
|
||||
"must use side 'left' or 'right'."
|
||||
)
|
||||
_validate_order(
|
||||
port.order,
|
||||
field=f"Component '{model_type}' display port '{port.name}' order",
|
||||
)
|
||||
|
||||
port_names = [port.name for port in ports]
|
||||
parameter_names = [parameter.name for parameter in parameters]
|
||||
result_names = [variable.name for variable in result_variables]
|
||||
display_port_names = [port.name for port in display.ports]
|
||||
_validate_unique_names(port_names, field=f"Component '{model_type}' ports")
|
||||
_validate_unique_names(
|
||||
parameter_names,
|
||||
field=f"Component '{model_type}' parameters",
|
||||
)
|
||||
_validate_unique_names(
|
||||
result_names,
|
||||
field=f"Component '{model_type}' result variables",
|
||||
)
|
||||
_validate_unique_names(
|
||||
display_port_names,
|
||||
field=f"Component '{model_type}' display ports",
|
||||
)
|
||||
if set(port_names) != set(display_port_names):
|
||||
raise ValueError(
|
||||
f"Component '{model_type}' display ports must exactly match "
|
||||
"its physical port declarations."
|
||||
)
|
||||
|
||||
|
||||
def discover_component_registries(
|
||||
library_references: Sequence[str] = ENABLED_COMPONENT_LIBRARIES,
|
||||
) -> tuple[
|
||||
dict[str, ComponentLibrarySpec],
|
||||
dict[str, ComponentModelSpec],
|
||||
]:
|
||||
libraries: dict[str, ComponentLibrarySpec] = {}
|
||||
models: dict[str, ComponentModelSpec] = {}
|
||||
|
||||
for library_reference in library_references:
|
||||
library = _load_reference(library_reference)
|
||||
if not isinstance(library, ComponentLibrarySpec):
|
||||
raise ValueError(
|
||||
f"Enabled library '{library_reference}' must reference "
|
||||
"ComponentLibrarySpec."
|
||||
)
|
||||
validate_component_library(library)
|
||||
if library.id in libraries:
|
||||
raise ValueError(f"Duplicate component library id: {library.id}.")
|
||||
libraries[library.id] = library
|
||||
|
||||
for model_reference in library.models:
|
||||
component_class = _load_reference(model_reference)
|
||||
if not isinstance(component_class, type) or not issubclass(
|
||||
component_class,
|
||||
Component,
|
||||
):
|
||||
raise ValueError(
|
||||
f"Library '{library.id}' model '{model_reference}' is not "
|
||||
"a Component subclass."
|
||||
)
|
||||
validate_component_model_class(component_class, library=library)
|
||||
model_type = cast(str, component_class.MODEL_TYPE)
|
||||
if model_type in models:
|
||||
existing = models[model_type].component_class
|
||||
raise ValueError(
|
||||
f"Duplicate component model type '{model_type}' from "
|
||||
f"{existing.__module__}.{existing.__name__} and "
|
||||
f"{component_class.__module__}.{component_class.__name__}."
|
||||
)
|
||||
spec = ComponentModelSpec(
|
||||
component_class=component_class,
|
||||
library=library,
|
||||
)
|
||||
try:
|
||||
spec.create(
|
||||
f"__catalog_validation_{model_type}",
|
||||
IdealGasMedium(),
|
||||
{},
|
||||
)
|
||||
except Exception as exc:
|
||||
raise ValueError(
|
||||
f"Component model '{model_type}' cannot be created with "
|
||||
"its declared defaults."
|
||||
) from exc
|
||||
models[model_type] = spec
|
||||
|
||||
if not libraries:
|
||||
raise ValueError("At least one component library must be enabled.")
|
||||
return libraries, models
|
||||
|
||||
|
||||
COMPONENT_LIBRARY_REGISTRY, COMPONENT_MODEL_REGISTRY = (
|
||||
discover_component_registries()
|
||||
)
|
||||
|
||||
|
||||
def validate_component_registries() -> None:
|
||||
"""Re-run public registry invariants for tests and startup diagnostics."""
|
||||
|
||||
discovered_libraries, discovered_models = discover_component_registries()
|
||||
if tuple(discovered_libraries) != tuple(COMPONENT_LIBRARY_REGISTRY):
|
||||
raise ValueError("Component library registry differs from discovery output.")
|
||||
if tuple(discovered_models) != tuple(COMPONENT_MODEL_REGISTRY):
|
||||
raise ValueError("Component model registry differs from discovery output.")
|
||||
|
||||
|
||||
def build_component_catalog() -> dict[str, object]:
|
||||
components_by_library: dict[str, list[ComponentModelSpec]] = {
|
||||
library_id: [] for library_id in COMPONENT_LIBRARY_REGISTRY
|
||||
}
|
||||
for component in COMPONENT_MODEL_REGISTRY.values():
|
||||
components_by_library[component.library.id].append(component)
|
||||
|
||||
libraries: list[dict[str, object]] = []
|
||||
for library in sorted(
|
||||
COMPONENT_LIBRARY_REGISTRY.values(),
|
||||
key=lambda item: (item.order, item.id),
|
||||
):
|
||||
payload = library.as_catalog_dict()
|
||||
payload["components"] = [
|
||||
component.as_catalog_dict()
|
||||
for component in sorted(
|
||||
components_by_library[library.id],
|
||||
key=lambda item: (item.display.order, item.model_type),
|
||||
)
|
||||
]
|
||||
libraries.append(payload)
|
||||
|
||||
return {
|
||||
"schemaVersion": 1,
|
||||
"libraries": libraries,
|
||||
}
|
||||
|
||||
|
||||
def get_component_model_spec(model_type: str) -> ComponentModelSpec:
|
||||
try:
|
||||
return COMPONENT_MODEL_REGISTRY[model_type]
|
||||
except KeyError as exc:
|
||||
raise ValueError(f"Unsupported model type: {model_type}.") from exc
|
||||
Reference in new issue
Block a user