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.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) ) 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.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: 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"] definitions = {definition.name: definition for definition in cls.PARAMETERS} 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.") 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}