Replace Python numerical kernels with native C execution

This commit is contained in:
ljz committed 2026-09-10 01:12:18 +08:00
1 parent 48da6be21c
commit 3b38f73fe0
227 files changed
+16801 -75499

No files matched your search

+28 -267
View File
@@ -1,40 +1,17 @@
from __future__ import annotations
from abc import ABC, abstractmethod
from collections.abc import Callable, Mapping
from typing import TYPE_CHECKING, Any, ClassVar
from abc import ABC
from collections.abc import Mapping
from typing import TYPE_CHECKING, ClassVar
from app.simulation.core.catalog import ComponentDisplaySpec
from app.simulation.core.equations import EquationResidual
from app.simulation.core.metadata import (
ParameterDefinition,
ResultVariableDefinition,
ResultVariableMetadata,
THERMODYNAMIC_VOLUME_RESULT_VARIABLES,
)
from app.simulation.core.equations import EquationDefinition
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition, ResultVariableMetadata, THERMODYNAMIC_VOLUME_RESULT_VARIABLES
from app.simulation.core.ports import PortDefinition, PortState
if TYPE_CHECKING:
from app.simulation.core.medium import GasMedium
class Component(ABC):
MODEL_TYPE: ClassVar[str | None] = None
MODEL_VERSION: ClassVar[str | None] = None
# ``True`` means that pressure/flow residuals read values written by
# ``update_stream_outflows`` or ``update_flow_temperature_references``.
# ``False`` is an explicit promise that those residuals are independent of
# stream propagation. ``None`` keeps custom components conservative: when
# they override either stream hook, the closure planner retains the legacy
# full-network thermofluid fixed point.
PRESSURE_FLOW_DEPENDS_ON_STREAM: ClassVar[bool | None] = None
# Exact residual suffixes whose declared variables are summed, in order,
# to form a ``sumToZero`` flow equation. The causal solver deliberately
# reads this capability from the concrete class ``__dict__``: subclasses
# must repeat the promise after changing any equation semantics.
PRESSURE_FLOW_EXACT_SUM_TO_ZERO_EQUATION_SUFFIXES: ClassVar[
frozenset[str]
] = frozenset()
PORTS: ClassVar[tuple[PortDefinition, ...]] = ()
PARAMETERS: ClassVar[tuple[ParameterDefinition, ...]] = ()
RESULT_VARIABLES: ClassVar[tuple[ResultVariableDefinition, ...]] = ()
@@ -52,80 +29,53 @@ class Component(ABC):
@property
def port_definitions(self) -> tuple[PortDefinition, ...]:
return tuple(
port.definition
for port in self._ports.values()
if port.definition is not None
)
return tuple((port.definition for port in self._ports.values() if port.definition is not None))
@classmethod
def active_port_definitions_for_parameters(
cls,
parameters: Mapping[str, float],
) -> tuple[PortDefinition, ...]:
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
def active_port_definitions(self) -> tuple[PortDefinition, ...]:
"""Instance ports that participate in execution and result reporting."""
return self.port_definitions
@property
def required_connection_ports(self) -> tuple[str, ...]:
"""Physical ports that must have an external connection before simulation."""
return tuple(
definition.name
for definition in self.active_port_definitions
if definition.kind == "physical"
)
return tuple((definition.name for definition in self.active_port_definitions if definition.kind == 'physical'))
def register_port(self, port: PortState) -> PortState:
definition = port.definition
if definition is None:
raise ValueError(f"Component {self.name} cannot register an undefined port.")
raise ValueError(f'Component {self.name} cannot register an undefined port.')
if definition.name in self._ports:
raise ValueError(f"Duplicate port {self.name}.{definition.name}.")
raise ValueError(f'Duplicate port {self.name}.{definition.name}.')
self._ports[definition.name] = port
return port
def register_declared_port(self, name: str) -> PortState:
try:
definition = next(item for item in self.PORTS if item.name == name)
definition = next((item for item in self.PORTS if item.name == name))
except StopIteration as exc:
raise ValueError(
f"Component model {self.model_type} does not declare port {name}."
) from exc
raise ValueError(f'Component model {self.model_type} does not declare port {name}.') from exc
return self.register_port(PortState(definition=definition))
def set_parameter_values(self, values: Mapping[str, float]) -> None:
definitions = {definition.name: definition for definition in self.PARAMETERS}
unknown = sorted(set(values) - set(definitions))
if unknown:
raise ValueError(
f"Component {self.name} contains unsupported parameters: "
+ ", ".join(unknown)
+ "."
)
raise ValueError(f'Component {self.name} contains unsupported parameters: ' + ', '.join(unknown) + '.')
missing = sorted(set(definitions) - set(values))
if missing:
raise ValueError(
f"Component {self.name} is missing parameters: "
+ ", ".join(missing)
+ "."
)
raise ValueError(f'Component {self.name} is missing parameters: ' + ', '.join(missing) + '.')
resolved: dict[str, float] = {}
for name, definition in definitions.items():
value = float(values[name])
message = definition.validation_message(value)
if message is not None:
raise ValueError(
f"Parameter '{name}' on component '{self.name}' {message}."
)
raise ValueError(f"Parameter '{name}' on component '{self.name}' {message}.")
resolved[name] = value
self._parameter_values = resolved
@@ -137,229 +87,40 @@ class Component(ABC):
try:
return self._ports[name]
except KeyError as exc:
raise ValueError(f"Component {self.name} has no port named {name}.") from exc
def component_result_values(self) -> Mapping[str, float]:
return {}
def result_values(self) -> dict[str, float]:
component_values = dict(self.component_result_values())
declared = {definition.name: definition for definition in self.RESULT_VARIABLES}
unknown = sorted(set(component_values) - set(declared))
if unknown:
raise ValueError(
f"Component {self.name} returned undeclared result variables: "
+ ", ".join(unknown)
+ "."
)
values: dict[str, float] = {}
for name, definition in declared.items():
if not definition.visible:
continue
if name not in component_values:
raise ValueError(
f"Component {self.name} did not provide declared result variable {name}."
)
values[name] = float(component_values[name])
for port_definition in self.active_port_definitions:
port = self.get_port(port_definition.name)
for variable in port_definition.variables:
if not variable.result_visible:
continue
values[f"{port_definition.name}.{variable.name}"] = float(
getattr(port, variable.name)
)
return values
raise ValueError(f'Component {self.name} has no port named {name}.') from exc
def result_variable_metadata(self) -> tuple[ResultVariableMetadata, ...]:
metadata = [
ResultVariableMetadata(
key=f"{self.name}.{definition.name}",
component_id=self.name,
component_type=self.model_type,
scope="component",
name=definition.name,
label=definition.label,
quantity=definition.quantity,
unit=definition.unit,
category=definition.category,
order=definition.order,
)
for definition in self.RESULT_VARIABLES
if definition.visible
]
metadata = [ResultVariableMetadata(key=f'{self.name}.{definition.name}', component_id=self.name, component_type=self.model_type, scope='component', name=definition.name, label=definition.label, quantity=definition.quantity, unit=definition.unit, category=definition.category, order=definition.order) for definition in self.RESULT_VARIABLES if definition.visible]
for port_definition in self.active_port_definitions:
for variable in port_definition.variables:
if not variable.result_visible:
continue
metadata.append(
ResultVariableMetadata(
key=f"{self.name}.{port_definition.name}.{variable.name}",
component_id=self.name,
component_type=self.model_type,
scope="port",
port_name=port_definition.name,
name=variable.name,
label=variable.label or variable.name,
quantity=variable.quantity or variable.name,
unit=variable.unit,
category=variable.role,
order=variable.order,
)
)
metadata.append(ResultVariableMetadata(key=f'{self.name}.{port_definition.name}.{variable.name}', component_id=self.name, component_type=self.model_type, scope='port', port_name=port_definition.name, name=variable.name, label=variable.label or variable.name, quantity=variable.quantity or variable.name, unit=variable.unit, category=variable.role, order=variable.order))
return tuple(metadata)
def parameter_interface_dicts(self) -> list[dict[str, object]]:
return [
definition.as_interface_dict(
value=self._parameter_values.get(definition.name)
)
for definition in self.PARAMETERS
]
return [definition.as_interface_dict(value=self._parameter_values.get(definition.name)) for definition in self.PARAMETERS]
@classmethod
def create(
cls,
*,
name: str,
medium: GasMedium,
parameters: Mapping[str, float],
) -> Component:
def create(cls, *, name: str, medium: GasMedium, parameters: Mapping[str, float]) -> Component:
"""Create a catalog model from normalized SI parameters."""
raise NotImplementedError(f'Component model {cls.__name__} must implement create().')
EQUATIONS = ()
raise NotImplementedError(
f"Component model {cls.__name__} must implement create()."
)
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
"""Return algebraic residuals after the network assigns port states."""
return ()
def pressure_flow_equation_values(self) -> tuple[float, ...]:
"""Return live residual values in the declared equation order.
Components with frequently evaluated equations can override this
method to avoid rebuilding immutable equation metadata during closure.
The default keeps third-party components compatible with the public
residual API.
"""
return tuple(
float(equation.value)
for equation in self.pressure_flow_equation_residuals()
)
def pressure_flow_equation_value_readers(
self,
) -> Mapping[str, Callable[[], float]]:
"""Return explicitly separable scalar residual readers.
The solver consumes this optional capability only when the concrete
component class declares the method itself. Subclasses therefore
cannot accidentally inherit an equation-purity promise.
"""
return {}
def update_stream_outflows(self, connected_h: Mapping[str, float]) -> None:
"""Update connector outflow properties from current flow directions."""
return None
def update_flow_temperature_references(
self,
connected_h: Mapping[str, float],
) -> None:
"""Update enthalpy references used only by pressure-flow laws.
Most components use the normal stream enthalpy for both energy
transport and upstream-property evaluation. AMESim node submodels can
expose a distinct temperature reference, so the default is a no-op.
"""
return None
def pneumatic_volume_outputs(self) -> Mapping[str, tuple[float, float]]:
"""Return directed ``volume``/``volume_flow`` values by pneumatic port.
Most pneumatic components contribute no external chamber volume. Moving
boundaries such as PNRP17 override this hook; the network resolver then
propagates the pair to the component connected at the same physical port.
"""
return {}
def equation_definitions(self):
def bind(value):
if isinstance(value, str):
return value.replace('__MODEL__', self.name)
return tuple((bind(v) for v in value))
return tuple((EquationDefinition(id=bind(e['id']), owner=e['owner'], owner_id=self.name, relation=e['relation'], variables=bind(e['variables']), role=e['role']) for e in self.EQUATIONS))
class DynamicComponent(Component):
state_size = 2
@staticmethod
def actual_stream_enthalpy(
port_m_flow: float,
connected_h: float,
internal_h: float,
) -> float:
"""Approximate `actualStream(port.h_outflow)` for a mixed control volume port."""
return connected_h if port_m_flow > 0.0 else internal_h
def connection_inlet_enthalpy(
self,
port_m_flow: float,
connected_h: float,
internal_h: float,
) -> float:
"""Resolve the enthalpy convected into this control volume through one port."""
return self.actual_stream_enthalpy(
port_m_flow=port_m_flow,
connected_h=connected_h,
internal_h=internal_h,
)
@abstractmethod
def get_state_vector(self) -> list[float]:
raise NotImplementedError
@abstractmethod
def set_state_vector(self, values: list[float]) -> None:
raise NotImplementedError
def refresh_thermodynamic_ports(self) -> Any:
raise NotImplementedError
def state_derivative_from_ports(
self,
connected_h: Mapping[str, float],
) -> list[float]:
raise NotImplementedError
class ThermodynamicVolumeComponent(DynamicComponent):
"""Two-state gas volume exposing the shared thermodynamic result contract."""
RESULT_VARIABLES = THERMODYNAMIC_VOLUME_RESULT_VARIABLES
def component_result_values(self) -> Mapping[str, float]:
state = self.get_state_vector()
if len(state) < 2:
raise ValueError(
f"Thermodynamic component {self.name} must expose mass and energy states."
)
properties = self.refresh_thermodynamic_ports()
return {
"m": float(state[0]),
"U": float(state[1]),
"p": float(properties.p),
"T": float(properties.T),
"rho": float(properties.rho),
"u": float(properties.u),
"h": float(properties.h),
}
class AlgebraicComponent(Component):
"""Stateless element described by algebraic constraints only."""