flode 0.60.3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- flode/__init__.py +88 -0
- flode/analysis/__init__.py +37 -0
- flode/analysis/frequency_response.py +349 -0
- flode/analysis/linearize.py +714 -0
- flode/analysis/stability.py +229 -0
- flode/blocks/__init__.py +107 -0
- flode/blocks/_lti_utils.py +121 -0
- flode/blocks/cast.py +66 -0
- flode/blocks/continuous.py +474 -0
- flode/blocks/discontinuities.py +258 -0
- flode/blocks/discrete.py +612 -0
- flode/blocks/logic.py +120 -0
- flode/blocks/lookup.py +1025 -0
- flode/blocks/mathops.py +598 -0
- flode/blocks/pythonfunc.py +560 -0
- flode/blocks/pythonfunc_rewrite.py +1935 -0
- flode/blocks/pythonfunc_source.py +392 -0
- flode/blocks/random_source.py +179 -0
- flode/blocks/rounding.py +65 -0
- flode/blocks/routing.py +582 -0
- flode/blocks/sinks.py +309 -0
- flode/blocks/sources.py +215 -0
- flode/blocks/transport_delay.py +92 -0
- flode/blocks/userfunc.py +359 -0
- flode/core/__init__.py +5 -0
- flode/core/block.py +497 -0
- flode/core/decorator.py +1149 -0
- flode/core/dtypes.py +1026 -0
- flode/core/identifiers.py +157 -0
- flode/core/persistence.py +1319 -0
- flode/core/simulator.py +1690 -0
- flode/exceptions.py +226 -0
- flode/libraries/__init__.py +341 -0
- flode/libraries/_loader.py +302 -0
- flode/libraries/std.flwlib.json +347 -0
- flode/server/__init__.py +23 -0
- flode/server/app.py +185 -0
- flode/server/cli.py +555 -0
- flode/server/config.py +516 -0
- flode/server/errors.py +397 -0
- flode/server/library_registry.py +125 -0
- flode/server/migrations/__init__.py +80 -0
- flode/server/registry.py +950 -0
- flode/server/registry_translations.py +804 -0
- flode/server/routes/__init__.py +16 -0
- flode/server/routes/blocks.py +447 -0
- flode/server/routes/files.py +800 -0
- flode/server/routes/libraries.py +129 -0
- flode/server/routes/models.py +67 -0
- flode/server/routes/simulations.py +355 -0
- flode/server/runtime.py +354 -0
- flode/server/security/__init__.py +7 -0
- flode/server/security/origin.py +122 -0
- flode/server/security/paths.py +143 -0
- flode/server/settings.py +42 -0
- flode/server/static/.app-version +1 -0
- flode/server/static/assets/index-BnL2nNOe.css +1 -0
- flode/server/static/assets/index-DMWZBAA_.js +100 -0
- flode/server/static/assets/index-DMWZBAA_.js.map +1 -0
- flode/server/static/favicon.ico +0 -0
- flode/server/static/favicon.svg +9 -0
- flode/server/static/index.html +15 -0
- flode/subsystems/__init__.py +20 -0
- flode/subsystems/_mask.py +154 -0
- flode/subsystems/control_blocks.py +245 -0
- flode/subsystems/ports.py +146 -0
- flode/subsystems/subsystem.py +1173 -0
- flode-0.60.3.dist-info/METADATA +91 -0
- flode-0.60.3.dist-info/RECORD +73 -0
- flode-0.60.3.dist-info/WHEEL +5 -0
- flode-0.60.3.dist-info/entry_points.txt +2 -0
- flode-0.60.3.dist-info/licenses/LICENSE +21 -0
- flode-0.60.3.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""SPEC-0012 / ADR-0059 (v5.5.0): 不連続要素ブロック (Wave 2 第 1 弾)。
|
|
2
|
+
|
|
3
|
+
業界標準ブロック線図ツールの "Discontinuities" カテゴリ相当の **状態を持つ非線形要素**:
|
|
4
|
+
|
|
5
|
+
* ``RateLimiter`` — アクチュエータの slew rate (変化率) を制限する
|
|
6
|
+
* ``Relay`` — ヒステリシス付き ON / OFF スイッチ
|
|
7
|
+
|
|
8
|
+
両ブロックは sample_time gated 離散ブロック (`ZeroOrderHoldDirect` 同型の
|
|
9
|
+
1-state hold パターン) として実装する (ADR-0014 §(3))。`solve_ivp` 連続系
|
|
10
|
+
との見うち互換 (中間時刻は state hold、サンプル境界でのみ state 更新)。
|
|
11
|
+
|
|
12
|
+
設計の要点 (SPEC-0012 §機能要件):
|
|
13
|
+
|
|
14
|
+
* **n_states=1**: 前回出力値 (RateLimiter) または relay state (Relay、
|
|
15
|
+
1.0=ON / 0.0=OFF) を hold
|
|
16
|
+
* **direct_feedthrough=True**: サンプル時刻で現入力 ``u`` を参照
|
|
17
|
+
* **`reset()` lifecycle hook**: Relay の ``x0_state`` 文字列 enum から state
|
|
18
|
+
を再初期化 (SPEC-0010 RandomSource と同パターン、Simulator.run() の
|
|
19
|
+
`if hasattr(b, "reset"): b.reset()` 経路、simulator.py:1004-1006)
|
|
20
|
+
* **x0=0 placeholder の代わりに明示的 x0**: RateLimiter は ``x0`` 数値、
|
|
21
|
+
Relay は ``x0_state`` enum で「ユーザー意図の初期状態」を保持。
|
|
22
|
+
Simulator.run() の [A'] update → [A] output 順序 (ADR-0015) でも、
|
|
23
|
+
本ブロックは「現入力が前 state から離れていない限り state を保つ」設計
|
|
24
|
+
のため placeholder ではなく実際の初期状態を持つ
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
from typing import Any
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
import numpy.typing as npt
|
|
33
|
+
|
|
34
|
+
from ..core.block import Block
|
|
35
|
+
from ..exceptions import BlockSpecError
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class RateLimiter(Block):
|
|
39
|
+
"""アクチュエータの変化率 (slew rate) を制限する離散ブロック。
|
|
40
|
+
|
|
41
|
+
サンプル時刻 ``t_k = k * sample_time`` で:
|
|
42
|
+
|
|
43
|
+
.. code-block:: text
|
|
44
|
+
|
|
45
|
+
max_delta_up = rising_slew_rate * sample_time # > 0
|
|
46
|
+
max_delta_down = falling_slew_rate * sample_time # < 0
|
|
47
|
+
delta_clipped = clip(u(t_k) - x_prev, max_delta_down, max_delta_up)
|
|
48
|
+
x_new = x_prev + delta_clipped
|
|
49
|
+
|
|
50
|
+
中間時刻 ``t ∈ (t_k, t_{k+1})`` では出力 = 前 sample 値の hold。
|
|
51
|
+
``solve_ivp`` の可変ステップ再評価でも同一 ``t`` で同一値を返す
|
|
52
|
+
(積分の連続性 + 再現性)。
|
|
53
|
+
|
|
54
|
+
Args:
|
|
55
|
+
rising_slew_rate: 立ち上がり最大変化率 [unit/s]、``> 0`` 必須
|
|
56
|
+
(既定 1.0)。
|
|
57
|
+
falling_slew_rate: 立ち下がり最大変化率 [unit/s]、``< 0`` 必須
|
|
58
|
+
(既定 -1.0、対称デフォルト)。
|
|
59
|
+
sample_time: サンプル周期 [s]、``> 0`` 必須。
|
|
60
|
+
x0: 初期出力値 (既定 0.0)。
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
BlockSpecError: ``rising_slew_rate <= 0``、``falling_slew_rate >= 0``、
|
|
64
|
+
``sample_time <= 0``。
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
def __init__(
|
|
68
|
+
self,
|
|
69
|
+
*,
|
|
70
|
+
sample_time: float,
|
|
71
|
+
rising_slew_rate: float = 1.0,
|
|
72
|
+
falling_slew_rate: float = -1.0,
|
|
73
|
+
x0: float = 0.0,
|
|
74
|
+
id: str | None = None,
|
|
75
|
+
name: str | None = None,
|
|
76
|
+
) -> None:
|
|
77
|
+
if not isinstance(sample_time, (int, float)) or isinstance(sample_time, bool):
|
|
78
|
+
raise BlockSpecError(
|
|
79
|
+
f"RateLimiter: sample_time must be a number, got {type(sample_time).__name__}"
|
|
80
|
+
)
|
|
81
|
+
if sample_time <= 0.0:
|
|
82
|
+
raise BlockSpecError(f"RateLimiter: sample_time must be > 0, got {sample_time}")
|
|
83
|
+
if rising_slew_rate <= 0.0:
|
|
84
|
+
raise BlockSpecError(
|
|
85
|
+
f"RateLimiter: rising_slew_rate must be > 0, got {rising_slew_rate}"
|
|
86
|
+
)
|
|
87
|
+
if falling_slew_rate >= 0.0:
|
|
88
|
+
raise BlockSpecError(
|
|
89
|
+
f"RateLimiter: falling_slew_rate must be < 0, got {falling_slew_rate}"
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
super().__init__(
|
|
93
|
+
id=id,
|
|
94
|
+
name=name,
|
|
95
|
+
n_inputs=1,
|
|
96
|
+
n_outputs=1,
|
|
97
|
+
n_states=1,
|
|
98
|
+
direct_feedthrough=True,
|
|
99
|
+
sample_time=sample_time,
|
|
100
|
+
)
|
|
101
|
+
self.rising_slew_rate = float(rising_slew_rate)
|
|
102
|
+
self.falling_slew_rate = float(falling_slew_rate)
|
|
103
|
+
self.x0 = np.array([float(x0)])
|
|
104
|
+
self._params: dict[str, Any] = {
|
|
105
|
+
"sample_time": float(sample_time),
|
|
106
|
+
"rising_slew_rate": self.rising_slew_rate,
|
|
107
|
+
"falling_slew_rate": self.falling_slew_rate,
|
|
108
|
+
"x0": float(x0),
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
|
112
|
+
# state hold: サンプル境界以外でも中間時刻でも state 値を返す。
|
|
113
|
+
return np.array([float(x[0])])
|
|
114
|
+
|
|
115
|
+
def update(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
|
116
|
+
# サンプル境界でのみ Simulator が呼ぶ。slew 制限を適用して新 state を返す。
|
|
117
|
+
# DiscreteIntegrator と同様に _resolved_sample_time が解決済か確認。
|
|
118
|
+
ts = self._resolved_sample_time
|
|
119
|
+
if ts is None or ts <= 0.0:
|
|
120
|
+
raise BlockSpecError(
|
|
121
|
+
f"RateLimiter {self.id!r}: sample_time has not been resolved. "
|
|
122
|
+
"Add this block to a Simulator and call run() (or invoke "
|
|
123
|
+
"_resolve_sample_times) before calling update() directly."
|
|
124
|
+
)
|
|
125
|
+
# rising_slew_rate > 0 / falling_slew_rate < 0 は __init__ で検証済 (= ここでは
|
|
126
|
+
# 不変条件として再検証しない)。direct な attribute 書き換えで反転させた場合
|
|
127
|
+
# は clip 方向が崩れるが、ユーザー設計時のミスとして許容する。
|
|
128
|
+
max_up = self.rising_slew_rate * ts
|
|
129
|
+
max_down = self.falling_slew_rate * ts
|
|
130
|
+
delta = float(u[0]) - float(x[0])
|
|
131
|
+
# clip(delta, max_down, max_up) — max_down は負、max_up は正。
|
|
132
|
+
if delta > max_up:
|
|
133
|
+
delta = max_up
|
|
134
|
+
elif delta < max_down:
|
|
135
|
+
delta = max_down
|
|
136
|
+
return np.array([float(x[0]) + delta])
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Relay(Block):
|
|
140
|
+
"""ヒステリシス付き ON / OFF スイッチ。
|
|
141
|
+
|
|
142
|
+
サンプル時刻で:
|
|
143
|
+
|
|
144
|
+
.. code-block:: text
|
|
145
|
+
|
|
146
|
+
if state == OFF and u(t_k) >= switch_on_point:
|
|
147
|
+
state = ON
|
|
148
|
+
elif state == ON and u(t_k) <= switch_off_point:
|
|
149
|
+
state = OFF
|
|
150
|
+
y = output_on if state == ON else output_off
|
|
151
|
+
|
|
152
|
+
内部状態は float ``1.0`` = ON / ``0.0`` = OFF で保持 (state vector に
|
|
153
|
+
float でしか入らないため)。中間時刻は前回 sample 値を hold。
|
|
154
|
+
|
|
155
|
+
Args:
|
|
156
|
+
switch_on_point: ON へ遷移する入力しきい値 (既定 0.5)。
|
|
157
|
+
switch_off_point: OFF へ遷移する入力しきい値 (既定 -0.5)。
|
|
158
|
+
``< switch_on_point`` 必須 (= hysteresis band 形成)。
|
|
159
|
+
output_on: ON 状態での出力値 (既定 1.0)。
|
|
160
|
+
output_off: OFF 状態での出力値 (既定 0.0)。
|
|
161
|
+
sample_time: サンプル周期 [s]、``> 0`` 必須。
|
|
162
|
+
x0_state: 初期状態 ``"on"`` または ``"off"`` (既定 ``"off"``)。
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
BlockSpecError: ``switch_off_point >= switch_on_point``、
|
|
166
|
+
``sample_time <= 0``、``x0_state`` enum 値外。
|
|
167
|
+
"""
|
|
168
|
+
|
|
169
|
+
_ALLOWED_X0_STATES: tuple[str, ...] = ("on", "off")
|
|
170
|
+
# ADR-0019 / ADR-0039 follow-up: GUI ParameterPanel が enum select を出すヒント
|
|
171
|
+
_param_enums = {"x0_state": _ALLOWED_X0_STATES}
|
|
172
|
+
|
|
173
|
+
def __init__(
|
|
174
|
+
self,
|
|
175
|
+
*,
|
|
176
|
+
sample_time: float,
|
|
177
|
+
switch_on_point: float = 0.5,
|
|
178
|
+
switch_off_point: float = -0.5,
|
|
179
|
+
output_on: float = 1.0,
|
|
180
|
+
output_off: float = 0.0,
|
|
181
|
+
x0_state: str = "off",
|
|
182
|
+
id: str | None = None,
|
|
183
|
+
name: str | None = None,
|
|
184
|
+
) -> None:
|
|
185
|
+
if x0_state not in self._ALLOWED_X0_STATES:
|
|
186
|
+
raise BlockSpecError(
|
|
187
|
+
f"Relay: x0_state must be one of {self._ALLOWED_X0_STATES}, got {x0_state!r}"
|
|
188
|
+
)
|
|
189
|
+
if not isinstance(sample_time, (int, float)) or isinstance(sample_time, bool):
|
|
190
|
+
raise BlockSpecError(
|
|
191
|
+
f"Relay: sample_time must be a number, got {type(sample_time).__name__}"
|
|
192
|
+
)
|
|
193
|
+
if sample_time <= 0.0:
|
|
194
|
+
raise BlockSpecError(f"Relay: sample_time must be > 0, got {sample_time}")
|
|
195
|
+
if not (switch_off_point < switch_on_point):
|
|
196
|
+
raise BlockSpecError(
|
|
197
|
+
f"Relay: switch_off_point ({switch_off_point}) must be "
|
|
198
|
+
f"< switch_on_point ({switch_on_point}) for hysteresis"
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
super().__init__(
|
|
202
|
+
id=id,
|
|
203
|
+
name=name,
|
|
204
|
+
n_inputs=1,
|
|
205
|
+
n_outputs=1,
|
|
206
|
+
n_states=1,
|
|
207
|
+
direct_feedthrough=True,
|
|
208
|
+
sample_time=sample_time,
|
|
209
|
+
)
|
|
210
|
+
self.switch_on_point = float(switch_on_point)
|
|
211
|
+
self.switch_off_point = float(switch_off_point)
|
|
212
|
+
self.output_on = float(output_on)
|
|
213
|
+
self.output_off = float(output_off)
|
|
214
|
+
self.x0_state = x0_state
|
|
215
|
+
# 初期 state を enum から float に変換 (RandomSource パターン参照)。
|
|
216
|
+
self.x0 = self._x0_from_state()
|
|
217
|
+
|
|
218
|
+
self._params: dict[str, Any] = {
|
|
219
|
+
"sample_time": float(sample_time),
|
|
220
|
+
"switch_on_point": self.switch_on_point,
|
|
221
|
+
"switch_off_point": self.switch_off_point,
|
|
222
|
+
"output_on": self.output_on,
|
|
223
|
+
"output_off": self.output_off,
|
|
224
|
+
"x0_state": x0_state,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
def _x0_from_state(self) -> npt.NDArray[Any]:
|
|
228
|
+
"""``x0_state`` enum から float 1-vector を構築する (DRY helper)。
|
|
229
|
+
|
|
230
|
+
``__init__`` と ``reset()`` の両方から呼ばれ、将来 enum 値が増えても
|
|
231
|
+
変更箇所を 1 か所に集約する。
|
|
232
|
+
"""
|
|
233
|
+
return np.array([1.0 if self.x0_state == "on" else 0.0])
|
|
234
|
+
|
|
235
|
+
def reset(self) -> None:
|
|
236
|
+
"""``Simulator.run()`` 開始時の lifecycle hook (simulator.py:1004-1006)。
|
|
237
|
+
|
|
238
|
+
``x0`` を ``x0_state`` enum から再構築する。同一 Simulator で
|
|
239
|
+
``run()`` を複数回呼んでも、各 run で同一初期 state から開始する
|
|
240
|
+
(= bit-identical 再現性)。
|
|
241
|
+
"""
|
|
242
|
+
self.x0 = self._x0_from_state()
|
|
243
|
+
|
|
244
|
+
def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
|
245
|
+
# state hold: x[0] (= 0.0 or 1.0) を見て output_on / output_off を選ぶ。
|
|
246
|
+
# 0.5 を境にした分岐で float 精度 (機械精度) の境界揺らぎを許容。
|
|
247
|
+
return np.array([self.output_on if x[0] > 0.5 else self.output_off])
|
|
248
|
+
|
|
249
|
+
def update(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
|
250
|
+
# サンプル境界で遷移ロジックを評価する。
|
|
251
|
+
current_on = x[0] > 0.5
|
|
252
|
+
u_val = float(u[0])
|
|
253
|
+
if not current_on and u_val >= self.switch_on_point:
|
|
254
|
+
return np.array([1.0])
|
|
255
|
+
if current_on and u_val <= self.switch_off_point:
|
|
256
|
+
return np.array([0.0])
|
|
257
|
+
# 遷移条件を満たさなければ state 維持。
|
|
258
|
+
return np.array([float(x[0])])
|