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,229 @@
1
+ """安定性解析 (固有値 / 漸近安定性 / 根軌跡) — ADR-0027 §(1)B / §(6)(7)。
2
+
3
+ ``LinearSystem`` (ADR-0026) を入力に取る:
4
+
5
+ - :func:`eigenvalues` / :func:`is_stable` は **numpy のみで動く** (extras 不要、
6
+ ADR-0027 §Decision Option 3 hybrid)。
7
+ - :func:`root_locus` は ``python-control`` 経由 (`flode[control]` extras 必須、
8
+ 未インストール時は ``ImportError`` で誘導、ADR-0027 §(10) E4)。
9
+
10
+ MIMO 入力に対する :func:`root_locus` は ``input_idx`` / ``output_idx`` で SISO
11
+ 抽出する (ADR-0027 §(7))。
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from dataclasses import dataclass
17
+ from typing import TYPE_CHECKING, Any
18
+
19
+ import numpy as np
20
+ import numpy.typing as npt
21
+
22
+ from ..exceptions import BlockSpecError
23
+ from .linearize import LinearSystem
24
+
25
+ if TYPE_CHECKING: # pragma: no cover - optional matplotlib type
26
+ from matplotlib.axes import Axes
27
+
28
+ _FLODE_CONTROL_HINT = "Install via `pip install flode[control]` or `pip install python-control`."
29
+
30
+
31
+ def _import_control() -> Any:
32
+ """``python-control`` を遅延 import (extras 未インストール時に案内付き)。"""
33
+ try:
34
+ import control as _control
35
+ except ImportError as e:
36
+ raise ImportError(
37
+ f"This function requires the optional `python-control` package. {_FLODE_CONTROL_HINT}"
38
+ ) from e
39
+ return _control
40
+
41
+
42
+ # ---------------------------------------------------------------------------
43
+ # 固有値 / 漸近安定性 (numpy のみ)
44
+ # ---------------------------------------------------------------------------
45
+
46
+
47
+ def eigenvalues(ls: LinearSystem) -> npt.NDArray[Any]:
48
+ """A 行列の固有値を返す (`np.linalg.eig` の薄ラッパ)。
49
+
50
+ Args:
51
+ ls: :class:`LinearSystem`。
52
+
53
+ Returns:
54
+ 複素 1D ndarray、shape ``(n,)``。順序は LAPACK ``geev`` の依存。
55
+
56
+ Raises:
57
+ BlockSpecError: 空の状態空間 (``A.shape[0] == 0``)。
58
+ """
59
+ if ls.A.shape[0] == 0:
60
+ raise BlockSpecError(
61
+ "eigenvalues: empty state-space (A.shape[0] == 0). "
62
+ "Linearise a model with at least one continuous state first."
63
+ )
64
+ eig_values, _ = np.linalg.eig(ls.A)
65
+ return np.asarray(eig_values, dtype=complex)
66
+
67
+
68
+ def is_stable(ls: LinearSystem, *, tol: float = 1e-9) -> bool:
69
+ """連続系の漸近安定性判定 (ADR-0027 §(6))。
70
+
71
+ 全固有値の実部 < ``-tol`` のとき ``True``。境界値 (``Re(λ) ∈ [-tol, +tol]``)
72
+ は **不安定として扱う** (= 限界安定 / 中立安定は False、純虚軸極を含むモデルは
73
+ False)。
74
+
75
+ 第三状態 (``"marginal"``) を返さない理由は ADR-0027 §(6) 参照: API の単純さを
76
+ 優先し、境界値が必要な利用者は :func:`eigenvalues` 直接利用。
77
+
78
+ Args:
79
+ ls: :class:`LinearSystem`。
80
+ tol: マージン (default ``1e-9``、LAPACK ``geev`` の数値誤差を吸収)。
81
+
82
+ Returns:
83
+ ``True`` (漸近安定) / ``False`` (それ以外)。
84
+
85
+ Raises:
86
+ BlockSpecError: 空の状態空間。
87
+ ValueError: ``tol < 0``。
88
+ """
89
+ if tol < 0.0:
90
+ raise ValueError(f"is_stable: tol must be >= 0, got {tol}")
91
+ eig_values = eigenvalues(ls)
92
+ return bool(np.all(np.real(eig_values) < -tol))
93
+
94
+
95
+ # ---------------------------------------------------------------------------
96
+ # 根軌跡 (python-control 経由)
97
+ # ---------------------------------------------------------------------------
98
+
99
+
100
+ @dataclass(frozen=True, eq=False)
101
+ class RootLocus:
102
+ """根軌跡データ (SISO のみ、ADR-0027 §(2)/§(7))。
103
+
104
+ Attributes:
105
+ roots: 各 K 値での閉ループ極の位置、shape ``(n_K, n_states)``、dtype complex。
106
+ gains: 対応する K 値、shape ``(n_K,)``。
107
+ input_idx: SISO 抽出に用いた入力 idx (= ``ls.B`` の列)。
108
+ output_idx: SISO 抽出に用いた出力 idx (= ``ls.C`` の行)。
109
+ """
110
+
111
+ roots: npt.NDArray[Any]
112
+ gains: npt.NDArray[Any]
113
+ input_idx: int
114
+ output_idx: int
115
+
116
+ def plot(
117
+ self,
118
+ ax: Axes | None = None,
119
+ *,
120
+ show: bool = False,
121
+ ) -> Axes:
122
+ """根軌跡を複素平面に描画する (各極の K sweep を line plot)。"""
123
+ import matplotlib.pyplot as plt
124
+
125
+ if ax is None:
126
+ _, ax = plt.subplots()
127
+ for j in range(self.roots.shape[1]):
128
+ ax.plot(
129
+ np.real(self.roots[:, j]),
130
+ np.imag(self.roots[:, j]),
131
+ marker=".",
132
+ markersize=2,
133
+ linewidth=0.7,
134
+ )
135
+ ax.axhline(0.0, color="gray", linewidth=0.5)
136
+ ax.axvline(0.0, color="gray", linewidth=0.5)
137
+ ax.set_xlabel("Re")
138
+ ax.set_ylabel("Im")
139
+ ax.grid(True, linestyle=":")
140
+ ax.set_title(f"Root locus (input[{self.input_idx}] → output[{self.output_idx}])")
141
+ if show:
142
+ plt.show()
143
+ return ax
144
+
145
+
146
+ def _siso_extract(
147
+ ls: LinearSystem, input_idx: int, output_idx: int
148
+ ) -> tuple[npt.NDArray[Any], npt.NDArray[Any], npt.NDArray[Any], npt.NDArray[Any]]:
149
+ """``ls`` から ``(input_idx, output_idx)`` SISO サブシステムを抽出する。
150
+
151
+ A はそのまま、B は対象列のみ、C は対象行のみ、D は対象要素のみ。
152
+ """
153
+ n_in = ls.B.shape[1]
154
+ n_out = ls.C.shape[0]
155
+ if input_idx < 0 or input_idx >= n_in:
156
+ raise BlockSpecError(
157
+ f"root_locus: input_idx={input_idx} out of range (LinearSystem has {n_in} input(s))"
158
+ )
159
+ if output_idx < 0 or output_idx >= n_out:
160
+ raise BlockSpecError(
161
+ f"root_locus: output_idx={output_idx} out of range (LinearSystem has {n_out} output(s))"
162
+ )
163
+ A = np.asarray(ls.A, dtype=float)
164
+ B = np.asarray(ls.B[:, input_idx : input_idx + 1], dtype=float)
165
+ C = np.asarray(ls.C[output_idx : output_idx + 1, :], dtype=float)
166
+ D = np.asarray(ls.D[output_idx : output_idx + 1, input_idx : input_idx + 1], dtype=float)
167
+ return A, B, C, D
168
+
169
+
170
+ def root_locus(
171
+ ls: LinearSystem,
172
+ *,
173
+ k_range: tuple[float, float] | npt.NDArray[Any] | None = None,
174
+ input_idx: int = 0,
175
+ output_idx: int = 0,
176
+ ) -> RootLocus:
177
+ """SISO 抽出した伝達関数の根軌跡を計算する (ADR-0027 §(7))。
178
+
179
+ MIMO ``LinearSystem`` の場合は ``input_idx`` / ``output_idx`` で SISO 抽出。
180
+
181
+ Args:
182
+ ls: :class:`LinearSystem`。
183
+ k_range: K sweep 範囲。``(k_min, k_max)`` または明示 ndarray。``None`` で
184
+ ``python-control`` のデフォルト (極零点の絶対値から推定された対数スケール)。
185
+ input_idx: SISO 抽出する入力 idx (default ``0``)。
186
+ output_idx: SISO 抽出する出力 idx (default ``0``)。
187
+
188
+ Returns:
189
+ :class:`RootLocus`。
190
+
191
+ Raises:
192
+ ImportError: ``flode[control]`` extras 未インストール。
193
+ BlockSpecError: ``input_idx`` / ``output_idx`` 範囲外、空の状態空間。
194
+ """
195
+ if ls.A.shape[0] == 0:
196
+ raise BlockSpecError(
197
+ "root_locus: empty state-space (A.shape[0] == 0). "
198
+ "Linearise a model with at least one continuous state first."
199
+ )
200
+ A, B, C, D = _siso_extract(ls, input_idx, output_idx)
201
+ control = _import_control()
202
+ siso = control.ss(A, B, C, D)
203
+ # python-control 0.10 では ``root_locus_map`` が SISO 用、戻り値の構造は
204
+ # ``(roots, gains)`` を持つ ``ContourList``-like オブジェクト or tuple。
205
+ # ``rlocus`` (legacy) は figure を作る副作用ありなので ``root_locus_map`` を選択。
206
+ if k_range is None:
207
+ rl_data = control.root_locus_map(siso)
208
+ elif isinstance(k_range, tuple) and len(k_range) == 2:
209
+ # k_min, k_max を対数スケールで sweep
210
+ k_min, k_max = float(k_range[0]), float(k_range[1])
211
+ if k_min <= 0.0 or k_max <= k_min:
212
+ raise BlockSpecError(
213
+ f"root_locus: invalid k_range={k_range}; expect (k_min, k_max) with "
214
+ f"0 < k_min < k_max"
215
+ )
216
+ gains = np.logspace(np.log10(k_min), np.log10(k_max), 50)
217
+ rl_data = control.root_locus_map(siso, gains=gains)
218
+ else:
219
+ rl_data = control.root_locus_map(siso, gains=np.asarray(k_range, dtype=float))
220
+
221
+ roots = np.asarray(rl_data.loci, dtype=complex)
222
+ gains = np.asarray(rl_data.gains, dtype=float)
223
+ # python-control では loci shape が ``(n_K, n_states)``。numpy default 形式。
224
+ return RootLocus(
225
+ roots=roots,
226
+ gains=gains,
227
+ input_idx=int(input_idx),
228
+ output_idx=int(output_idx),
229
+ )
@@ -0,0 +1,107 @@
1
+ from .cast import Cast
2
+ from .continuous import (
3
+ Derivative,
4
+ Integrator,
5
+ MimoTransferFunction,
6
+ StateSpace,
7
+ TransferFunction,
8
+ )
9
+ from .discontinuities import RateLimiter, Relay
10
+ from .discrete import (
11
+ DiscreteIntegrator,
12
+ DiscreteStateSpace,
13
+ DiscreteTransferFunction,
14
+ RateTransition,
15
+ UnitDelay,
16
+ ZeroOrderHoldDirect,
17
+ )
18
+ from .logic import LogicalOperator, RelationalOperator
19
+ from .lookup import (
20
+ InterpolationUsingPrelookup,
21
+ LookupTable1D,
22
+ LookupTable2D,
23
+ LookupTableND,
24
+ Prelookup,
25
+ )
26
+ from .mathops import (
27
+ Abs,
28
+ CompareToConstant,
29
+ CompareToZero,
30
+ DeadZone,
31
+ Divide,
32
+ Gain,
33
+ MathFunction,
34
+ MinMax,
35
+ Product,
36
+ Saturation,
37
+ Sign,
38
+ Sum,
39
+ TrigFunction,
40
+ )
41
+ from .pythonfunc import PythonFunction
42
+ from .random_source import RandomSource
43
+ from .rounding import Rounding
44
+ from .routing import Demux, From, Goto, Merge, MultiportSwitch, Mux, Switch
45
+ from .sinks import Display, Scope, Terminator, XYGraph
46
+ from .sources import Clock, Constant, PulseGenerator, Ramp, Sine, Step
47
+ from .transport_delay import TransportDelay
48
+ from .userfunc import Fcn
49
+
50
+ __all__ = [
51
+ "Abs",
52
+ "Cast",
53
+ "Clock",
54
+ "CompareToConstant",
55
+ "CompareToZero",
56
+ "Constant",
57
+ "DeadZone",
58
+ "Demux",
59
+ "Derivative",
60
+ "Display",
61
+ "DiscreteIntegrator",
62
+ "DiscreteStateSpace",
63
+ "DiscreteTransferFunction",
64
+ "Divide",
65
+ "Fcn",
66
+ "From",
67
+ "Gain",
68
+ "Goto",
69
+ "Integrator",
70
+ "InterpolationUsingPrelookup",
71
+ "LogicalOperator",
72
+ "LookupTable1D",
73
+ "LookupTable2D",
74
+ "LookupTableND",
75
+ "MathFunction",
76
+ "Merge",
77
+ "MimoTransferFunction",
78
+ "MinMax",
79
+ "MultiportSwitch",
80
+ "Mux",
81
+ "Prelookup",
82
+ "Product",
83
+ "PulseGenerator",
84
+ "PythonFunction",
85
+ "Ramp",
86
+ "RandomSource",
87
+ "RateLimiter",
88
+ "RateTransition",
89
+ "RelationalOperator",
90
+ "Relay",
91
+ "Rounding",
92
+ "Saturation",
93
+ "Scope",
94
+ "Sign",
95
+ "Sine",
96
+ "StateSpace",
97
+ "Step",
98
+ "Sum",
99
+ "Switch",
100
+ "Terminator",
101
+ "TransferFunction",
102
+ "TransportDelay",
103
+ "TrigFunction",
104
+ "UnitDelay",
105
+ "XYGraph",
106
+ "ZeroOrderHoldDirect",
107
+ ]
@@ -0,0 +1,121 @@
1
+ """LTI ブロック共通ユーティリティ (ADR-0006、ADR-0010 §(6))。
2
+
3
+ 連続版と離散版の SS / TF ブロックで共有する定数とヘルパーを置く。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any
9
+
10
+ import numpy as np
11
+ import numpy.typing as npt
12
+
13
+ from ..exceptions import BlockSpecError
14
+
15
+ # ADR-0006 §(6): D 行列の最大絶対値がこの閾値を超えたら direct_feedthrough = True
16
+ _DF_TOLERANCE = 1e-12
17
+
18
+
19
+ def build_companion_form_siso(
20
+ numerator: npt.NDArray[Any], denominator: npt.NDArray[Any]
21
+ ) -> tuple[npt.NDArray[Any], npt.NDArray[Any], npt.NDArray[Any], npt.NDArray[Any]]:
22
+ """SISO 伝達関数 ``H(s) = num(s) / den(s)`` を controllable canonical form に変換。
23
+
24
+ ADR-0010 §(6) で予告された companion form 自前構築実装。``scipy.signal.tf2ss`` の
25
+ ゼロ多項式 (off-diagonal の `numerators[i][j] = [0.0]`) で BadCoefficients warning
26
+ を出す問題を回避する。MIMO 共通分母 TF を SISO ごとに realize して結合する用途
27
+ (`MimoTransferFunction`) で使う。
28
+
29
+ 係数の慣習:
30
+ ``numerator``、``denominator`` は **s の降べき** で表現
31
+ (numpy / scipy convention)。例: ``[1, 2, 3]`` は ``s^2 + 2*s + 3``。
32
+
33
+ Controllable canonical form:
34
+ 分母 ``d(s) = a_n s^n + a_{n-1} s^{n-1} + ... + a_1 s + a_0`` (a_n = 1 に正規化) と
35
+ 分子 ``b(s) = b_n s^n + b_{n-1} s^{n-1} + ... + b_0`` に対し:
36
+
37
+ A = [[0, 1, 0, ..., 0],
38
+ [0, 0, 1, ..., 0],
39
+ ...
40
+ [0, 0, 0, ..., 1],
41
+ [-a_0, -a_1, ..., -a_{n-1}]] shape (n, n)
42
+ B = [0, 0, ..., 1]^T shape (n, 1)
43
+ C = [b_0 - b_n*a_0, ..., b_{n-1} - b_n*a_{n-1}] shape (1, n)
44
+ D = [b_n] shape (1, 1)
45
+
46
+ biproper (deg(num) == deg(den)) のとき D = b_n/a_n、strict proper のとき
47
+ D = 0 になる (b_n = 0)。
48
+
49
+ Args:
50
+ numerator: 分子多項式の係数 (shape ``(m+1,)``、``m = deg(num)``)。
51
+ denominator: 分母多項式の係数 (shape ``(n+1,)``、``n = deg(den)``)。
52
+
53
+ Returns:
54
+ (A, B, C, D) tuple。shape はそれぞれ ``(n, n)``、``(n, 1)``、``(1, n)``、``(1, 1)``。
55
+
56
+ Raises:
57
+ BlockSpecError: ``num`` / ``den`` が空、``den[0] == 0``、improper
58
+ (``deg(num) > deg(den)``) の場合。
59
+ """
60
+ num = np.asarray(numerator, dtype=float).ravel()
61
+ den = np.asarray(denominator, dtype=float).ravel()
62
+ if num.size == 0 or den.size == 0:
63
+ raise BlockSpecError("build_companion_form_siso: numerator/denominator must be non-empty")
64
+ if den[0] == 0.0:
65
+ raise BlockSpecError(
66
+ "build_companion_form_siso: leading coefficient of denominator must be non-zero"
67
+ )
68
+ deg_num = num.size - 1
69
+ deg_den = den.size - 1
70
+ if deg_num > deg_den:
71
+ raise BlockSpecError(
72
+ f"build_companion_form_siso: improper system "
73
+ f"(deg(num)={deg_num} > deg(den)={deg_den}). "
74
+ f"Only proper or biproper systems are supported."
75
+ )
76
+ n = deg_den
77
+ if n == 0:
78
+ # H(s) = b_0 / a_0 (定数ゲイン)。state 不要だが、本ヘルパーは状態を持つ
79
+ # SS 表現を返す契約なので呼び出し側で n>=1 のみを許可する想定。
80
+ raise BlockSpecError(
81
+ "build_companion_form_siso: deg(den)=0 (pure gain) is not supported by SS realization; "
82
+ "use a static block (e.g. Gain) instead"
83
+ )
84
+
85
+ # 分母を monic 化: a_n = 1
86
+ den_monic = den / den[0]
87
+ # a = [a_0, a_1, ..., a_{n-1}] (係数昇べきで参照しやすくするため反転)
88
+ # den_monic の格納は降べき: den_monic[0]=1, den_monic[1]=a_{n-1}, ..., den_monic[n]=a_0
89
+ # よって a_i = den_monic[n - i]
90
+ a_asc = den_monic[::-1].copy() # a_asc[i] = a_i (昇べき)、a_asc[n]=1
91
+
92
+ # 分子を分母と同じ正規化で除して、長さを n+1 に左 0 パディング
93
+ num_normed = num / den[0]
94
+ if num_normed.size < n + 1:
95
+ num_padded = np.concatenate([np.zeros(n + 1 - num_normed.size, dtype=float), num_normed])
96
+ else:
97
+ num_padded = num_normed
98
+ # b_asc[i] = b_i (昇べき)、b_asc[n] = b_n (= biproper 時の D)
99
+ b_asc = num_padded[::-1].copy()
100
+
101
+ # A: companion matrix (controllable canonical form)
102
+ A = np.zeros((n, n), dtype=float)
103
+ if n > 1:
104
+ # 上三角の 1 つ上のサブダイアゴナルに 1 を配置
105
+ for i in range(n - 1):
106
+ A[i, i + 1] = 1.0
107
+ # 最終行: -a_0, -a_1, ..., -a_{n-1}
108
+ A[n - 1, :] = -a_asc[:n]
109
+
110
+ # B: 末尾要素のみ 1
111
+ B = np.zeros((n, 1), dtype=float)
112
+ B[n - 1, 0] = 1.0
113
+
114
+ # D: b_n (biproper のとき非ゼロ、proper のとき 0)
115
+ D = np.array([[b_asc[n]]], dtype=float)
116
+
117
+ # C: c_i = b_i - b_n * a_i (i = 0..n-1)、shape (1, n)
118
+ c_row = b_asc[:n] - b_asc[n] * a_asc[:n]
119
+ C = c_row.reshape(1, n)
120
+
121
+ return A, B, C, D
flode/blocks/cast.py ADDED
@@ -0,0 +1,66 @@
1
+ """``Cast`` — 実 dtype 変換ブロック (SPEC-0028 / ADR-0077 SM-D)。
2
+
3
+ v0.56.0 で ``output_type`` (値の意味論、旧 SPEC-0026) は撤去され、型概念は
4
+ ``dtype`` に一本化された。``Cast`` は**常に実変換する**ブロックであり、恒等
5
+ パススルー (旧 ``dtype="auto"``) は存在しない — 「置いたのに何も変わらない」
6
+ Cast を許さないため。
7
+
8
+ 旧 ``output_type`` の等価機能は既存ブロックで表現する (schema 0.12 migration が
9
+ 自動変換する対応):
10
+
11
+ * ``output_type="int"`` (偶数丸め) → :class:`~flode.blocks.rounding.Rounding`
12
+ (``mode="round"``)
13
+ * ``output_type="bool"`` (``u != 0`` → 0/1) →
14
+ :class:`~flode.blocks.mathops.CompareToZero` (``op="!="``)
15
+ """
16
+
17
+ from __future__ import annotations
18
+
19
+ from typing import Any
20
+
21
+ import numpy as np
22
+ import numpy.typing as npt
23
+
24
+ from ..core.block import Block
25
+ from ..core.dtypes import DTYPE_VOCABULARY, cast_value
26
+ from ..exceptions import BlockSpecError
27
+
28
+
29
+ class Cast(Block):
30
+ """実 dtype 変換 ``y = cast(u, dtype)``。
31
+
32
+ 出力は**実際にその numpy dtype** になる (SPEC-0028)。``dtype="float64"``
33
+ (既定) も恒等ではなく実変換 (``astype``)。float → 整数は**ゼロ方向切り捨て**、
34
+ nan → 0 / ±inf → 飽和は決定的 (規則の SSOT は
35
+ :func:`flode.core.dtypes.cast_value`)。
36
+
37
+ Cast を含むモデルは常に dtype 宣言モデルとして実行される
38
+ (:func:`flode.core.dtypes.has_declared_dtype`)。
39
+
40
+ Args:
41
+ dtype: 出力 dtype。``"float64"`` (既定) / ``"bool"`` / ``"uint8"`` /
42
+ ``"int32"`` / ``"int64"``。``"auto"`` は存在しない (Cast は常に変換)。
43
+
44
+ Raises:
45
+ BlockSpecError: ``dtype`` が語彙外。
46
+ """
47
+
48
+ _param_enums = {"dtype": DTYPE_VOCABULARY}
49
+
50
+ def __init__(
51
+ self,
52
+ dtype: str = "float64",
53
+ *,
54
+ id: str | None = None,
55
+ name: str | None = None,
56
+ ) -> None:
57
+ if dtype not in DTYPE_VOCABULARY:
58
+ raise BlockSpecError(f"Cast: dtype must be one of {DTYPE_VOCABULARY}, got {dtype!r}")
59
+ super().__init__(id=id, name=name, n_inputs=1, n_outputs=1)
60
+ self.dtype = dtype
61
+ self._params = {"dtype": dtype}
62
+
63
+ def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
64
+ # 実 dtype 変換 (SPEC-0028 §2.2)。float() を経由しないことで
65
+ # int64 の精度 (> 2^53) を守る。
66
+ return cast_value(np.asarray(u).reshape(-1)[:1], self.dtype)