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
flode/__init__.py
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# v0.27.0 (minor、後方互換): ADR-0045 採択。Workspace convergence Stage 1 =
|
|
2
|
+
# multi-pane split。`<main>` 内 Diagram + Scope を ``react-resizable-panels``
|
|
3
|
+
# のネスト split で任意配置可能に、SplitTree state は localStorage に
|
|
4
|
+
# ``flode.workspace_layout.<hash>.<b64url(path)>`` キーで永続化。DiagramCanvas
|
|
5
|
+
# は React Portal で投影することで SplitTree 再構造でも viewport を保持
|
|
6
|
+
# (= v0.26.12 規律継承)。既存 ``ScopePanelContainer`` (react-rnd float、
|
|
7
|
+
# ADR-0044) は docked split と並存。Phase 6c (Workspace
|
|
8
|
+
# convergence) Stage 1 として ADR-0040 §Amendments §(1) で位置付け、Stage 2 /
|
|
9
|
+
# 3 (= activity bar + Launcher、drag-to-split-tab) は後続 ADR で順次着手。
|
|
10
|
+
__version__ = "0.60.3"
|
|
11
|
+
|
|
12
|
+
from .analysis import (
|
|
13
|
+
BodeResponse,
|
|
14
|
+
LinearSystem,
|
|
15
|
+
NyquistResponse,
|
|
16
|
+
RootLocus,
|
|
17
|
+
bode,
|
|
18
|
+
eigenvalues,
|
|
19
|
+
is_stable,
|
|
20
|
+
linearize,
|
|
21
|
+
nyquist,
|
|
22
|
+
root_locus,
|
|
23
|
+
)
|
|
24
|
+
from .core.block import Block
|
|
25
|
+
from .core.decorator import block
|
|
26
|
+
from .core.simulator import Simulator
|
|
27
|
+
from .exceptions import (
|
|
28
|
+
AlgebraicLoopError,
|
|
29
|
+
BlockSpecError,
|
|
30
|
+
FlodeError,
|
|
31
|
+
LibraryEntryNotFoundError,
|
|
32
|
+
LibraryFileError,
|
|
33
|
+
ModelLoadError,
|
|
34
|
+
ModelSerializationError,
|
|
35
|
+
SchedulingError,
|
|
36
|
+
SchemaVersionError,
|
|
37
|
+
SimulationStillRunningError,
|
|
38
|
+
SolverError,
|
|
39
|
+
UnknownBlockIdError,
|
|
40
|
+
UnknownBlockTypeError,
|
|
41
|
+
)
|
|
42
|
+
from .libraries import (
|
|
43
|
+
Library,
|
|
44
|
+
LibraryEntry,
|
|
45
|
+
export_subsystem_to_library,
|
|
46
|
+
load_library,
|
|
47
|
+
validate_library,
|
|
48
|
+
)
|
|
49
|
+
from .subsystems import Enable, Inport, Outport, Subsystem, Trigger
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
"AlgebraicLoopError",
|
|
53
|
+
"Block",
|
|
54
|
+
"BlockSpecError",
|
|
55
|
+
"BodeResponse",
|
|
56
|
+
"Enable",
|
|
57
|
+
"Inport",
|
|
58
|
+
"Library",
|
|
59
|
+
"LibraryEntry",
|
|
60
|
+
"LibraryEntryNotFoundError",
|
|
61
|
+
"LibraryFileError",
|
|
62
|
+
"LinearSystem",
|
|
63
|
+
"ModelLoadError",
|
|
64
|
+
"ModelSerializationError",
|
|
65
|
+
"NyquistResponse",
|
|
66
|
+
"Outport",
|
|
67
|
+
"FlodeError",
|
|
68
|
+
"RootLocus",
|
|
69
|
+
"SchedulingError",
|
|
70
|
+
"SchemaVersionError",
|
|
71
|
+
"SimulationStillRunningError",
|
|
72
|
+
"Simulator",
|
|
73
|
+
"SolverError",
|
|
74
|
+
"Subsystem",
|
|
75
|
+
"Trigger",
|
|
76
|
+
"UnknownBlockIdError",
|
|
77
|
+
"UnknownBlockTypeError",
|
|
78
|
+
"block",
|
|
79
|
+
"bode",
|
|
80
|
+
"eigenvalues",
|
|
81
|
+
"export_subsystem_to_library",
|
|
82
|
+
"is_stable",
|
|
83
|
+
"linearize",
|
|
84
|
+
"load_library",
|
|
85
|
+
"nyquist",
|
|
86
|
+
"root_locus",
|
|
87
|
+
"validate_library",
|
|
88
|
+
]
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"""線形化と周波数 / 安定性解析機能 (ADR-0026 / ADR-0027)。
|
|
2
|
+
|
|
3
|
+
公開 API:
|
|
4
|
+
|
|
5
|
+
- :func:`linearize` (added in ``v0.10.0``) — 動作点 ``(t, x, u)`` 周りで Jacobian を
|
|
6
|
+
中心差分 / 前進差分で数値計算し、状態空間 ``(A, B, C, D)`` を返す。
|
|
7
|
+
- :class:`LinearSystem` (added in ``v0.10.0``) — 線形化結果の dataclass
|
|
8
|
+
(numpy 行列 + 状態 / 入出力ラベル + 動作点情報 + ``to_control_ss()`` /
|
|
9
|
+
``bode()`` / ``nyquist()`` / ``eigenvalues()`` / ``is_stable()`` /
|
|
10
|
+
``root_locus()`` ヘルパ)。
|
|
11
|
+
- :func:`bode` / :func:`nyquist` (added in ``v0.10.1``) — Bode / Nyquist 応答を
|
|
12
|
+
``python-control`` 経由で計算 (``flode[control]`` extras 必須)。
|
|
13
|
+
- :func:`eigenvalues` / :func:`is_stable` (added in ``v0.10.1``) — A 行列の固有値と
|
|
14
|
+
漸近安定性判定 (numpy のみ、extras 不要)。
|
|
15
|
+
- :func:`root_locus` (added in ``v0.10.1``) — SISO 抽出した根軌跡 (extras 必須)。
|
|
16
|
+
- :class:`BodeResponse` / :class:`NyquistResponse` / :class:`RootLocus` —
|
|
17
|
+
対応する解析結果 dataclass (frozen, ndarray + ``plot()``)。
|
|
18
|
+
|
|
19
|
+
:meth:`flode.Simulator.linearize` メソッド経由でも同等の API を提供する。
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .frequency_response import BodeResponse, NyquistResponse, bode, nyquist
|
|
23
|
+
from .linearize import LinearSystem, linearize
|
|
24
|
+
from .stability import RootLocus, eigenvalues, is_stable, root_locus
|
|
25
|
+
|
|
26
|
+
__all__ = [
|
|
27
|
+
"BodeResponse",
|
|
28
|
+
"LinearSystem",
|
|
29
|
+
"NyquistResponse",
|
|
30
|
+
"RootLocus",
|
|
31
|
+
"bode",
|
|
32
|
+
"eigenvalues",
|
|
33
|
+
"is_stable",
|
|
34
|
+
"linearize",
|
|
35
|
+
"nyquist",
|
|
36
|
+
"root_locus",
|
|
37
|
+
]
|
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
"""周波数応答 (Bode / Nyquist) — ADR-0027 §(1)A / §(2)。
|
|
2
|
+
|
|
3
|
+
``LinearSystem`` (ADR-0026) を入力に取り、``python-control`` の
|
|
4
|
+
``frequency_response`` を薄くラップして ``BodeResponse`` / ``NyquistResponse``
|
|
5
|
+
を返す。可視化は ``Scope.plot`` (ADR-0023) と同じパターンで matplotlib に委譲。
|
|
6
|
+
|
|
7
|
+
依存:
|
|
8
|
+
|
|
9
|
+
- :mod:`numpy` (コア)
|
|
10
|
+
- :mod:`control` — ``flode[control]`` extras 経由 (未インストール時は ``ImportError``
|
|
11
|
+
で ``pip install flode[control]`` を案内、ADR-0027 §(10) E1)。
|
|
12
|
+
- :mod:`matplotlib.pyplot` — 可視化のみ、import は ``plot()`` 内で遅延。
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from dataclasses import dataclass
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
import numpy as np
|
|
21
|
+
import numpy.typing as npt
|
|
22
|
+
|
|
23
|
+
from ..exceptions import BlockSpecError
|
|
24
|
+
from .linearize import LinearSystem
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING: # pragma: no cover - optional matplotlib type
|
|
27
|
+
from matplotlib.axes import Axes
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
_FLODE_CONTROL_HINT = "Install via `pip install flode[control]` or `pip install python-control`."
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _import_control() -> Any:
|
|
34
|
+
"""``python-control`` を遅延 import (extras 未インストール時に案内付き)。"""
|
|
35
|
+
try:
|
|
36
|
+
import control as _control
|
|
37
|
+
except ImportError as e:
|
|
38
|
+
raise ImportError(
|
|
39
|
+
f"This function requires the optional `python-control` package. {_FLODE_CONTROL_HINT}"
|
|
40
|
+
) from e
|
|
41
|
+
return _control
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# BodeResponse
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
@dataclass(frozen=True, eq=False)
|
|
50
|
+
class BodeResponse:
|
|
51
|
+
"""Bode 応答データ (ADR-0027 §(2))。
|
|
52
|
+
|
|
53
|
+
``magnitude``/``phase`` は ``(p, m, n_omega)`` の 3D ndarray (= ``python-control``
|
|
54
|
+
0.10 の標準形式)。SISO (p=m=1) の場合も 3D を維持し、利用者は ``[0, 0, :]`` で
|
|
55
|
+
1D 化する。
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
magnitude: 線形振幅。shape ``(p, m, n_omega)``。
|
|
59
|
+
phase: 位相 [rad]。shape ``(p, m, n_omega)``。
|
|
60
|
+
omega: 周波数 [rad/s] または [Hz]、shape ``(n_omega,)``。
|
|
61
|
+
is_hz: ``omega`` が Hz 単位か rad/s 単位か (default ``False`` = rad/s)。
|
|
62
|
+
input_names: ``LinearSystem.input_names`` のコピー (introspection 用)。
|
|
63
|
+
output_names: ``LinearSystem.output_names`` のコピー。
|
|
64
|
+
|
|
65
|
+
Note:
|
|
66
|
+
``frozen=True, eq=False`` (= ADR-0026 ``LinearSystem`` と同じ規約)。
|
|
67
|
+
ndarray 含む dataclass の ``__eq__`` は truth-ambiguous になるため eq 無効。
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
magnitude: npt.NDArray[Any]
|
|
71
|
+
phase: npt.NDArray[Any]
|
|
72
|
+
omega: npt.NDArray[Any]
|
|
73
|
+
is_hz: bool
|
|
74
|
+
input_names: list[str]
|
|
75
|
+
output_names: list[str]
|
|
76
|
+
|
|
77
|
+
def magnitude_db(self) -> npt.NDArray[Any]:
|
|
78
|
+
"""振幅を dB に変換するヘルパ (``20 * log10(abs(magnitude))``)。
|
|
79
|
+
|
|
80
|
+
Returns:
|
|
81
|
+
shape ``(p, m, n_omega)``、単位 dB。
|
|
82
|
+
|
|
83
|
+
Note:
|
|
84
|
+
反共振点 (``magnitude == 0``) では ``-inf`` を返す。matplotlib は描画時に
|
|
85
|
+
自動 skip する (ADR-0027 §Risks #2 の方針)。
|
|
86
|
+
"""
|
|
87
|
+
return np.asarray(20.0 * np.log10(np.abs(self.magnitude)), dtype=float)
|
|
88
|
+
|
|
89
|
+
def plot(
|
|
90
|
+
self,
|
|
91
|
+
ax: Axes | None = None,
|
|
92
|
+
*,
|
|
93
|
+
input_idx: int = 0,
|
|
94
|
+
output_idx: int = 0,
|
|
95
|
+
show: bool = False,
|
|
96
|
+
deg: bool = True,
|
|
97
|
+
) -> Axes:
|
|
98
|
+
"""matplotlib で Bode 線図 (mag/phase 2 段) を描画する。
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
ax: 既存 ``Axes``。``None`` のとき ``plt.subplots(2, 1)`` で 2 段の axes を
|
|
102
|
+
作る。指定する場合は対応する figure に位相用 twinx を生成する仕様にせず、
|
|
103
|
+
単一の ``ax`` には magnitude のみ描画する (= 2 段プロット欲しい場合は
|
|
104
|
+
``None`` 指定が推奨、ADR-0023 ``Scope.plot`` パターン)。
|
|
105
|
+
input_idx: SISO 抽出する入力 idx (default ``0``)。
|
|
106
|
+
output_idx: SISO 抽出する出力 idx。
|
|
107
|
+
show: ``True`` なら ``plt.show()`` を呼ぶ。
|
|
108
|
+
deg: 位相を度で表示するか rad のままか (default ``True`` = degree)。
|
|
109
|
+
|
|
110
|
+
Returns:
|
|
111
|
+
``ax`` (matplotlib.axes.Axes)。``ax=None`` で 2 段作った場合は magnitude
|
|
112
|
+
軸 (上段)。
|
|
113
|
+
"""
|
|
114
|
+
import matplotlib.pyplot as plt
|
|
115
|
+
|
|
116
|
+
if input_idx < 0 or input_idx >= self.magnitude.shape[1]:
|
|
117
|
+
raise BlockSpecError(
|
|
118
|
+
f"BodeResponse.plot: input_idx={input_idx} out of range "
|
|
119
|
+
f"[0, {self.magnitude.shape[1]})"
|
|
120
|
+
)
|
|
121
|
+
if output_idx < 0 or output_idx >= self.magnitude.shape[0]:
|
|
122
|
+
raise BlockSpecError(
|
|
123
|
+
f"BodeResponse.plot: output_idx={output_idx} out of range "
|
|
124
|
+
f"[0, {self.magnitude.shape[0]})"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
mag = np.asarray(self.magnitude_db()[output_idx, input_idx, :], dtype=float)
|
|
128
|
+
ph = np.asarray(self.phase[output_idx, input_idx, :], dtype=float)
|
|
129
|
+
if deg:
|
|
130
|
+
ph = np.degrees(ph)
|
|
131
|
+
x_label = "ω [Hz]" if self.is_hz else "ω [rad/s]"
|
|
132
|
+
y_phase_label = "phase [deg]" if deg else "phase [rad]"
|
|
133
|
+
|
|
134
|
+
if ax is None:
|
|
135
|
+
_, axes = plt.subplots(2, 1, sharex=True)
|
|
136
|
+
ax_mag, ax_phase = axes
|
|
137
|
+
else:
|
|
138
|
+
ax_mag = ax
|
|
139
|
+
ax_phase = None
|
|
140
|
+
|
|
141
|
+
ax_mag.semilogx(self.omega, mag)
|
|
142
|
+
ax_mag.set_ylabel("magnitude [dB]")
|
|
143
|
+
ax_mag.grid(True, which="both", linestyle=":")
|
|
144
|
+
ax_mag.set_title(
|
|
145
|
+
f"{self.output_names[output_idx]} ← {self.input_names[input_idx]}"
|
|
146
|
+
if (self.output_names and self.input_names)
|
|
147
|
+
else "Bode plot"
|
|
148
|
+
)
|
|
149
|
+
if ax_phase is not None:
|
|
150
|
+
ax_phase.semilogx(self.omega, ph)
|
|
151
|
+
ax_phase.set_xlabel(x_label)
|
|
152
|
+
ax_phase.set_ylabel(y_phase_label)
|
|
153
|
+
ax_phase.grid(True, which="both", linestyle=":")
|
|
154
|
+
else:
|
|
155
|
+
ax_mag.set_xlabel(x_label)
|
|
156
|
+
|
|
157
|
+
if show:
|
|
158
|
+
plt.show()
|
|
159
|
+
# ``ax_mag`` は呼び出し元から渡された ``ax`` または ``plt.subplots(2, 1)`` で
|
|
160
|
+
# 作った上段 axes。``Axes`` 型は ``mypy.overrides = ignore_missing_imports`` で
|
|
161
|
+
# Any に縮退するため、戻り型整合のため明示 ignore。
|
|
162
|
+
return ax_mag # type: ignore[no-any-return]
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# NyquistResponse
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@dataclass(frozen=True, eq=False)
|
|
171
|
+
class NyquistResponse:
|
|
172
|
+
"""Nyquist 軌跡データ (ADR-0027 §(2))。
|
|
173
|
+
|
|
174
|
+
Attributes:
|
|
175
|
+
response: 複素応答 G(jω)、shape ``(p, m, n_omega)``、dtype complex128。
|
|
176
|
+
omega: 周波数 [rad/s]、shape ``(n_omega,)``。
|
|
177
|
+
input_names / output_names: ``LinearSystem`` ラベル継承。
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
response: npt.NDArray[Any]
|
|
181
|
+
omega: npt.NDArray[Any]
|
|
182
|
+
input_names: list[str]
|
|
183
|
+
output_names: list[str]
|
|
184
|
+
|
|
185
|
+
def plot(
|
|
186
|
+
self,
|
|
187
|
+
ax: Axes | None = None,
|
|
188
|
+
*,
|
|
189
|
+
input_idx: int = 0,
|
|
190
|
+
output_idx: int = 0,
|
|
191
|
+
show: bool = False,
|
|
192
|
+
) -> Axes:
|
|
193
|
+
"""Nyquist 軌跡を複素平面 (Re-Im) に描画する。"""
|
|
194
|
+
import matplotlib.pyplot as plt
|
|
195
|
+
|
|
196
|
+
if input_idx < 0 or input_idx >= self.response.shape[1]:
|
|
197
|
+
raise BlockSpecError(
|
|
198
|
+
f"NyquistResponse.plot: input_idx={input_idx} out of range "
|
|
199
|
+
f"[0, {self.response.shape[1]})"
|
|
200
|
+
)
|
|
201
|
+
if output_idx < 0 or output_idx >= self.response.shape[0]:
|
|
202
|
+
raise BlockSpecError(
|
|
203
|
+
f"NyquistResponse.plot: output_idx={output_idx} out of range "
|
|
204
|
+
f"[0, {self.response.shape[0]})"
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if ax is None:
|
|
208
|
+
_, ax = plt.subplots()
|
|
209
|
+
g = self.response[output_idx, input_idx, :]
|
|
210
|
+
ax.plot(np.real(g), np.imag(g))
|
|
211
|
+
ax.plot(np.real(g), -np.imag(g), linestyle="--") # 共役軌跡
|
|
212
|
+
ax.axhline(0.0, color="gray", linewidth=0.5)
|
|
213
|
+
ax.axvline(0.0, color="gray", linewidth=0.5)
|
|
214
|
+
ax.scatter([-1.0], [0.0], marker="x", color="red", label="-1 + 0j")
|
|
215
|
+
ax.set_xlabel("Re(G)")
|
|
216
|
+
ax.set_ylabel("Im(G)")
|
|
217
|
+
ax.set_aspect("equal", adjustable="datalim")
|
|
218
|
+
ax.grid(True, linestyle=":")
|
|
219
|
+
ax.set_title(
|
|
220
|
+
f"Nyquist {self.output_names[output_idx]} ← {self.input_names[input_idx]}"
|
|
221
|
+
if (self.output_names and self.input_names)
|
|
222
|
+
else "Nyquist plot"
|
|
223
|
+
)
|
|
224
|
+
ax.legend()
|
|
225
|
+
if show:
|
|
226
|
+
plt.show()
|
|
227
|
+
return ax
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
# 関数 API: bode / nyquist
|
|
232
|
+
# ---------------------------------------------------------------------------
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def _validate_linear_system(ls: LinearSystem) -> None:
|
|
236
|
+
if ls.A.shape[0] == 0:
|
|
237
|
+
raise BlockSpecError(
|
|
238
|
+
"Frequency response requires a non-empty state-space (A.shape[0] > 0). "
|
|
239
|
+
"Linearise a model with at least one continuous state first."
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def bode(
|
|
244
|
+
ls: LinearSystem,
|
|
245
|
+
*,
|
|
246
|
+
omega: npt.NDArray[Any] | None = None,
|
|
247
|
+
omega_limits: tuple[float, float] | None = None,
|
|
248
|
+
omega_num: int | None = None,
|
|
249
|
+
Hz: bool = False,
|
|
250
|
+
) -> BodeResponse:
|
|
251
|
+
"""``LinearSystem`` の Bode 応答 (magnitude / phase) を計算する。
|
|
252
|
+
|
|
253
|
+
内部で ``python-control.frequency_response`` を呼ぶ。``flode[control]`` extras が
|
|
254
|
+
必要 (ADR-0027 §Decision Option 3 hybrid)。
|
|
255
|
+
|
|
256
|
+
Args:
|
|
257
|
+
ls: ADR-0026 :class:`LinearSystem`。
|
|
258
|
+
omega: 周波数グリッド [rad/s]。``None`` のとき ``python-control`` の自動
|
|
259
|
+
範囲 (極零点の log10 spread に基づく)。
|
|
260
|
+
omega_limits: ``(omega_min, omega_max)`` を log10 スケールで指定。``omega``
|
|
261
|
+
と排他。
|
|
262
|
+
omega_num: 自動 omega の点数 (``None`` で control デフォルト)。
|
|
263
|
+
Hz: ``True`` で周波数を Hz 単位として返す (内部計算は rad/s)。
|
|
264
|
+
|
|
265
|
+
Returns:
|
|
266
|
+
:class:`BodeResponse`。
|
|
267
|
+
|
|
268
|
+
Raises:
|
|
269
|
+
ImportError: ``flode[control]`` extras 未インストール。
|
|
270
|
+
BlockSpecError: ``ls`` が空の状態空間 (``A.shape[0] == 0``) または
|
|
271
|
+
``omega``/``omega_limits`` の同時指定。
|
|
272
|
+
"""
|
|
273
|
+
_validate_linear_system(ls)
|
|
274
|
+
if omega is not None and omega_limits is not None:
|
|
275
|
+
raise BlockSpecError("bode: pass either omega or omega_limits, not both")
|
|
276
|
+
control = _import_control()
|
|
277
|
+
sys_ss = ls.to_control_ss()
|
|
278
|
+
fr = control.frequency_response(
|
|
279
|
+
sys_ss,
|
|
280
|
+
omega=omega,
|
|
281
|
+
omega_limits=omega_limits,
|
|
282
|
+
omega_num=omega_num,
|
|
283
|
+
Hz=Hz,
|
|
284
|
+
squeeze=False,
|
|
285
|
+
)
|
|
286
|
+
mag = np.asarray(fr.magnitude, dtype=float)
|
|
287
|
+
phase = np.asarray(fr.phase, dtype=float)
|
|
288
|
+
omega_arr = np.asarray(fr.omega, dtype=float)
|
|
289
|
+
# python-control 0.10 では SISO 時に shape (n_omega,) を返すケースもあるため
|
|
290
|
+
# ``squeeze=False`` を渡しているが、念のため次元が落ちていれば 3D に揃える
|
|
291
|
+
if mag.ndim == 1:
|
|
292
|
+
mag = mag.reshape(1, 1, -1)
|
|
293
|
+
phase = phase.reshape(1, 1, -1)
|
|
294
|
+
return BodeResponse(
|
|
295
|
+
magnitude=mag,
|
|
296
|
+
phase=phase,
|
|
297
|
+
omega=omega_arr,
|
|
298
|
+
is_hz=bool(Hz),
|
|
299
|
+
input_names=list(ls.input_names),
|
|
300
|
+
output_names=list(ls.output_names),
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def nyquist(
|
|
305
|
+
ls: LinearSystem,
|
|
306
|
+
*,
|
|
307
|
+
omega: npt.NDArray[Any] | None = None,
|
|
308
|
+
omega_limits: tuple[float, float] | None = None,
|
|
309
|
+
omega_num: int | None = None,
|
|
310
|
+
) -> NyquistResponse:
|
|
311
|
+
"""``LinearSystem`` の Nyquist 軌跡 (G(jω) 複素応答) を計算する。
|
|
312
|
+
|
|
313
|
+
Args:
|
|
314
|
+
ls: :class:`LinearSystem`。
|
|
315
|
+
omega: 周波数グリッド。``None`` で control 自動範囲。
|
|
316
|
+
omega_limits: ``(omega_min, omega_max)`` log10 スケール。``omega`` と排他。
|
|
317
|
+
omega_num: 自動 omega の点数。
|
|
318
|
+
|
|
319
|
+
Returns:
|
|
320
|
+
:class:`NyquistResponse`。
|
|
321
|
+
|
|
322
|
+
Raises:
|
|
323
|
+
ImportError: ``flode[control]`` extras 未インストール。
|
|
324
|
+
BlockSpecError: 空の状態空間 / ``omega`` ``omega_limits`` 同時指定。
|
|
325
|
+
"""
|
|
326
|
+
_validate_linear_system(ls)
|
|
327
|
+
if omega is not None and omega_limits is not None:
|
|
328
|
+
raise BlockSpecError("nyquist: pass either omega or omega_limits, not both")
|
|
329
|
+
control = _import_control()
|
|
330
|
+
sys_ss = ls.to_control_ss()
|
|
331
|
+
fr = control.frequency_response(
|
|
332
|
+
sys_ss,
|
|
333
|
+
omega=omega,
|
|
334
|
+
omega_limits=omega_limits,
|
|
335
|
+
omega_num=omega_num,
|
|
336
|
+
squeeze=False,
|
|
337
|
+
)
|
|
338
|
+
mag = np.asarray(fr.magnitude, dtype=float)
|
|
339
|
+
phase = np.asarray(fr.phase, dtype=float)
|
|
340
|
+
if mag.ndim == 1:
|
|
341
|
+
mag = mag.reshape(1, 1, -1)
|
|
342
|
+
phase = phase.reshape(1, 1, -1)
|
|
343
|
+
response = mag * np.exp(1j * phase)
|
|
344
|
+
return NyquistResponse(
|
|
345
|
+
response=np.asarray(response, dtype=complex),
|
|
346
|
+
omega=np.asarray(fr.omega, dtype=float),
|
|
347
|
+
input_names=list(ls.input_names),
|
|
348
|
+
output_names=list(ls.output_names),
|
|
349
|
+
)
|