完善建模交互、组件图标与系统协议
This commit is contained in:
1 parent
456c29b3b6
commit
6572defaa4
66 files changed
+10067
-4163
No files matched your search
@@ -186,7 +186,7 @@ RESULT_VARIABLES / DISPLAY / create()`,再把类路径加入库清单。完整
|
||||
`testmodel_tank_temperature.svg`
|
||||
11. 基于 `ModelicaModels/Simulation/Testmodel_res.csv` 的逐时刻对比与误差摘要导出。
|
||||
12. 基于 `unittest` 的自动回归测试,当前已覆盖初始化守恒、主变量基线、运行接口、内部闭合诊断、通用分支兼容层、通用结果键与旧键别名一致性,以及部分中间闭合过程行为。
|
||||
13. 面向 System XML v2 的拓扑驱动仿真 MVP:压力-流量非线性闭合、stream 焓传播、动态状态自动拼装和端口结果序列。
|
||||
13. 面向 System XML v3 的拓扑驱动仿真 MVP:压力-流量非线性闭合、stream 焓传播、动态状态自动拼装和端口结果序列。
|
||||
|
||||
当前没有实现:
|
||||
|
||||
|
||||
@@ -81,12 +81,29 @@ class AmesimForc(AlgebraicComponent):
|
||||
"""AMESim FORC signal-to-force converter."""
|
||||
|
||||
MODEL_TYPE = "amesim_forc"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
MODEL_VERSION = "0.2.0"
|
||||
PORTS = (
|
||||
PortDefinition.signal("res", nominal_role="input"),
|
||||
PortDefinition.mechanical_translational("port_2"),
|
||||
)
|
||||
PARAMETERS = ()
|
||||
PARAMETERS = (
|
||||
ParameterDefinition(
|
||||
"direction",
|
||||
1.0,
|
||||
label="力方向",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
editor="choice",
|
||||
options=(
|
||||
ParameterOption(1.0, "正向"),
|
||||
ParameterOption(-1.0, "反向"),
|
||||
),
|
||||
description=(
|
||||
"显式控制输入信号相对于机械端口正方向的力符号;"
|
||||
"图标旋转和镜像不会改变该参数。"
|
||||
),
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("force", "输出力", "force", "N", "signal", 10),
|
||||
)
|
||||
@@ -102,12 +119,12 @@ class AmesimForc(AlgebraicComponent):
|
||||
order=20,
|
||||
)
|
||||
|
||||
def __init__(self, name: str) -> None:
|
||||
def __init__(self, name: str, *, direction: float = 1.0) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({})
|
||||
self.set_parameter_values({"direction": direction})
|
||||
self.direction = float(direction)
|
||||
self.res = self.register_declared_port("res")
|
||||
self.port_2 = self.register_declared_port("port_2")
|
||||
self._orientation_sign = 1.0
|
||||
|
||||
@classmethod
|
||||
def create(
|
||||
@@ -117,20 +134,11 @@ class AmesimForc(AlgebraicComponent):
|
||||
medium: IdealGasMedium,
|
||||
parameters: Mapping[str, float],
|
||||
) -> "AmesimForc":
|
||||
return cls(name=name)
|
||||
|
||||
def apply_layout_transform(self, *, rotation: int, mirrored: bool) -> None:
|
||||
"""Apply the AMESim icon direction to the signed force output."""
|
||||
|
||||
normalized_rotation = int(rotation) % 360
|
||||
if normalized_rotation not in {0, 90, 180, 270}:
|
||||
raise ValueError("FORC rotation must be a multiple of 90 degrees.")
|
||||
direction = -1.0 if normalized_rotation in {180, 270} else 1.0
|
||||
self._orientation_sign = -direction if mirrored else direction
|
||||
return cls(name=name, direction=parameters["direction"])
|
||||
|
||||
@property
|
||||
def output_force(self) -> float:
|
||||
return float(self.res.signal)
|
||||
return self.direction * float(self.res.signal)
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
return (
|
||||
@@ -141,7 +149,7 @@ class AmesimForc(AlgebraicComponent):
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.port_2.f", f"{self.name}.res.signal"),
|
||||
role="flow",
|
||||
value=self.port_2.f + self._orientation_sign * self.output_force,
|
||||
value=self.port_2.f + self.output_force,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -366,7 +374,7 @@ class AmesimMecmas21(DynamicComponent):
|
||||
),
|
||||
ParameterDefinition(
|
||||
"useFriction",
|
||||
1.0,
|
||||
2.0,
|
||||
label="启用摩擦",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
@@ -383,7 +391,7 @@ class AmesimMecmas21(DynamicComponent):
|
||||
),
|
||||
ParameterDefinition(
|
||||
"stoptype",
|
||||
4.0,
|
||||
1.0,
|
||||
label="限位类型",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
@@ -1031,14 +1039,33 @@ class AmesimLmechn1(AlgebraicComponent):
|
||||
"""AMESim LMECHN1 first public dynamic linear mechanical node."""
|
||||
|
||||
MODEL_TYPE = "amesim_lmechn1"
|
||||
MODEL_VERSION = "0.1.0"
|
||||
MODEL_VERSION = "0.2.0"
|
||||
PORTS = tuple(
|
||||
PortDefinition.mechanical_translational(f"port_{index}")
|
||||
for index in range(1, 10)
|
||||
for index in range(1, 22)
|
||||
)
|
||||
PARAMETERS = (
|
||||
ParameterDefinition("v1", 8.0, label="右侧端口数", quantity="dimensionless", unit="", minimum=1.0, maximum=8.0),
|
||||
ParameterDefinition("sum", 1.0, label="节点求和模式", quantity="dimensionless", unit="", minimum=0.0),
|
||||
ParameterDefinition(
|
||||
"v1",
|
||||
2.0,
|
||||
label="右侧端口数",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
minimum=1.0,
|
||||
maximum=20.0,
|
||||
description="设置工作区中显示的右侧机械端口数量,最多 20 个。",
|
||||
),
|
||||
ParameterDefinition(
|
||||
"sum",
|
||||
1.0,
|
||||
label="节点求和模式",
|
||||
quantity="dimensionless",
|
||||
unit="",
|
||||
editor="choice",
|
||||
options=(
|
||||
ParameterOption(1.0, "各端口力代数和为零(标准节点)"),
|
||||
),
|
||||
),
|
||||
)
|
||||
RESULT_VARIABLES = (
|
||||
ResultVariableDefinition("tforce", "节点合力", "force", "N", "derived", 10),
|
||||
@@ -1049,13 +1076,16 @@ class AmesimLmechn1(AlgebraicComponent):
|
||||
category_id="mechanical",
|
||||
symbol="amesim_lmechn1",
|
||||
ports=tuple(
|
||||
[PortDisplaySpec(f"port_{index}", "left", order=index * 10) for index in range(1, 9)]
|
||||
+ [PortDisplaySpec("port_9", "right", order=90)]
|
||||
[
|
||||
PortDisplaySpec(f"port_{index}", "right", order=index * 10)
|
||||
for index in range(1, 21)
|
||||
]
|
||||
+ [PortDisplaySpec("port_21", "left", order=210)]
|
||||
),
|
||||
order=50,
|
||||
)
|
||||
|
||||
def __init__(self, name: str, medium: IdealGasMedium, *, v1: float = 8.0, sum: float = 1.0) -> None:
|
||||
def __init__(self, name: str, medium: IdealGasMedium, *, v1: float = 2.0, sum: float = 1.0) -> None:
|
||||
super().__init__(name=name)
|
||||
self.set_parameter_values({"v1": v1, "sum": sum})
|
||||
self.v1 = int(v1)
|
||||
@@ -1078,21 +1108,30 @@ class AmesimLmechn1(AlgebraicComponent):
|
||||
|
||||
@property
|
||||
def active_ports(self) -> tuple[str, ...]:
|
||||
return tuple(f"port_{index}" for index in range(1, self.v1 + 1)) + ("port_9",)
|
||||
return tuple(f"port_{index}" for index in range(1, self.v1 + 2))
|
||||
|
||||
@property
|
||||
def reference_port_name(self) -> str:
|
||||
return f"port_{self.v1 + 1}"
|
||||
|
||||
@property
|
||||
def required_connection_ports(self) -> tuple[str, ...]:
|
||||
return self.active_ports
|
||||
|
||||
@property
|
||||
def total_force(self) -> float:
|
||||
# AMESim's ``tforce`` is the force transmitted by the summed branch
|
||||
# ports (1..v1). Port 9 is the balancing/common port and is excluded
|
||||
# from that reported value.
|
||||
# ports (1..v1). The final active port is the balancing/common port and
|
||||
# is excluded from that reported value.
|
||||
return sum(self.get_port(port_name).f for port_name in self.active_ports[:-1])
|
||||
|
||||
@property
|
||||
def force_balance(self) -> float:
|
||||
return self.total_force + self.port_9.f
|
||||
return self.total_force + self.get_port(self.reference_port_name).f
|
||||
|
||||
def pressure_flow_equation_residuals(self) -> tuple[EquationResidual, ...]:
|
||||
reference = self.port_9
|
||||
reference_name = self.reference_port_name
|
||||
reference = self.get_port(reference_name)
|
||||
residuals: list[EquationResidual] = []
|
||||
for port_name in self.active_ports[:-1]:
|
||||
port = self.get_port(port_name)
|
||||
@@ -1102,7 +1141,7 @@ class AmesimLmechn1(AlgebraicComponent):
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="equal",
|
||||
variables=(f"{self.name}.{port_name}.x", f"{self.name}.port_9.x"),
|
||||
variables=(f"{self.name}.{port_name}.x", f"{self.name}.{reference_name}.x"),
|
||||
role="effort",
|
||||
value=port.x - reference.x,
|
||||
)
|
||||
@@ -1113,7 +1152,7 @@ class AmesimLmechn1(AlgebraicComponent):
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="equal",
|
||||
variables=(f"{self.name}.{port_name}.v", f"{self.name}.port_9.v"),
|
||||
variables=(f"{self.name}.{port_name}.v", f"{self.name}.{reference_name}.v"),
|
||||
role="effort",
|
||||
value=port.v - reference.v,
|
||||
)
|
||||
@@ -1129,6 +1168,39 @@ class AmesimLmechn1(AlgebraicComponent):
|
||||
value=self.force_balance,
|
||||
)
|
||||
)
|
||||
for definition in self.PORTS[self.v1 + 1 :]:
|
||||
port = self.get_port(definition.name)
|
||||
residuals.extend(
|
||||
(
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{definition.name}_inactive_x",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.{definition.name}.x",),
|
||||
role="effort",
|
||||
value=port.x,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{definition.name}_inactive_v",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.{definition.name}.v",),
|
||||
role="effort",
|
||||
value=port.v,
|
||||
),
|
||||
EquationResidual(
|
||||
id=f"{self.name}:{definition.name}_inactive_force",
|
||||
owner="component",
|
||||
owner_id=self.name,
|
||||
relation="constitutive",
|
||||
variables=(f"{self.name}.{definition.name}.f",),
|
||||
role="flow",
|
||||
value=port.f,
|
||||
),
|
||||
)
|
||||
)
|
||||
return tuple(residuals)
|
||||
|
||||
def component_result_values(self) -> Mapping[str, float]:
|
||||
|
||||
@@ -1,12 +1,3 @@
|
||||
"""Temporary component library used to validate the model authoring contract."""
|
||||
|
||||
from app.simulation.components.experimental.library import LIBRARY
|
||||
|
||||
|
||||
# Compatibility aliases for code written before the v1 library manifest.
|
||||
LIBRARY_ID = LIBRARY.id
|
||||
LIBRARY_LABEL = LIBRARY.label
|
||||
LIBRARY_VERSION = LIBRARY.version
|
||||
LIBRARY_ORDER = LIBRARY.order
|
||||
LIBRARY_SOURCE_PACKAGE = LIBRARY.source_package
|
||||
LIBRARY_TEMPORARY = LIBRARY.temporary
|
||||
@@ -44,6 +44,16 @@ class Component(ABC):
|
||||
if port.definition is not None
|
||||
)
|
||||
|
||||
@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.port_definitions
|
||||
if definition.kind == "physical"
|
||||
)
|
||||
|
||||
def register_port(self, port: PortState) -> PortState:
|
||||
definition = port.definition
|
||||
if definition is None:
|
||||
|
||||
@@ -47,6 +47,14 @@ class ThermofluidClosureError(RuntimeError):
|
||||
"""Raised when stream enthalpy and pressure-flow do not reach one fixed point."""
|
||||
|
||||
|
||||
class SimulationSampleTimeError(ValueError):
|
||||
"""Stable failure contract for an unsafe or unrepresentable sample grid."""
|
||||
|
||||
def __init__(self, code: str, message: str) -> None:
|
||||
super().__init__(message)
|
||||
self.code = code
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class GenericSimulationResult:
|
||||
success: bool
|
||||
@@ -106,10 +114,9 @@ def simulation_preparation_issues(
|
||||
) -> tuple[SimulationPreparationIssue, ...]:
|
||||
issues: list[SimulationPreparationIssue] = []
|
||||
physical_endpoints = {
|
||||
Endpoint(component.name, definition.name)
|
||||
Endpoint(component.name, port_name)
|
||||
for component in network.components.values()
|
||||
for definition in component.port_definitions
|
||||
if definition.kind == "physical"
|
||||
for port_name in component.required_connection_ports
|
||||
}
|
||||
connected_endpoints = {
|
||||
endpoint
|
||||
@@ -148,8 +155,18 @@ def simulation_preparation_issues(
|
||||
)
|
||||
)
|
||||
|
||||
adjacency = {name: set() for name in network.components}
|
||||
physical_component_names = {
|
||||
component.name
|
||||
for component in network.components.values()
|
||||
if any(
|
||||
definition.kind == "physical"
|
||||
for definition in component.port_definitions
|
||||
)
|
||||
}
|
||||
adjacency = {name: set() for name in physical_component_names}
|
||||
for connection in network.connections:
|
||||
if connection.kind != "physical":
|
||||
continue
|
||||
first, second = connection.endpoints
|
||||
adjacency[first.component].add(second.component)
|
||||
adjacency[second.component].add(first.component)
|
||||
@@ -224,20 +241,95 @@ def simulation_sample_times(
|
||||
*,
|
||||
max_points: int = 10001,
|
||||
) -> list[float]:
|
||||
if max_points < 2:
|
||||
raise SimulationSampleTimeError(
|
||||
"SIMULATION_SAMPLE_LIMIT_INVALID",
|
||||
"Simulation sample limit must allow at least two points.",
|
||||
)
|
||||
t_start = float(config.t_start)
|
||||
t_stop = float(config.t_stop)
|
||||
if not isfinite(t_start) or not isfinite(t_stop):
|
||||
raise SimulationSampleTimeError(
|
||||
"SIMULATION_VALUE_NOT_FINITE",
|
||||
"Simulation start and stop times must be finite.",
|
||||
)
|
||||
if step <= 0.0 or not isfinite(step):
|
||||
raise ValueError("Simulation sample step must be finite and greater than zero.")
|
||||
duration = config.t_stop - config.t_start
|
||||
raise SimulationSampleTimeError(
|
||||
"SIMULATION_SAMPLE_STEP_INVALID",
|
||||
"Simulation sample step must be finite and greater than zero.",
|
||||
)
|
||||
duration = t_stop - t_start
|
||||
if not isfinite(duration):
|
||||
raise SimulationSampleTimeError(
|
||||
"SIMULATION_TIME_SPAN_NOT_FINITE",
|
||||
"Simulation time span must be finite.",
|
||||
)
|
||||
if duration <= 0.0:
|
||||
raise ValueError("Simulation stop time must be greater than start time.")
|
||||
interval_count = int(floor(duration / step + 1e-12))
|
||||
times = [config.t_start + index * step for index in range(interval_count + 1)]
|
||||
if times[-1] < config.t_stop - 1e-12:
|
||||
times.append(config.t_stop)
|
||||
else:
|
||||
times[-1] = config.t_stop
|
||||
if len(times) > max_points:
|
||||
raise ValueError(
|
||||
f"Simulation requests {len(times)} samples; the limit is {max_points}."
|
||||
raise SimulationSampleTimeError(
|
||||
"SIMULATION_TIME_RANGE_INVALID",
|
||||
"Simulation stop time must be greater than start time.",
|
||||
)
|
||||
|
||||
# Bound the grid before dividing by a potentially tiny step or allocating
|
||||
# the result list. This avoids both float-to-int overflow and an OOM-sized
|
||||
# ``range``/list when input comes from an external System XML document.
|
||||
maximum_interval_count = max_points - 1
|
||||
if step < duration / maximum_interval_count:
|
||||
raise SimulationSampleTimeError(
|
||||
"SIMULATION_SAMPLE_COUNT_EXCEEDED",
|
||||
f"Simulation sample count exceeds the limit of {max_points}; "
|
||||
"increase sampleStep.",
|
||||
)
|
||||
|
||||
ratio = duration / step
|
||||
if not isfinite(ratio):
|
||||
raise SimulationSampleTimeError(
|
||||
"SIMULATION_SAMPLE_COUNT_EXCEEDED",
|
||||
f"Simulation sample count exceeds the limit of {max_points}; "
|
||||
"increase sampleStep.",
|
||||
)
|
||||
interval_count = int(floor(ratio))
|
||||
last_regular_time = t_start + interval_count * step
|
||||
append_stop = last_regular_time < t_stop
|
||||
requested_point_count = interval_count + 1 + int(append_stop)
|
||||
if requested_point_count > max_points:
|
||||
raise SimulationSampleTimeError(
|
||||
"SIMULATION_SAMPLE_COUNT_EXCEEDED",
|
||||
f"Simulation requests {requested_point_count} samples; "
|
||||
f"the limit is {max_points}.",
|
||||
)
|
||||
|
||||
times = [t_start]
|
||||
for index in range(1, interval_count + 1):
|
||||
candidate = t_start + index * step
|
||||
if not isfinite(candidate):
|
||||
raise SimulationSampleTimeError(
|
||||
"SIMULATION_SAMPLE_TIME_UNREPRESENTABLE",
|
||||
"Simulation sampleStep cannot be represented over the requested "
|
||||
"absolute time range.",
|
||||
)
|
||||
if candidate >= t_stop:
|
||||
candidate = t_stop
|
||||
if candidate <= times[-1]:
|
||||
raise SimulationSampleTimeError(
|
||||
"SIMULATION_SAMPLE_TIME_UNREPRESENTABLE",
|
||||
"Simulation sampleStep is too small to advance floating-point "
|
||||
"time over the requested absolute time range.",
|
||||
)
|
||||
times.append(candidate)
|
||||
if candidate == t_stop:
|
||||
break
|
||||
if times[-1] < t_stop:
|
||||
times.append(t_stop)
|
||||
|
||||
if len(times) < 2 or any(
|
||||
current >= following
|
||||
for current, following in zip(times, times[1:])
|
||||
):
|
||||
raise SimulationSampleTimeError(
|
||||
"SIMULATION_SAMPLE_TIME_UNREPRESENTABLE",
|
||||
"Simulation sample times must contain at least two strictly "
|
||||
"increasing values.",
|
||||
)
|
||||
return times
|
||||
|
||||
|
||||
@@ -91,8 +91,10 @@ class SimulationNetwork:
|
||||
) -> Connection:
|
||||
endpoint_a = Endpoint(endpoint_a_component, endpoint_a_port)
|
||||
endpoint_b = Endpoint(endpoint_b_component, endpoint_b_port)
|
||||
if endpoint_a == endpoint_b:
|
||||
raise ValueError(f"Cannot connect endpoint {endpoint_a} to itself.")
|
||||
if endpoint_a.component == endpoint_b.component:
|
||||
raise ValueError(
|
||||
f"Cannot connect component {endpoint_a.component} to itself."
|
||||
)
|
||||
|
||||
first_port = self._port_for(endpoint_a)
|
||||
second_port = self._port_for(endpoint_b)
|
||||
@@ -131,6 +133,16 @@ class SimulationNetwork:
|
||||
+ ", ".join(occupied)
|
||||
+ ". Use a junction component for branching."
|
||||
)
|
||||
else:
|
||||
signal_input = (
|
||||
endpoint_a
|
||||
if first_definition.nominal_role == "input"
|
||||
else endpoint_b
|
||||
)
|
||||
if signal_input in occupied_endpoints:
|
||||
raise ValueError(
|
||||
f"Signal input {signal_input} already has a driver."
|
||||
)
|
||||
|
||||
if first_definition.kind == "physical" and endpoint_b.key < endpoint_a.key:
|
||||
endpoint_a, endpoint_b = endpoint_b, endpoint_a
|
||||
|
||||
Reference in new issue
Block a user