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

@@ -1,355 +1,79 @@
"""Component parameters, ports and output definitions; numerical equations execute in C."""
from __future__ import annotations
from collections.abc import Mapping
from math import floor
from app.simulation.core.base import AlgebraicComponent
from app.simulation.core.catalog import (
ComponentDisplaySpec,
ParameterGroupDisplaySpec,
PortDisplaySpec,
)
from app.simulation.core.metadata import (
ParameterCondition,
ParameterDefinition,
ParameterOption,
ResultVariableDefinition,
)
from app.simulation.core.catalog import ComponentDisplaySpec, ParameterGroupDisplaySpec, PortDisplaySpec
from app.simulation.core.metadata import ParameterCondition, ParameterDefinition, ParameterOption, ResultVariableDefinition
from app.simulation.core.medium import IdealGasMedium
from app.simulation.core.ports import PortDefinition
def _ud00_stage_parameters(index: int) -> tuple[ParameterDefinition, ...]:
visible_when = (
()
if index == 1
else (
ParameterCondition(
"nstages",
tuple(float(stage_count) for stage_count in range(index, 9)),
),
)
)
return (
ParameterDefinition(
f"start{index}",
0.0 if index == 1 else 1.0,
label=f"第 {index} 段起点",
quantity="dimensionless",
unit="",
description=f"第 {index} 段开始时的输出值。",
visible_when=visible_when,
),
ParameterDefinition(
f"end{index}",
1.0,
label=f"第 {index} 段终点",
quantity="dimensionless",
unit="",
description=f"第 {index} 段结束时的输出值。",
visible_when=visible_when,
),
ParameterDefinition(
f"t{index}",
1.0 if index == 1 else 0.0,
label=f"第 {index} 段时长",
quantity="time",
unit="s",
minimum=0.0,
description=f"第 {index} 段的持续时间。",
visible_when=visible_when,
),
)
_UD00_STAGE_PARAMETERS = tuple(
parameter
for stage_index in range(1, 9)
for parameter in _ud00_stage_parameters(stage_index)
)
visible_when = () if index == 1 else (ParameterCondition('nstages', tuple((float(stage_count) for stage_count in range(index, 9)))),)
return (ParameterDefinition(f'start{index}', 0.0 if index == 1 else 1.0, label=f'第 {index} 段起点', quantity='dimensionless', unit='', description=f'第 {index} 段开始时的输出值。', visible_when=visible_when), ParameterDefinition(f'end{index}', 1.0, label=f'第 {index} 段终点', quantity='dimensionless', unit='', description=f'第 {index} 段结束时的输出值。', visible_when=visible_when), ParameterDefinition(f't{index}', 1.0 if index == 1 else 0.0, label=f'第 {index} 段时长', quantity='time', unit='s', minimum=0.0, description=f'第 {index} 段的持续时间。', visible_when=visible_when))
_UD00_STAGE_PARAMETERS = tuple((parameter for stage_index in range(1, 9) for parameter in _ud00_stage_parameters(stage_index)))
class AmesimStep0(AlgebraicComponent):
"""AMESim STEP0 scalar step signal source."""
MODEL_TYPE = 'amesim_step0'
MODEL_VERSION = '0.1.0'
PORTS = (PortDefinition.signal('out', nominal_role='output'),)
PARAMETERS = (ParameterDefinition('initial', 0.0, label='初始值', quantity='dimensionless', unit=''), ParameterDefinition('final', 1.0, label='阶跃后值', quantity='dimensionless', unit=''), ParameterDefinition('time', 0.0, label='阶跃时间', quantity='time', unit='s'))
RESULT_VARIABLES = (ResultVariableDefinition('y', '输出', 'dimensionless', '', 'signal', 10),)
DISPLAY = ComponentDisplaySpec(label='STEP0 阶跃信号', library_id='amesim', category_id='signals', symbol='amesim_step0', ports=(PortDisplaySpec('out', 'right', order=10),), order=10)
MODEL_TYPE = "amesim_step0"
MODEL_VERSION = "0.1.0"
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
PARAMETERS = (
ParameterDefinition("initial", 0.0, label="初始值", quantity="dimensionless", unit=""),
ParameterDefinition("final", 1.0, label="阶跃后值", quantity="dimensionless", unit=""),
ParameterDefinition("time", 0.0, label="阶跃时间", quantity="time", unit="s"),
)
RESULT_VARIABLES = (
ResultVariableDefinition("y", "输出", "dimensionless", "", "signal", 10),
)
DISPLAY = ComponentDisplaySpec(
label="STEP0 阶跃信号",
library_id="amesim",
category_id="signals",
symbol="amesim_step0",
ports=(PortDisplaySpec("out", "right", order=10),),
order=10,
)
def __init__(
self,
name: str,
medium: IdealGasMedium,
*,
initial: float = 0.0,
final: float = 1.0,
time: float = 0.0,
) -> None:
def __init__(self, name: str, medium: IdealGasMedium, *, initial: float=0.0, final: float=1.0, time: float=0.0) -> None:
super().__init__(name=name)
self.set_parameter_values({"initial": initial, "final": final, "time": time})
self.set_parameter_values({'initial': initial, 'final': final, 'time': time})
self.initial = float(initial)
self.final = float(final)
self.time = float(time)
self.out = self.register_declared_port("out")
self.out.signal = self.output_at(0.0)
self.out = self.register_declared_port('out')
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> "AmesimStep0":
return cls(
name=name,
medium=medium,
initial=parameters["initial"],
final=parameters["final"],
time=parameters["time"],
)
def output_at(self, time: float) -> float:
return self.final if time >= self.time else self.initial
def signal_output_values(self, time: float) -> dict[str, float]:
return {"out": self.output_at(time)}
def signal_event_times(
self,
start_time: float,
stop_time: float,
) -> tuple[float, ...]:
"""Expose the exact STEP0 switch time as an integration split point."""
return (self.time,) if start_time < self.time < stop_time else ()
def component_result_values(self) -> Mapping[str, float]:
return {"y": self.out.signal}
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> 'AmesimStep0':
return cls(name=name, medium=medium, initial=parameters['initial'], final=parameters['final'], time=parameters['time'])
EQUATIONS = ()
class AmesimUd00(AlgebraicComponent):
"""AMESim UD00 piecewise-linear scalar signal source."""
MODEL_TYPE = 'amesim_ud00'
MODEL_VERSION = '0.2.0'
PORTS = (PortDefinition.signal('out', nominal_role='output'),)
PARAMETERS = (ParameterDefinition('tstart', 0.0, label='启动时间', quantity='time', unit='s', description='分段信号开始输出第一段之前的等待时间。'), *_UD00_STAGE_PARAMETERS, ParameterDefinition('nstages', 1.0, label='段数', quantity='dimensionless', unit='', minimum=1.0, maximum=8.0, editor='choice', options=tuple((ParameterOption(float(stage_count), str(stage_count)) for stage_count in range(1, 9))), description='参与输出计算的有效线性分段数量。'), ParameterDefinition('iscyclic', 0.0, label='循环', quantity='dimensionless', unit='', minimum=0.0, maximum=1.0, editor='choice', options=(ParameterOption(0.0, '否'), ParameterOption(1.0, '是')), description='当前公共协议编码:0 表示单次输出,1 表示循环输出。'))
RESULT_VARIABLES = (ResultVariableDefinition('y', '输出', 'dimensionless', '', 'signal', 10),)
DISPLAY = ComponentDisplaySpec(label='UD00 分段线性信号', library_id='amesim', category_id='signals', symbol='amesim_ud00', ports=(PortDisplaySpec('out', 'right', order=10),), order=20, parameter_groups=(ParameterGroupDisplaySpec(id='stages', label='分段参数', parameters=tuple((parameter.name for parameter in _UD00_STAGE_PARAMETERS)), order=10),))
MODEL_TYPE = "amesim_ud00"
MODEL_VERSION = "0.2.0"
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
PARAMETERS = (
ParameterDefinition(
"tstart",
0.0,
label="启动时间",
quantity="time",
unit="s",
description="分段信号开始输出第一段之前的等待时间。",
),
*_UD00_STAGE_PARAMETERS,
ParameterDefinition(
"nstages",
1.0,
label="段数",
quantity="dimensionless",
unit="",
minimum=1.0,
maximum=8.0,
editor="choice",
options=tuple(
ParameterOption(float(stage_count), str(stage_count))
for stage_count in range(1, 9)
),
description="参与输出计算的有效线性分段数量。",
),
ParameterDefinition(
"iscyclic",
0.0,
label="循环",
quantity="dimensionless",
unit="",
minimum=0.0,
maximum=1.0,
editor="choice",
options=(
ParameterOption(0.0, "否"),
ParameterOption(1.0, "是"),
),
description="当前公共协议编码:0 表示单次输出,1 表示循环输出。",
),
)
RESULT_VARIABLES = (
ResultVariableDefinition("y", "输出", "dimensionless", "", "signal", 10),
)
DISPLAY = ComponentDisplaySpec(
label="UD00 分段线性信号",
library_id="amesim",
category_id="signals",
symbol="amesim_ud00",
ports=(PortDisplaySpec("out", "right", order=10),),
order=20,
parameter_groups=(
ParameterGroupDisplaySpec(
id="stages",
label="分段参数",
parameters=tuple(
parameter.name for parameter in _UD00_STAGE_PARAMETERS
),
order=10,
),
),
)
def __init__(
self,
name: str,
medium: IdealGasMedium,
*,
tstart: float = 0.0,
starts: tuple[float, ...] = (0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
ends: tuple[float, ...] = (1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0),
durations: tuple[float, ...] = (1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0),
nstages: int = 1,
iscyclic: bool = False,
) -> None:
def __init__(self, name: str, medium: IdealGasMedium, *, tstart: float=0.0, starts: tuple[float, ...]=(0.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0), ends: tuple[float, ...]=(1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0), durations: tuple[float, ...]=(1.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), nstages: int=1, iscyclic: bool=False) -> None:
super().__init__(name=name)
if len(starts) != 8 or len(ends) != 8 or len(durations) != 8:
raise ValueError("UD00 requires exactly eight start, end, and duration values.")
raise ValueError('UD00 requires exactly eight start, end, and duration values.')
if nstages < 1 or nstages > 8:
raise ValueError("UD00 nstages must be between 1 and 8.")
raise ValueError('UD00 nstages must be between 1 and 8.')
self.tstart = float(tstart)
self.starts = tuple(float(value) for value in starts)
self.ends = tuple(float(value) for value in ends)
self.durations = tuple(float(value) for value in durations)
self.starts = tuple((float(value) for value in starts))
self.ends = tuple((float(value) for value in ends))
self.durations = tuple((float(value) for value in durations))
self.nstages = int(nstages)
self.iscyclic = bool(iscyclic)
values: dict[str, float] = {"tstart": self.tstart, "nstages": float(self.nstages), "iscyclic": float(int(self.iscyclic))}
values: dict[str, float] = {'tstart': self.tstart, 'nstages': float(self.nstages), 'iscyclic': float(int(self.iscyclic))}
for index in range(1, 9):
values[f"start{index}"] = self.starts[index - 1]
values[f"end{index}"] = self.ends[index - 1]
values[f"t{index}"] = self.durations[index - 1]
values[f'start{index}'] = self.starts[index - 1]
values[f'end{index}'] = self.ends[index - 1]
values[f't{index}'] = self.durations[index - 1]
self.set_parameter_values(values)
self.out = self.register_declared_port("out")
self.out.signal = self.output_at(0.0)
self.out = self.register_declared_port('out')
@classmethod
def create(
cls,
*,
name: str,
medium: IdealGasMedium,
parameters: Mapping[str, float],
) -> "AmesimUd00":
nstages = parameters["nstages"]
iscyclic = parameters["iscyclic"]
def create(cls, *, name: str, medium: IdealGasMedium, parameters: Mapping[str, float]) -> 'AmesimUd00':
nstages = parameters['nstages']
iscyclic = parameters['iscyclic']
definitions = {definition.name: definition for definition in cls.PARAMETERS}
for parameter_name, value in (
("nstages", nstages),
("iscyclic", iscyclic),
):
for parameter_name, value in (('nstages', nstages), ('iscyclic', iscyclic)):
numeric_value = float(value)
if not numeric_value.is_integer():
raise ValueError(f"UD00 {parameter_name} must be an integer.")
raise ValueError(f'UD00 {parameter_name} must be an integer.')
message = definitions[parameter_name].validation_message(numeric_value)
if message is not None:
raise ValueError(f"UD00 {parameter_name} {message}.")
return cls(
name=name,
medium=medium,
tstart=parameters["tstart"],
starts=tuple(parameters[f"start{index}"] for index in range(1, 9)),
ends=tuple(parameters[f"end{index}"] for index in range(1, 9)),
durations=tuple(parameters[f"t{index}"] for index in range(1, 9)),
nstages=int(nstages),
iscyclic=bool(int(iscyclic)),
)
def output_at(self, time: float) -> float:
elapsed = max(float(time) - self.tstart, 0.0)
active_durations = self.durations[: self.nstages]
total_duration = sum(active_durations)
if self.iscyclic and total_duration > 0.0:
elapsed = elapsed % total_duration
stage_start_time = 0.0
for index, duration in enumerate(active_durations):
stage_end_time = stage_start_time + duration
if elapsed < stage_end_time or index == self.nstages - 1:
if duration <= 0.0:
return self.ends[index]
fraction = (elapsed - stage_start_time) / duration
return self.starts[index] + fraction * (self.ends[index] - self.starts[index])
stage_start_time = stage_end_time
return self.ends[self.nstages - 1]
def signal_output_values(self, time: float) -> dict[str, float]:
return {"out": self.output_at(time)}
def signal_event_times(
self,
start_time: float,
stop_time: float,
) -> tuple[float, ...]:
"""Return UD00 start, stage, and repeated cycle boundaries.
The final non-cyclic stage is intentionally not given an end event:
``output_at`` continues that stage's slope after its configured duration.
"""
if stop_time <= start_time:
return ()
active_durations = self.durations[: self.nstages]
stage_offsets = [0.0]
elapsed = 0.0
for duration in active_durations[:-1]:
elapsed += duration
stage_offsets.append(elapsed)
if not self.iscyclic:
return tuple(
sorted(
{
event_time
for offset in stage_offsets
if start_time
< (event_time := self.tstart + offset)
< stop_time
}
)
)
cycle_duration = sum(active_durations)
if cycle_duration <= 0.0:
return ()
events: set[float] = set()
for offset in stage_offsets:
first_boundary = self.tstart + offset
cycle_index = max(
0,
floor((start_time - first_boundary) / cycle_duration) + 1,
)
event_time = first_boundary + cycle_index * cycle_duration
while event_time < stop_time:
if event_time > start_time:
events.add(event_time)
cycle_index += 1
event_time = first_boundary + cycle_index * cycle_duration
return tuple(sorted(events))
def component_result_values(self) -> Mapping[str, float]:
return {"y": self.out.signal}
raise ValueError(f'UD00 {parameter_name} {message}.')
return cls(name=name, medium=medium, tstart=parameters['tstart'], starts=tuple((parameters[f'start{index}'] for index in range(1, 9))), ends=tuple((parameters[f'end{index}'] for index in range(1, 9))), durations=tuple((parameters[f't{index}'] for index in range(1, 9))), nstages=int(nstages), iscyclic=bool(int(iscyclic)))
EQUATIONS = ()