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,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
|
+
)
|
flode/blocks/__init__.py
ADDED
|
@@ -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)
|