268 lines
11 KiB
Python
268 lines
11 KiB
Python
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, PortDisplaySpec
|
|
from app.simulation.core.metadata import ParameterDefinition, ResultVariableDefinition
|
|
from app.simulation.core.medium import IdealGasMedium
|
|
from app.simulation.core.ports import PortDefinition
|
|
|
|
|
|
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,
|
|
)
|
|
|
|
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.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)
|
|
|
|
@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}
|
|
|
|
|
|
class AmesimUd00(AlgebraicComponent):
|
|
"""AMESim UD00 piecewise-linear scalar signal source."""
|
|
|
|
MODEL_TYPE = "amesim_ud00"
|
|
MODEL_VERSION = "0.1.0"
|
|
PORTS = (PortDefinition.signal("out", nominal_role="output"),)
|
|
PARAMETERS = (
|
|
ParameterDefinition("tstart", 0.0, label="启动时间", quantity="time", unit="s"),
|
|
ParameterDefinition("start1", 0.0, label="第 1 段起点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("end1", 1.0, label="第 1 段终点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("t1", 1.0, label="第 1 段时长", quantity="time", unit="s", minimum=0.0),
|
|
ParameterDefinition("start2", 1.0, label="第 2 段起点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("end2", 1.0, label="第 2 段终点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("t2", 0.0, label="第 2 段时长", quantity="time", unit="s", minimum=0.0),
|
|
ParameterDefinition("start3", 1.0, label="第 3 段起点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("end3", 1.0, label="第 3 段终点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("t3", 0.0, label="第 3 段时长", quantity="time", unit="s", minimum=0.0),
|
|
ParameterDefinition("start4", 1.0, label="第 4 段起点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("end4", 1.0, label="第 4 段终点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("t4", 0.0, label="第 4 段时长", quantity="time", unit="s", minimum=0.0),
|
|
ParameterDefinition("start5", 1.0, label="第 5 段起点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("end5", 1.0, label="第 5 段终点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("t5", 0.0, label="第 5 段时长", quantity="time", unit="s", minimum=0.0),
|
|
ParameterDefinition("start6", 1.0, label="第 6 段起点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("end6", 1.0, label="第 6 段终点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("t6", 0.0, label="第 6 段时长", quantity="time", unit="s", minimum=0.0),
|
|
ParameterDefinition("start7", 1.0, label="第 7 段起点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("end7", 1.0, label="第 7 段终点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("t7", 0.0, label="第 7 段时长", quantity="time", unit="s", minimum=0.0),
|
|
ParameterDefinition("start8", 1.0, label="第 8 段起点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("end8", 1.0, label="第 8 段终点", quantity="dimensionless", unit=""),
|
|
ParameterDefinition("t8", 0.0, label="第 8 段时长", quantity="time", unit="s", minimum=0.0),
|
|
ParameterDefinition("nstages", 1.0, label="段数", quantity="dimensionless", unit="", minimum=1.0, maximum=8.0),
|
|
ParameterDefinition("iscyclic", 0.0, label="循环", quantity="dimensionless", unit="", minimum=0.0, maximum=1.0),
|
|
)
|
|
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,
|
|
)
|
|
|
|
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.")
|
|
if nstages < 1 or nstages > 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.nstages = int(nstages)
|
|
self.iscyclic = bool(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]
|
|
self.set_parameter_values(values)
|
|
self.out = self.register_declared_port("out")
|
|
self.out.signal = self.output_at(0.0)
|
|
|
|
@classmethod
|
|
def create(
|
|
cls,
|
|
*,
|
|
name: str,
|
|
medium: IdealGasMedium,
|
|
parameters: Mapping[str, float],
|
|
) -> "AmesimUd00":
|
|
nstages = parameters["nstages"]
|
|
iscyclic = parameters["iscyclic"]
|
|
if not float(nstages).is_integer():
|
|
raise ValueError("UD00 nstages must be an integer.")
|
|
if not float(iscyclic).is_integer():
|
|
raise ValueError("UD00 iscyclic must be 0 or 1.")
|
|
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}
|