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.
Files changed (73) hide show
  1. flode/__init__.py +88 -0
  2. flode/analysis/__init__.py +37 -0
  3. flode/analysis/frequency_response.py +349 -0
  4. flode/analysis/linearize.py +714 -0
  5. flode/analysis/stability.py +229 -0
  6. flode/blocks/__init__.py +107 -0
  7. flode/blocks/_lti_utils.py +121 -0
  8. flode/blocks/cast.py +66 -0
  9. flode/blocks/continuous.py +474 -0
  10. flode/blocks/discontinuities.py +258 -0
  11. flode/blocks/discrete.py +612 -0
  12. flode/blocks/logic.py +120 -0
  13. flode/blocks/lookup.py +1025 -0
  14. flode/blocks/mathops.py +598 -0
  15. flode/blocks/pythonfunc.py +560 -0
  16. flode/blocks/pythonfunc_rewrite.py +1935 -0
  17. flode/blocks/pythonfunc_source.py +392 -0
  18. flode/blocks/random_source.py +179 -0
  19. flode/blocks/rounding.py +65 -0
  20. flode/blocks/routing.py +582 -0
  21. flode/blocks/sinks.py +309 -0
  22. flode/blocks/sources.py +215 -0
  23. flode/blocks/transport_delay.py +92 -0
  24. flode/blocks/userfunc.py +359 -0
  25. flode/core/__init__.py +5 -0
  26. flode/core/block.py +497 -0
  27. flode/core/decorator.py +1149 -0
  28. flode/core/dtypes.py +1026 -0
  29. flode/core/identifiers.py +157 -0
  30. flode/core/persistence.py +1319 -0
  31. flode/core/simulator.py +1690 -0
  32. flode/exceptions.py +226 -0
  33. flode/libraries/__init__.py +341 -0
  34. flode/libraries/_loader.py +302 -0
  35. flode/libraries/std.flwlib.json +347 -0
  36. flode/server/__init__.py +23 -0
  37. flode/server/app.py +185 -0
  38. flode/server/cli.py +555 -0
  39. flode/server/config.py +516 -0
  40. flode/server/errors.py +397 -0
  41. flode/server/library_registry.py +125 -0
  42. flode/server/migrations/__init__.py +80 -0
  43. flode/server/registry.py +950 -0
  44. flode/server/registry_translations.py +804 -0
  45. flode/server/routes/__init__.py +16 -0
  46. flode/server/routes/blocks.py +447 -0
  47. flode/server/routes/files.py +800 -0
  48. flode/server/routes/libraries.py +129 -0
  49. flode/server/routes/models.py +67 -0
  50. flode/server/routes/simulations.py +355 -0
  51. flode/server/runtime.py +354 -0
  52. flode/server/security/__init__.py +7 -0
  53. flode/server/security/origin.py +122 -0
  54. flode/server/security/paths.py +143 -0
  55. flode/server/settings.py +42 -0
  56. flode/server/static/.app-version +1 -0
  57. flode/server/static/assets/index-BnL2nNOe.css +1 -0
  58. flode/server/static/assets/index-DMWZBAA_.js +100 -0
  59. flode/server/static/assets/index-DMWZBAA_.js.map +1 -0
  60. flode/server/static/favicon.ico +0 -0
  61. flode/server/static/favicon.svg +9 -0
  62. flode/server/static/index.html +15 -0
  63. flode/subsystems/__init__.py +20 -0
  64. flode/subsystems/_mask.py +154 -0
  65. flode/subsystems/control_blocks.py +245 -0
  66. flode/subsystems/ports.py +146 -0
  67. flode/subsystems/subsystem.py +1173 -0
  68. flode-0.60.3.dist-info/METADATA +91 -0
  69. flode-0.60.3.dist-info/RECORD +73 -0
  70. flode-0.60.3.dist-info/WHEEL +5 -0
  71. flode-0.60.3.dist-info/entry_points.txt +2 -0
  72. flode-0.60.3.dist-info/licenses/LICENSE +21 -0
  73. flode-0.60.3.dist-info/top_level.txt +1 -0
@@ -0,0 +1,474 @@
1
+ """連続時間 LTI ブロック (``Integrator`` / ``StateSpace`` / ``TransferFunction`` /
2
+ ``MimoTransferFunction`` / ``Derivative``)。
3
+
4
+ - ADR-0006: SISO ``TransferFunction`` の SS 化方針 (scipy.signal.tf2ss 利用)
5
+ - ADR-0010 §(2)、ADR-0016 Phase 3 #1: ``MimoTransferFunction`` (共通分母 MIMO TF、
6
+ 自前 controllable canonical form 構築で scipy ゼロ多項式バグ回避)
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Any, ClassVar
12
+
13
+ import numpy as np
14
+ import numpy.typing as npt
15
+ import scipy.signal
16
+
17
+ from ..core.block import Block
18
+ from ..exceptions import BlockSpecError
19
+ from ._lti_utils import _DF_TOLERANCE, build_companion_form_siso
20
+
21
+
22
+ class Integrator(Block):
23
+ """連続時間積分器 ``y = x``、``x_dot = u``。
24
+
25
+ ``direct_feedthrough=False`` (出力は状態 ``x`` のみ参照) なので、閉ループ内
26
+ の代数ループを切る用途に使える。
27
+
28
+ Args:
29
+ x0: 初期状態 ``x(0)``。
30
+ """
31
+
32
+ # D-4 (SPEC-0028 Q11): 連続ブロックは入力に float64 を要求する
33
+ required_input_dtype: ClassVar[str | None] = "float64"
34
+
35
+ def __init__(
36
+ self,
37
+ x0: float = 0.0,
38
+ *,
39
+ id: str | None = None,
40
+ name: str | None = None,
41
+ ):
42
+ super().__init__(
43
+ id=id,
44
+ name=name,
45
+ n_inputs=1,
46
+ n_outputs=1,
47
+ n_states=1,
48
+ direct_feedthrough=False,
49
+ )
50
+ self.x0 = np.array([float(x0)])
51
+ self._params = {"x0": float(x0)}
52
+
53
+ def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
54
+ return np.array([x[0]])
55
+
56
+ def derivative(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
57
+ return np.array([u[0]])
58
+
59
+
60
+ class StateSpace(Block):
61
+ """連続 LTI 状態空間 ``x_dot = A x + B u``、``y = C x + D u``。
62
+
63
+ ``direct_feedthrough`` は ``D`` 行列の最大絶対値が ``1e-12`` を超えるかで
64
+ 自動推論する (ADR-0006 §(6))。
65
+
66
+ Args:
67
+ A: システム行列 (shape ``(n, n)``)。
68
+ B: 入力行列 (shape ``(n, m)``)、``m = n_inputs``。
69
+ C: 出力行列 (shape ``(p, n)``)、``p = n_outputs``。
70
+ D: 直達行列 (shape ``(p, m)``)。``None`` のときゼロ行列。
71
+ x0: 初期状態 (shape ``(n,)``)。``None`` のときゼロ。
72
+ """
73
+
74
+ # D-4 (SPEC-0028 Q11): 連続ブロックは入力に float64 を要求する
75
+ required_input_dtype: ClassVar[str | None] = "float64"
76
+
77
+ def __init__(
78
+ self,
79
+ A: npt.NDArray[Any],
80
+ B: npt.NDArray[Any],
81
+ C: npt.NDArray[Any],
82
+ D: npt.NDArray[Any] | None = None,
83
+ x0: npt.NDArray[Any] | None = None,
84
+ *,
85
+ id: str | None = None,
86
+ name: str | None = None,
87
+ ):
88
+ A_arr = np.asarray(A, dtype=float)
89
+ B_arr = np.asarray(B, dtype=float)
90
+ C_arr = np.asarray(C, dtype=float)
91
+ if A_arr.ndim != 2 or A_arr.shape[0] != A_arr.shape[1]:
92
+ raise BlockSpecError(f"StateSpace: A must be square, got shape {A_arr.shape}")
93
+ n = A_arr.shape[0]
94
+ if n == 0:
95
+ raise BlockSpecError(
96
+ "StateSpace: n_states=0 (pure gain) is not supported; use a static "
97
+ "block (e.g. Gain) instead"
98
+ )
99
+ if B_arr.ndim != 2 or B_arr.shape[0] != n:
100
+ raise BlockSpecError(
101
+ f"StateSpace: B must have shape (n, m) with n={n}, got {B_arr.shape}"
102
+ )
103
+ m = B_arr.shape[1]
104
+ if C_arr.ndim != 2 or C_arr.shape[1] != n:
105
+ raise BlockSpecError(
106
+ f"StateSpace: C must have shape (p, n) with n={n}, got {C_arr.shape}"
107
+ )
108
+ p = C_arr.shape[0]
109
+ D_arr: npt.NDArray[Any]
110
+ if D is None:
111
+ D_arr = np.zeros((p, m))
112
+ else:
113
+ D_arr = np.asarray(D, dtype=float)
114
+ if D_arr.shape != (p, m):
115
+ raise BlockSpecError(f"StateSpace: D must have shape ({p}, {m}), got {D_arr.shape}")
116
+ df = bool(np.max(np.abs(D_arr)) > _DF_TOLERANCE) if D_arr.size else False
117
+
118
+ super().__init__(
119
+ id=id,
120
+ name=name,
121
+ n_inputs=m,
122
+ n_outputs=p,
123
+ n_states=n,
124
+ direct_feedthrough=df,
125
+ )
126
+ self._A = A_arr
127
+ self._B = B_arr
128
+ self._C = C_arr
129
+ self._D = D_arr
130
+ if x0 is None:
131
+ self.x0 = np.zeros(n)
132
+ else:
133
+ x0_arr = np.atleast_1d(np.asarray(x0, dtype=float))
134
+ if x0_arr.shape != (n,):
135
+ raise BlockSpecError(f"StateSpace: x0 must have shape ({n},), got {x0_arr.shape}")
136
+ self.x0 = x0_arr
137
+ self._params = {
138
+ "A": A_arr,
139
+ "B": B_arr,
140
+ "C": C_arr,
141
+ "D": D_arr,
142
+ "x0": self.x0,
143
+ }
144
+
145
+ def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
146
+ return np.asarray(self._C @ x + self._D @ u, dtype=float).ravel()
147
+
148
+ def derivative(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
149
+ return np.asarray(self._A @ x + self._B @ u, dtype=float).ravel()
150
+
151
+
152
+ class TransferFunction(Block):
153
+ """連続 LTI 伝達関数 ``H(s) = num(s) / den(s)`` (SISO)。
154
+
155
+ 内部で ``scipy.signal.tf2ss`` により制御正準形 SS に変換して実装する
156
+ (ADR-0006 §(1))。``deg(num) <= deg(den)`` を要求 (improper は不可)。
157
+
158
+ Args:
159
+ numerator: 分子多項式の係数 (s の降べき、numpy convention)。
160
+ denominator: 分母多項式の係数。
161
+ x0: 初期状態 (shape ``(len(denominator)-1,)``)。
162
+ """
163
+
164
+ # D-4 (SPEC-0028 Q11): 連続ブロックは入力に float64 を要求する
165
+ required_input_dtype: ClassVar[str | None] = "float64"
166
+
167
+ def __init__(
168
+ self,
169
+ numerator: npt.NDArray[Any] | list[float],
170
+ denominator: npt.NDArray[Any] | list[float],
171
+ x0: npt.NDArray[Any] | None = None,
172
+ *,
173
+ id: str | None = None,
174
+ name: str | None = None,
175
+ ):
176
+ num = np.asarray(numerator, dtype=float).ravel()
177
+ den = np.asarray(denominator, dtype=float).ravel()
178
+ if num.size == 0 or den.size == 0:
179
+ raise BlockSpecError("TransferFunction: numerator/denominator must be non-empty")
180
+ if den[0] == 0.0:
181
+ raise BlockSpecError(
182
+ "TransferFunction: leading coefficient of denominator must be non-zero"
183
+ )
184
+ if np.all(num == 0.0):
185
+ raise BlockSpecError("TransferFunction: numerator cannot be all zeros")
186
+ # プロパー性: deg(num) > deg(den) は improper として拒否
187
+ # numpy convention: deg = len - 1
188
+ if (num.size - 1) > (den.size - 1):
189
+ raise BlockSpecError(
190
+ f"TransferFunction: improper system (deg(num)={num.size - 1} > "
191
+ f"deg(den)={den.size - 1}). Only proper or biproper systems are supported."
192
+ )
193
+
194
+ A, B, C, D = scipy.signal.tf2ss(num, den)
195
+ A = np.atleast_2d(np.asarray(A, dtype=float))
196
+ B = np.atleast_2d(np.asarray(B, dtype=float))
197
+ C = np.atleast_2d(np.asarray(C, dtype=float))
198
+ D = np.atleast_2d(np.asarray(D, dtype=float))
199
+ n = A.shape[0]
200
+ df = bool(np.max(np.abs(D)) > _DF_TOLERANCE) if D.size else False
201
+
202
+ super().__init__(
203
+ id=id,
204
+ name=name,
205
+ n_inputs=1,
206
+ n_outputs=1,
207
+ n_states=n,
208
+ direct_feedthrough=df,
209
+ )
210
+ self._A = A
211
+ self._B = B
212
+ self._C = C
213
+ self._D = D
214
+ self.numerator = num
215
+ self.denominator = den
216
+ if x0 is None:
217
+ self.x0 = np.zeros(n)
218
+ else:
219
+ x0_arr = np.atleast_1d(np.asarray(x0, dtype=float))
220
+ if x0_arr.shape != (n,):
221
+ raise BlockSpecError(
222
+ f"TransferFunction: x0 must have shape ({n},), got {x0_arr.shape}"
223
+ )
224
+ self.x0 = x0_arr
225
+ self._params = {
226
+ "numerator": num,
227
+ "denominator": den,
228
+ "x0": self.x0,
229
+ }
230
+
231
+ def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
232
+ # C: (1,n)、x: (n,)、D: (1,1)、u: (1,)
233
+ y = self._C @ x + self._D @ u
234
+ return np.asarray(y, dtype=float).ravel()
235
+
236
+ def derivative(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
237
+ return np.asarray(self._A @ x + self._B @ u, dtype=float).ravel()
238
+
239
+
240
+ class MimoTransferFunction(Block):
241
+ """連続 LTI MIMO 伝達関数 ``H(s) = N(s) / d(s)`` (共通分母版、ADR-0010 §(2)、ADR-0016)。
242
+
243
+ 入力 ``q`` 個 / 出力 ``p`` 個の伝達関数行列を扱う。共通分母 ``d(s)`` (1D) と、
244
+ 分子多項式行列 ``N(s) = [[n_{ij}(s)]]`` (3D list) を受け取る:
245
+
246
+ .. code-block:: text
247
+
248
+ H[i][j](s) = numerators[i][j] / denominator
249
+ i in [0, p)、j in [0, q)
250
+ 各 numerators[i][j] は s の降べき多項式係数。
251
+ denominator は s の降べき多項式係数 (共通)。
252
+
253
+ ADR-0010 §(7) で予告された companion form **自前構築** で実装する
254
+ (``scipy.signal.tf2ss`` のゼロ多項式時の ``BadCoefficients`` warning + 不正な
255
+ B/C/D 返却問題を回避)。
256
+
257
+ Internal realization:
258
+ 各 (i, j) の SISO 伝達関数 ``numerators[i][j] / denominator`` を
259
+ ``build_companion_form_siso`` で SS 化し、block-diagonal で結合する
260
+ (Option 2、ADR-0016 §(1) #1)。state size は ``p * q * n`` (n = deg(den))。
261
+
262
+ - ``A``: shape ``(p*q*n, p*q*n)``、各 ``A_{ij}`` を blkdiag 配置
263
+ - ``B``: shape ``(p*q*n, q)``、入力 j のみがブロック (i, j) の B に接続
264
+ - ``C``: shape ``(p, p*q*n)``、出力 i は ブロック (i, j) for all j の C を集約
265
+ - ``D``: shape ``(p, q)``、各 (i, j) で biproper なら非ゼロ
266
+
267
+ SM-A 信号モデル (ADR-0010 §(1)) に従い、ポートは ``q`` 個のスカラー入力 +
268
+ ``p`` 個のスカラー出力。
269
+
270
+ Phase 3 では **共通分母版のみ** をサポート (各 (i, j) で異なる分母を許容する
271
+ 独立分母版は Phase 4+ で再検討、ADR-0010 §(2))。
272
+
273
+ Args:
274
+ numerators: 3D list ``[p][q][num_coefs]``。``numerators[i][j]`` は出力 i に
275
+ 対する入力 j からの分子多項式係数 (s の降べき)。``proper`` であること
276
+ (各 ``len(numerators[i][j]) <= len(denominator)``)。``[0.0]`` (ゼロ
277
+ 多項式) を許容。
278
+ denominator: 共通分母多項式の係数 (s の降べき、1D)。``denominator[0] != 0``
279
+ 必須。``deg(denominator) >= 1`` 必須 (純粋ゲインは ``Gain`` を使う)。
280
+ x0: 初期状態 (shape ``(p*q*n,)``)。``None`` のときゼロ。なお
281
+ ``numerators[i][j] == [0.0]`` の (i, j) に対応する状態は dead state で
282
+ あり、その位置に非ゼロ値を入れても入出力に影響しない。
283
+
284
+ Raises:
285
+ BlockSpecError: 形状不正、空配列、improper system、零分母など。
286
+ """
287
+
288
+ # D-4 (SPEC-0028 Q11): 連続ブロックは入力に float64 を要求する
289
+ required_input_dtype: ClassVar[str | None] = "float64"
290
+
291
+ def __init__(
292
+ self,
293
+ numerators: list[list[list[float]]],
294
+ denominator: npt.NDArray[Any] | list[float],
295
+ x0: npt.NDArray[Any] | None = None,
296
+ *,
297
+ id: str | None = None,
298
+ name: str | None = None,
299
+ ):
300
+ # --- 入力 validation ---
301
+ if not isinstance(numerators, (list, tuple)) or len(numerators) == 0:
302
+ raise BlockSpecError(
303
+ "MimoTransferFunction: numerators must be a non-empty list of rows"
304
+ )
305
+ p = len(numerators)
306
+ if not isinstance(numerators[0], (list, tuple)):
307
+ raise BlockSpecError(
308
+ f"MimoTransferFunction: numerators[0] must be a list, "
309
+ f"got {type(numerators[0]).__name__}"
310
+ )
311
+ q = len(numerators[0])
312
+ if q == 0:
313
+ raise BlockSpecError(
314
+ "MimoTransferFunction: numerators[0] must be a non-empty list of polynomials"
315
+ )
316
+ for i, row in enumerate(numerators):
317
+ if not isinstance(row, (list, tuple)) or len(row) != q:
318
+ raise BlockSpecError(
319
+ f"MimoTransferFunction: numerators[{i}] must be a list of length {q}, "
320
+ f"got {len(row) if isinstance(row, (list, tuple)) else type(row).__name__}"
321
+ )
322
+ for j, poly in enumerate(row):
323
+ if not isinstance(poly, (list, tuple, np.ndarray)) or len(poly) == 0:
324
+ raise BlockSpecError(
325
+ f"MimoTransferFunction: numerators[{i}][{j}] must be a non-empty "
326
+ f"sequence of polynomial coefficients"
327
+ )
328
+
329
+ den = np.asarray(denominator, dtype=float).ravel()
330
+ if den.size == 0:
331
+ raise BlockSpecError("MimoTransferFunction: denominator must be non-empty")
332
+ if den[0] == 0.0:
333
+ raise BlockSpecError(
334
+ "MimoTransferFunction: leading coefficient of denominator must be non-zero"
335
+ )
336
+ n = den.size - 1 # deg(den)
337
+ if n == 0:
338
+ raise BlockSpecError(
339
+ "MimoTransferFunction: deg(denominator)=0 (pure gain) is not supported; "
340
+ "use a Gain block instead"
341
+ )
342
+
343
+ # --- 各 (i, j) を SISO companion realization に変換 ---
344
+ # block-diagonal A (p*q*n, p*q*n)、B / C / D は非ゼロ要素のみ書き込む
345
+ n_total = p * q * n
346
+ A = np.zeros((n_total, n_total), dtype=float)
347
+ B = np.zeros((n_total, q), dtype=float)
348
+ C = np.zeros((p, n_total), dtype=float)
349
+ D = np.zeros((p, q), dtype=float)
350
+
351
+ for i in range(p):
352
+ for j in range(q):
353
+ num_ij = np.asarray(numerators[i][j], dtype=float).ravel()
354
+ # ゼロ多項式 (entire numerator polynomial = 0): skip realization,
355
+ # この (i, j) は zero contribution。state を holding するだけ。
356
+ if np.all(num_ij == 0.0):
357
+ # A の block-diagonal 部分は denominator のみで決まるため、
358
+ # ゼロ numerator でも poles は持つ (autonomous decay)。ただし
359
+ # B[block, j] = 0、C[i, block] = 0 なので入出力に寄与しない。
360
+ A_ij, _, _, _ = build_companion_form_siso(np.array([0.0]), den)
361
+ block_start = (i * q + j) * n
362
+ block_end = block_start + n
363
+ A[block_start:block_end, block_start:block_end] = A_ij
364
+ # B / C / D はゼロのまま (この block は dead state)
365
+ continue
366
+
367
+ if num_ij.size > den.size:
368
+ raise BlockSpecError(
369
+ f"MimoTransferFunction: numerators[{i}][{j}] has degree "
370
+ f"{num_ij.size - 1} > deg(denominator)={n} (improper). "
371
+ f"Only proper or biproper systems are supported."
372
+ )
373
+
374
+ A_ij, B_ij, C_ij, D_ij = build_companion_form_siso(num_ij, den)
375
+ block_start = (i * q + j) * n
376
+ block_end = block_start + n
377
+ A[block_start:block_end, block_start:block_end] = A_ij
378
+ B[block_start:block_end, j : j + 1] = B_ij
379
+ C[i : i + 1, block_start:block_end] = C_ij
380
+ D[i, j] = float(D_ij[0, 0])
381
+
382
+ df = bool(np.max(np.abs(D)) > _DF_TOLERANCE) if D.size else False
383
+
384
+ super().__init__(
385
+ id=id,
386
+ name=name,
387
+ n_inputs=q,
388
+ n_outputs=p,
389
+ n_states=n_total,
390
+ direct_feedthrough=df,
391
+ )
392
+ self._A = A
393
+ self._B = B
394
+ self._C = C
395
+ self._D = D
396
+ self._p = p
397
+ self._q = q
398
+ self._n_per_block = n
399
+ # JSON serialize 用に元の入力を保持 (scalar / list 形式、ADR-0008)
400
+ self._numerators_raw = [
401
+ [np.asarray(numerators[i][j], dtype=float) for j in range(q)] for i in range(p)
402
+ ]
403
+ self._denominator = den
404
+
405
+ if x0 is None:
406
+ self.x0 = np.zeros(n_total)
407
+ else:
408
+ x0_arr = np.atleast_1d(np.asarray(x0, dtype=float))
409
+ if x0_arr.shape != (n_total,):
410
+ raise BlockSpecError(
411
+ f"MimoTransferFunction: x0 must have shape ({n_total},), got {x0_arr.shape}"
412
+ )
413
+ self.x0 = x0_arr
414
+
415
+ # JSON 表現は input list of lists のまま保存 (ADR-0008、3D list-of-lists)
416
+ self._params = {
417
+ "numerators": [[list(self._numerators_raw[i][j]) for j in range(q)] for i in range(p)],
418
+ "denominator": list(den),
419
+ "x0": self.x0,
420
+ }
421
+
422
+ def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
423
+ return np.asarray(self._C @ x + self._D @ u, dtype=float).ravel()
424
+
425
+ def derivative(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
426
+ return np.asarray(self._A @ x + self._B @ u, dtype=float).ravel()
427
+
428
+
429
+ class Derivative(Block):
430
+ """フィルタ付き微分 ``H(s) = N*s / (s + N)`` (ADR-0006 §(3))。
431
+
432
+ 純粋微分 ``s`` は実装不能なので 1 次フィルタ近似を採用する。``N`` を大きく
433
+ すると純粋微分に近づくがノイズも拡大する。リファレンスツールの Derivative の
434
+ Filter Coefficient ``N`` (default 1000) と同じ慣習に従う。
435
+
436
+ 実現形: 状態 ``x`` を ``x_dot = -N*x + N*u``、出力 ``y = -N*x + N*u``
437
+ とすると、入力から出力への伝達関数は ``N*s / (s + N)`` になる。
438
+ ``direct_feedthrough = True`` (``y`` に ``u`` が直接寄与する)。
439
+
440
+ Args:
441
+ N: フィルタ帯域 (rad/s)。default 1000.0。
442
+ x0: 内部状態の初期値。default 0.0。
443
+ """
444
+
445
+ # D-4 (SPEC-0028 Q11): 連続ブロックは入力に float64 を要求する
446
+ required_input_dtype: ClassVar[str | None] = "float64"
447
+
448
+ def __init__(
449
+ self,
450
+ N: float = 1000.0,
451
+ x0: float = 0.0,
452
+ *,
453
+ id: str | None = None,
454
+ name: str | None = None,
455
+ ):
456
+ if N <= 0.0:
457
+ raise BlockSpecError(f"Derivative: N must be > 0, got {N}")
458
+ super().__init__(
459
+ id=id,
460
+ name=name,
461
+ n_inputs=1,
462
+ n_outputs=1,
463
+ n_states=1,
464
+ direct_feedthrough=True,
465
+ )
466
+ self.N = float(N)
467
+ self.x0 = np.array([float(x0)])
468
+ self._params = {"N": self.N, "x0": float(x0)}
469
+
470
+ def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
471
+ return np.array([self.N * (u[0] - x[0])])
472
+
473
+ def derivative(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
474
+ return np.array([self.N * (u[0] - x[0])])