flode 0.63.0__tar.gz → 0.64.0__tar.gz
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-0.63.0 → flode-0.64.0}/CHANGELOG.md +50 -0
- {flode-0.63.0/flode.egg-info → flode-0.64.0}/PKG-INFO +1 -1
- {flode-0.63.0 → flode-0.64.0}/flode/__init__.py +1 -1
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/__init__.py +6 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/_lti_utils.py +92 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/continuous.py +28 -11
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/discrete.py +11 -4
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/lookup.py +99 -41
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/mathops.py +128 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/random_source.py +57 -3
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/sources.py +38 -9
- {flode-0.63.0 → flode-0.64.0}/flode/core/block.py +6 -1
- {flode-0.63.0 → flode-0.64.0}/flode/core/persistence.py +188 -2
- {flode-0.63.0 → flode-0.64.0}/flode/core/signals.py +70 -10
- {flode-0.63.0 → flode-0.64.0}/flode/server/registry.py +8 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/registry_translations.py +45 -0
- flode-0.64.0/flode/server/static/.app-version +1 -0
- flode-0.63.0/flode/server/static/assets/index-H0vWvCjy.js → flode-0.64.0/flode/server/static/assets/index-B85mUsTb.js +28 -28
- flode-0.64.0/flode/server/static/assets/index-B85mUsTb.js.map +1 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/static/index.html +1 -1
- {flode-0.63.0 → flode-0.64.0/flode.egg-info}/PKG-INFO +1 -1
- {flode-0.63.0 → flode-0.64.0}/flode.egg-info/SOURCES.txt +3 -2
- {flode-0.63.0 → flode-0.64.0}/pyproject.toml +1 -1
- {flode-0.63.0 → flode-0.64.0}/tests/test_persistence_migration_0_10_to_0_11.py +3 -3
- {flode-0.63.0 → flode-0.64.0}/tests/test_persistence_migration_0_11_to_0_12.py +1 -1
- {flode-0.63.0 → flode-0.64.0}/tests/test_persistence_migration_0_12_to_0_13.py +2 -2
- {flode-0.63.0 → flode-0.64.0}/tests/test_persistence_migration_0_13_to_0_14.py +3 -3
- flode-0.64.0/tests/test_persistence_migration_0_14_to_0_15.py +229 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_persistence_migration_0_8_to_0_9.py +4 -4
- {flode-0.63.0 → flode-0.64.0}/tests/test_persistence_migration_0_9_to_0_10.py +2 -2
- flode-0.63.0/flode/server/static/.app-version +0 -1
- flode-0.63.0/flode/server/static/assets/index-H0vWvCjy.js.map +0 -1
- {flode-0.63.0 → flode-0.64.0}/LICENSE +0 -0
- {flode-0.63.0 → flode-0.64.0}/MANIFEST.in +0 -0
- {flode-0.63.0 → flode-0.64.0}/README.md +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/analysis/__init__.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/analysis/frequency_response.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/analysis/linearize.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/analysis/stability.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/_elementwise.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/_vector_state.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/cast.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/discontinuities.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/logic.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/pythonfunc.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/pythonfunc_rewrite.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/pythonfunc_source.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/rounding.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/routing.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/sinks.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/transport_delay.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/blocks/userfunc.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/core/__init__.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/core/decorator.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/core/dtypes.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/core/identifiers.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/core/simulator.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/exceptions.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/libraries/__init__.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/libraries/_loader.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/libraries/std.flwlib.json +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/__init__.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/app.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/cli.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/config.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/errors.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/library_registry.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/migrations/__init__.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/routes/__init__.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/routes/blocks.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/routes/files.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/routes/libraries.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/routes/models.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/routes/simulations.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/runtime.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/security/__init__.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/security/origin.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/security/paths.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/settings.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/static/assets/index-BnL2nNOe.css +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/static/favicon.ico +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/server/static/favicon.svg +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/subsystems/__init__.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/subsystems/_mask.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/subsystems/control_blocks.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/subsystems/ports.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode/subsystems/subsystem.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode.egg-info/dependency_links.txt +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode.egg-info/entry_points.txt +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode.egg-info/requires.txt +0 -0
- {flode-0.63.0 → flode-0.64.0}/flode.egg-info/top_level.txt +0 -0
- {flode-0.63.0 → flode-0.64.0}/setup.cfg +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_adr0014_edge_cases.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_block_id.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_console_scripts.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_discrete_block_semantics.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_discrete_scheduler.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_discrete_timing_semantics.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_discrete_timing_semantics_vector.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_error_paths.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_multirate_block_semantics.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_persistence_migration_0_7_to_0_8.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_phase0_regression.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_rate_transition.py +0 -0
- {flode-0.63.0 → flode-0.64.0}/tests/test_version_consistency.py +0 -0
|
@@ -5,6 +5,56 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.64.0] - 2026-09-21 — StateSpace 系のベクトルポート統一とソース系のベクトル値 (ADR-0079 Stage 3)
|
|
9
|
+
|
|
10
|
+
### Changed (BREAKING: `StateSpace` 系のポートが変わります)
|
|
11
|
+
|
|
12
|
+
- **`StateSpace` / `DiscreteStateSpace` / `MimoTransferFunction` の入出力がベクトルポート
|
|
13
|
+
1 本ずつになった** (ADR-0079 §(7) D-9)。入力は `m >= 2` なら shape `(m,)` の 1 ポート
|
|
14
|
+
(`m == 1` はスカラ)、出力も同様 (`p`)。m 本のスカラ信号は `Mux` で束ねて繋ぎ、出力は
|
|
15
|
+
`Demux` で分けるか、ベクトルのまま下流へ渡す。Python API では `sim.connect(src, ss,
|
|
16
|
+
dst_idx=i)` (i ≥ 1) / `src_idx=j` の配線が `PortIndexError` になる (共通 mixin
|
|
17
|
+
`flode.blocks._lti_utils.LtiVectorPortMixin`、SM-A の `output` / `derivative` /
|
|
18
|
+
`update` / `advance` は不変で結果は bit-identical)。`port_shapes_*` は行列次元から
|
|
19
|
+
一意なので JSON には書かない
|
|
20
|
+
- **モデル schema `0.14 → 0.15`** (`_builtin_migrate_0_14_to_0_15`)。既存ファイルの
|
|
21
|
+
`m >= 2` / `p >= 2` の StateSpace 系ブロックには `Mux(n=m)` (`<id>__in_mux`) /
|
|
22
|
+
`Demux(n=p)` (`<id>__out_demux`) が挿入され、既存の結線・`branch_waypoints` が
|
|
23
|
+
付け替わる (数値等価、v0.63.0 で採取した基準 npz で固定)。挿入 ID は決定的
|
|
24
|
+
(衝突時のみ `_1`, `_2` …、64 code point 超は切詰め)、レイアウトは元ブロックから
|
|
25
|
+
水平に 120 px ずらした位置 (既存ブロックの座標は不変)、Subsystem 内部にも再帰。
|
|
26
|
+
SISO / SIMO (`m == p == 1`) は無変更。`linearize` の外部入出力ラベルは
|
|
27
|
+
`ss.in[0][i]` / `ss.out[0][j]` (1 ポートの flat index) になる
|
|
28
|
+
- `TransferFunction` / `DiscreteTransferFunction` (SISO)、時間ソース (`Step` / `Sine` /
|
|
29
|
+
`Ramp` / `Clock` / `PulseGenerator`)、`TransportDelay` / `Relay`、`Fcn`、`XYGraph` は
|
|
30
|
+
スカラ専用のまま (D-10)。エラーメッセージを Stage 3 の実態に更新
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **ソース系のベクトル値** (SPEC-0031 #18): `Constant(value=[…])` は配列の shape の信号を
|
|
35
|
+
出力する (shape の起点、`_params["value"]` は入れ子リスト)。`RandomSource(shape=(n,))`
|
|
36
|
+
はサンプル境界ごとに `shape` 個の独立サンプルを 1 回で引く (`shape=()` の既定は
|
|
37
|
+
従来の乱数系列と bit-identical、非既定時のみ JSON に書く)
|
|
38
|
+
- **lookup 5 クラスのベクトル入力** (`LookupTable1D` / `LookupTable2D` / `LookupTableND` /
|
|
39
|
+
`Prelookup` / `InterpolationUsingPrelookup`): 要素ごとにスカラ核 `_eval` を適用する
|
|
40
|
+
(`ElementwiseMixin`、`Demux → × n → Mux` 版と bit-identical、`extrapolation="error"` /
|
|
41
|
+
nan / ±inf の扱いも要素ごと)。2 入力以上はスカラ拡張を含む合流規則に従う
|
|
42
|
+
- **新ブロック** (SPEC-0031 #19、Math カテゴリ): `Reduce(operation="sum" | "product" |
|
|
43
|
+
"min" | "max" | "mean")` (全要素を 1 スカラに縮約、スカラ入力は恒等)、`DotProduct`
|
|
44
|
+
(同 shape 2 入力の内積)、`MatrixMultiply` (`u0 @ u1`、numpy matmul 規則を build 時に検査、
|
|
45
|
+
片方だけスカラは `shape.mismatch`)。shape 規則 `reduce` / `matmul` を解決器に追加
|
|
46
|
+
- GUI: Inspector の数値欄で **`[1, 2, 3]` を配列として受理** (`Constant.value` / `Gain.k` /
|
|
47
|
+
ベクトル状態 7 クラスの `x0`、frontend の表 `lib/arrayParams.ts`)、配列欄に数値 1 個を
|
|
48
|
+
書くとスカラに戻る。ブロック面の `Constant` は配列値を `[1, 2, 3]` / `[2×2]` と表示。
|
|
49
|
+
新ブロックの glyph (Σ / u·v / A×B) と `Reduce` の operation 表示
|
|
50
|
+
|
|
51
|
+
### Fixed
|
|
52
|
+
|
|
53
|
+
- **GUI で `Mux` → `Gain` / `Integrator` 等の要素ごとブロックを接続できなかった**
|
|
54
|
+
(v0.62.0 から)。接続前検証が registry 既定の `()` と Mux の `(n,)` を strict 比較して
|
|
55
|
+
拒否していた。`()` は「build 時に推論する」の意味なので、両端とも非 `()` の宣言で
|
|
56
|
+
食い違うときだけ拒否し、それ以外は backend の信号面解決器に委ねる (ADR-0079 D-3)
|
|
57
|
+
|
|
8
58
|
## [0.63.0] - 2026-09-21 — ベクトル状態と Subsystem 境界のテンソル化 (ADR-0079 Stage 2)
|
|
9
59
|
|
|
10
60
|
### Added
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
# ADR-0044) は docked split と並存。Phase 6c (Workspace
|
|
8
8
|
# convergence) Stage 1 として ADR-0040 §Amendments §(1) で位置付け、Stage 2 /
|
|
9
9
|
# 3 (= activity bar + Launcher、drag-to-split-tab) は後続 ADR で順次着手。
|
|
10
|
-
__version__ = "0.
|
|
10
|
+
__version__ = "0.64.0"
|
|
11
11
|
|
|
12
12
|
from .analysis import (
|
|
13
13
|
BodeResponse,
|
|
@@ -30,10 +30,13 @@ from .mathops import (
|
|
|
30
30
|
CompareToZero,
|
|
31
31
|
DeadZone,
|
|
32
32
|
Divide,
|
|
33
|
+
DotProduct,
|
|
33
34
|
Gain,
|
|
34
35
|
MathFunction,
|
|
36
|
+
MatrixMultiply,
|
|
35
37
|
MinMax,
|
|
36
38
|
Product,
|
|
39
|
+
Reduce,
|
|
37
40
|
Saturation,
|
|
38
41
|
Sign,
|
|
39
42
|
Sum,
|
|
@@ -64,6 +67,7 @@ __all__ = [
|
|
|
64
67
|
"DiscreteStateSpace",
|
|
65
68
|
"DiscreteTransferFunction",
|
|
66
69
|
"Divide",
|
|
70
|
+
"DotProduct",
|
|
67
71
|
"Fcn",
|
|
68
72
|
"From",
|
|
69
73
|
"Gain",
|
|
@@ -75,6 +79,7 @@ __all__ = [
|
|
|
75
79
|
"LookupTable2D",
|
|
76
80
|
"LookupTableND",
|
|
77
81
|
"MathFunction",
|
|
82
|
+
"MatrixMultiply",
|
|
78
83
|
"Merge",
|
|
79
84
|
"MimoTransferFunction",
|
|
80
85
|
"MinMax",
|
|
@@ -88,6 +93,7 @@ __all__ = [
|
|
|
88
93
|
"RandomSource",
|
|
89
94
|
"RateLimiter",
|
|
90
95
|
"RateTransition",
|
|
96
|
+
"Reduce",
|
|
91
97
|
"RelationalOperator",
|
|
92
98
|
"Relay",
|
|
93
99
|
"Rounding",
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
|
|
6
6
|
from __future__ import annotations
|
|
7
7
|
|
|
8
|
+
from collections.abc import Callable
|
|
8
9
|
from typing import Any
|
|
9
10
|
|
|
10
11
|
import numpy as np
|
|
@@ -15,6 +16,97 @@ from ..exceptions import BlockSpecError
|
|
|
15
16
|
# ADR-0006 §(6): D 行列の最大絶対値がこの閾値を超えたら direct_feedthrough = True
|
|
16
17
|
_DF_TOLERANCE = 1e-12
|
|
17
18
|
|
|
19
|
+
Shape = tuple[int, ...]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def lti_port_layout(n_signals: int) -> tuple[int, tuple[Shape, ...]]:
|
|
23
|
+
"""信号数 ``n_signals`` を ADR-0079 D-9 のポート規則に写す。
|
|
24
|
+
|
|
25
|
+
``StateSpace`` 系は入力 m 本 / 出力 p 本の信号を **ベクトルポート 1 本** で運ぶ:
|
|
26
|
+
``n >= 2`` なら shape ``(n,)`` の 1 ポート、``n == 1`` なら ``()`` の 1 ポート
|
|
27
|
+
(SISO / SIMO は 0.14 以前と同じ配線)、``n == 0`` ならポートなし。
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
``(n_ports, port_shapes)``。
|
|
31
|
+
"""
|
|
32
|
+
if n_signals <= 0:
|
|
33
|
+
return 0, ()
|
|
34
|
+
if n_signals == 1:
|
|
35
|
+
return 1, ((),)
|
|
36
|
+
return 1, ((n_signals,),)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class LtiVectorPortMixin:
|
|
40
|
+
"""``StateSpace`` 系の vector-port API (ADR-0079 §(7) D-9)。
|
|
41
|
+
|
|
42
|
+
SM-A の ``output`` / ``derivative`` / ``update`` / ``advance`` は 0.14 以前と同じ
|
|
43
|
+
「長さ m の 1-D ``u``」を受け取る契約のまま (1 バイトも変えない)。本 mixin の
|
|
44
|
+
``*_v`` は 1 本のベクトルポート (``(m,)``、m == 1 なら rank-0) を flat にして
|
|
45
|
+
SM-A 版へ渡し、出力 ``(p,)`` を p == 1 なら rank-0 に整形する。m 本のスカラ
|
|
46
|
+
ポートから組み立てていた配列と同じ値が渡るため、結果は bit-identical。
|
|
47
|
+
|
|
48
|
+
MRO 上は ``Block`` の **前** に置く。leaf は SM-A メソッドだけを定義するので
|
|
49
|
+
dual-override 検査に掛からない。``port_shapes_*`` は行列次元から一意に決まるため
|
|
50
|
+
JSON には書かない。
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
_serialize_port_shapes = False
|
|
54
|
+
|
|
55
|
+
# Block が持つ属性 / SM-A メソッド (型ヒントのみ、mypy 用)
|
|
56
|
+
n_outputs: int
|
|
57
|
+
output: Callable[..., npt.NDArray[Any]]
|
|
58
|
+
derivative: Callable[..., npt.NDArray[Any]]
|
|
59
|
+
update: Callable[..., npt.NDArray[Any]]
|
|
60
|
+
advance: Callable[..., npt.NDArray[Any]]
|
|
61
|
+
|
|
62
|
+
@staticmethod
|
|
63
|
+
def _u_flat(u: tuple[npt.NDArray[Any], ...]) -> npt.NDArray[Any]:
|
|
64
|
+
"""ベクトルポート 1 本 (または無し) を SM-A の 1-D ``u`` に戻す。"""
|
|
65
|
+
if not u:
|
|
66
|
+
return np.zeros(0, dtype=float)
|
|
67
|
+
return np.asarray(u[0], dtype=float).reshape(-1)
|
|
68
|
+
|
|
69
|
+
def _y_port(self, y: npt.NDArray[Any]) -> tuple[npt.NDArray[Any], ...]:
|
|
70
|
+
"""SM-A の 1-D 出力 ``(p,)`` をベクトルポート 1 本 (p == 1 なら rank-0) にする。"""
|
|
71
|
+
if self.n_outputs == 0:
|
|
72
|
+
return ()
|
|
73
|
+
arr = np.asarray(y, dtype=float).reshape(-1)
|
|
74
|
+
if arr.shape[0] == 1:
|
|
75
|
+
return (arr.reshape(()),)
|
|
76
|
+
return (arr,)
|
|
77
|
+
|
|
78
|
+
def output_v(
|
|
79
|
+
self,
|
|
80
|
+
t: float,
|
|
81
|
+
x: npt.NDArray[Any],
|
|
82
|
+
u: tuple[npt.NDArray[Any], ...],
|
|
83
|
+
) -> tuple[npt.NDArray[Any], ...]:
|
|
84
|
+
return self._y_port(self.output(t, x, self._u_flat(u)))
|
|
85
|
+
|
|
86
|
+
def derivative_v(
|
|
87
|
+
self,
|
|
88
|
+
t: float,
|
|
89
|
+
x: npt.NDArray[Any],
|
|
90
|
+
u: tuple[npt.NDArray[Any], ...],
|
|
91
|
+
) -> npt.NDArray[Any]:
|
|
92
|
+
return np.asarray(self.derivative(t, x, self._u_flat(u)), dtype=float).ravel()
|
|
93
|
+
|
|
94
|
+
def update_v(
|
|
95
|
+
self,
|
|
96
|
+
t: float,
|
|
97
|
+
x: npt.NDArray[Any],
|
|
98
|
+
u: tuple[npt.NDArray[Any], ...],
|
|
99
|
+
) -> npt.NDArray[Any]:
|
|
100
|
+
return np.asarray(self.update(t, x, self._u_flat(u)), dtype=float).ravel()
|
|
101
|
+
|
|
102
|
+
def advance_v(
|
|
103
|
+
self,
|
|
104
|
+
t: float,
|
|
105
|
+
x: npt.NDArray[Any],
|
|
106
|
+
u: tuple[npt.NDArray[Any], ...],
|
|
107
|
+
) -> npt.NDArray[Any]:
|
|
108
|
+
return np.asarray(self.advance(t, x, self._u_flat(u)), dtype=float).ravel()
|
|
109
|
+
|
|
18
110
|
|
|
19
111
|
def build_companion_form_siso(
|
|
20
112
|
numerator: npt.NDArray[Any], denominator: npt.NDArray[Any]
|
|
@@ -16,7 +16,12 @@ import scipy.signal
|
|
|
16
16
|
|
|
17
17
|
from ..core.block import Block
|
|
18
18
|
from ..exceptions import BlockSpecError
|
|
19
|
-
from ._lti_utils import
|
|
19
|
+
from ._lti_utils import (
|
|
20
|
+
_DF_TOLERANCE,
|
|
21
|
+
LtiVectorPortMixin,
|
|
22
|
+
build_companion_form_siso,
|
|
23
|
+
lti_port_layout,
|
|
24
|
+
)
|
|
20
25
|
from ._vector_state import VectorStateMixin, X0Like
|
|
21
26
|
|
|
22
27
|
|
|
@@ -73,16 +78,20 @@ class Integrator(VectorStateMixin, Block):
|
|
|
73
78
|
return self._u_state(u[0])
|
|
74
79
|
|
|
75
80
|
|
|
76
|
-
class StateSpace(Block):
|
|
81
|
+
class StateSpace(LtiVectorPortMixin, Block):
|
|
77
82
|
"""連続 LTI 状態空間 ``x_dot = A x + B u``、``y = C x + D u``。
|
|
78
83
|
|
|
79
84
|
``direct_feedthrough`` は ``D`` 行列の最大絶対値が ``1e-12`` を超えるかで
|
|
80
85
|
自動推論する (ADR-0006 §(6))。
|
|
81
86
|
|
|
87
|
+
ポート (ADR-0079 §(7) D-9、v0.64.0): 入力は **ベクトルポート 1 本** (``m >= 2`` なら
|
|
88
|
+
shape ``(m,)``、``m == 1`` なら スカラ)、出力も同様 (``p``)。m 本のスカラ信号は
|
|
89
|
+
``Mux`` で束ねて繋ぐ (0.14 以前のファイルは load 時に自動変換される)。
|
|
90
|
+
|
|
82
91
|
Args:
|
|
83
92
|
A: システム行列 (shape ``(n, n)``)。
|
|
84
|
-
B: 入力行列 (shape ``(n, m)``)、``m =
|
|
85
|
-
C: 出力行列 (shape ``(p, n)``)、``p =
|
|
93
|
+
B: 入力行列 (shape ``(n, m)``)、``m`` = 入力ベクトルの次元。
|
|
94
|
+
C: 出力行列 (shape ``(p, n)``)、``p`` = 出力ベクトルの次元。
|
|
86
95
|
D: 直達行列 (shape ``(p, m)``)。``None`` のときゼロ行列。
|
|
87
96
|
x0: 初期状態 (shape ``(n,)``)。``None`` のときゼロ。
|
|
88
97
|
"""
|
|
@@ -131,13 +140,17 @@ class StateSpace(Block):
|
|
|
131
140
|
raise BlockSpecError(f"StateSpace: D must have shape ({p}, {m}), got {D_arr.shape}")
|
|
132
141
|
df = bool(np.max(np.abs(D_arr)) > _DF_TOLERANCE) if D_arr.size else False
|
|
133
142
|
|
|
143
|
+
n_in, shapes_in = lti_port_layout(m)
|
|
144
|
+
n_out, shapes_out = lti_port_layout(p)
|
|
134
145
|
super().__init__(
|
|
135
146
|
id=id,
|
|
136
147
|
name=name,
|
|
137
|
-
n_inputs=
|
|
138
|
-
n_outputs=
|
|
148
|
+
n_inputs=n_in,
|
|
149
|
+
n_outputs=n_out,
|
|
139
150
|
n_states=n,
|
|
140
151
|
direct_feedthrough=df,
|
|
152
|
+
port_shapes_in=shapes_in,
|
|
153
|
+
port_shapes_out=shapes_out,
|
|
141
154
|
)
|
|
142
155
|
self._A = A_arr
|
|
143
156
|
self._B = B_arr
|
|
@@ -253,7 +266,7 @@ class TransferFunction(Block):
|
|
|
253
266
|
return np.asarray(self._A @ x + self._B @ u, dtype=float).ravel()
|
|
254
267
|
|
|
255
268
|
|
|
256
|
-
class MimoTransferFunction(Block):
|
|
269
|
+
class MimoTransferFunction(LtiVectorPortMixin, Block):
|
|
257
270
|
"""連続 LTI MIMO 伝達関数 ``H(s) = N(s) / d(s)`` (共通分母版、ADR-0010 §(2)、ADR-0016)。
|
|
258
271
|
|
|
259
272
|
入力 ``q`` 個 / 出力 ``p`` 個の伝達関数行列を扱う。共通分母 ``d(s)`` (1D) と、
|
|
@@ -280,8 +293,8 @@ class MimoTransferFunction(Block):
|
|
|
280
293
|
- ``C``: shape ``(p, p*q*n)``、出力 i は ブロック (i, j) for all j の C を集約
|
|
281
294
|
- ``D``: shape ``(p, q)``、各 (i, j) で biproper なら非ゼロ
|
|
282
295
|
|
|
283
|
-
|
|
284
|
-
``p``
|
|
296
|
+
ポート (ADR-0079 §(7) D-9、v0.64.0): 入力はベクトルポート 1 本 (``q >= 2`` なら
|
|
297
|
+
shape ``(q,)``、``q == 1`` ならスカラ)、出力も同様 (``p``)。
|
|
285
298
|
|
|
286
299
|
Phase 3 では **共通分母版のみ** をサポート (各 (i, j) で異なる分母を許容する
|
|
287
300
|
独立分母版は Phase 4+ で再検討、ADR-0010 §(2))。
|
|
@@ -397,13 +410,17 @@ class MimoTransferFunction(Block):
|
|
|
397
410
|
|
|
398
411
|
df = bool(np.max(np.abs(D)) > _DF_TOLERANCE) if D.size else False
|
|
399
412
|
|
|
413
|
+
n_in, shapes_in = lti_port_layout(q)
|
|
414
|
+
n_out, shapes_out = lti_port_layout(p)
|
|
400
415
|
super().__init__(
|
|
401
416
|
id=id,
|
|
402
417
|
name=name,
|
|
403
|
-
n_inputs=
|
|
404
|
-
n_outputs=
|
|
418
|
+
n_inputs=n_in,
|
|
419
|
+
n_outputs=n_out,
|
|
405
420
|
n_states=n_total,
|
|
406
421
|
direct_feedthrough=df,
|
|
422
|
+
port_shapes_in=shapes_in,
|
|
423
|
+
port_shapes_out=shapes_out,
|
|
407
424
|
)
|
|
408
425
|
self._A = A
|
|
409
426
|
self._B = B
|
|
@@ -32,7 +32,7 @@ import scipy.signal
|
|
|
32
32
|
|
|
33
33
|
from ..core.block import Block
|
|
34
34
|
from ..exceptions import BlockSpecError
|
|
35
|
-
from ._lti_utils import _DF_TOLERANCE
|
|
35
|
+
from ._lti_utils import _DF_TOLERANCE, LtiVectorPortMixin, lti_port_layout
|
|
36
36
|
from ._vector_state import VectorStateMixin, X0Like
|
|
37
37
|
|
|
38
38
|
_zohd_logger = logging.getLogger("flode.blocks.discrete")
|
|
@@ -465,12 +465,15 @@ class ZeroOrderHoldDirect(VectorStateMixin, Block):
|
|
|
465
465
|
return self._u_state(u[0])
|
|
466
466
|
|
|
467
467
|
|
|
468
|
-
class DiscreteStateSpace(Block):
|
|
468
|
+
class DiscreteStateSpace(LtiVectorPortMixin, Block):
|
|
469
469
|
"""離散 LTI 状態空間 ``x[k+1] = A x[k] + B u[k]``、``y[k] = C x[k] + D u[k]`` (リファレンスツール互換)。
|
|
470
470
|
|
|
471
471
|
ADR-0006 §(5)、ADR-0015 §(3) で 2n-state augmentation。``direct_feedthrough`` は
|
|
472
472
|
``D`` の最大絶対値が ``1e-12`` を超えるかで自動推論。
|
|
473
473
|
|
|
474
|
+
ポート (ADR-0079 §(7) D-9、v0.64.0): 入力はベクトルポート 1 本 (``m >= 2`` なら
|
|
475
|
+
shape ``(m,)``、``m == 1`` ならスカラ)、出力も同様 (``p``)。
|
|
476
|
+
|
|
474
477
|
Internal state (n_states=2n、ADR-0015 §(3)):
|
|
475
478
|
x[0..n-1] = output_curr — 現サンプル境界での状態 (前回 fire で確定)
|
|
476
479
|
x[n..2n-1] = next_x — 次サンプル境界で前半にシフトされる buffer
|
|
@@ -537,14 +540,18 @@ class DiscreteStateSpace(Block):
|
|
|
537
540
|
df = bool(np.max(np.abs(D_arr)) > _DF_TOLERANCE) if D_arr.size else False
|
|
538
541
|
|
|
539
542
|
# ADR-0015 §(3): 2n-state augmentation で n_states = 2n
|
|
543
|
+
n_in, shapes_in = lti_port_layout(m)
|
|
544
|
+
n_out, shapes_out = lti_port_layout(p)
|
|
540
545
|
super().__init__(
|
|
541
546
|
id=id,
|
|
542
547
|
name=name,
|
|
543
|
-
n_inputs=
|
|
544
|
-
n_outputs=
|
|
548
|
+
n_inputs=n_in,
|
|
549
|
+
n_outputs=n_out,
|
|
545
550
|
n_states=2 * n,
|
|
546
551
|
direct_feedthrough=df,
|
|
547
552
|
sample_time=sample_time,
|
|
553
|
+
port_shapes_in=shapes_in,
|
|
554
|
+
port_shapes_out=shapes_out,
|
|
548
555
|
)
|
|
549
556
|
self._A = A_arr
|
|
550
557
|
self._B = B_arr
|
|
@@ -23,6 +23,7 @@ from scipy.interpolate import RegularGridInterpolator, interp1d
|
|
|
23
23
|
|
|
24
24
|
from ..core.block import Block
|
|
25
25
|
from ..exceptions import BlockEvalError, BlockSpecError
|
|
26
|
+
from ._elementwise import ElementwiseMixin
|
|
26
27
|
|
|
27
28
|
_logger = logging.getLogger(__name__)
|
|
28
29
|
|
|
@@ -31,7 +32,25 @@ _logger = logging.getLogger(__name__)
|
|
|
31
32
|
LOOKUP_ND_AXIS_WARNING_THRESHOLD = 6
|
|
32
33
|
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
def _elementwise(
|
|
36
|
+
fn: Any, *arrays: npt.NDArray[Any], n_out: int = 1
|
|
37
|
+
) -> tuple[npt.NDArray[Any], ...]:
|
|
38
|
+
"""スカラ核 ``fn`` を要素ごとに適用する (ADR-0079 Stage 3、lookup 系のベクトル化)。
|
|
39
|
+
|
|
40
|
+
入力は解決器の合流規則 (完全一致 + rank-0 拡張) を満たすので ``np.broadcast_arrays``
|
|
41
|
+
で揃え、``np.vectorize`` で ``fn`` を各要素に呼ぶ。全入力が rank-0 なら各出力も
|
|
42
|
+
rank-0 で、SM-A ``output`` と同じスカラ核を通るため bit-identical。補間オブジェクトの
|
|
43
|
+
配列呼びは使わない (``extrapolation="error"`` / nan / ±inf の要素ごとの分岐を
|
|
44
|
+
スカラ核 1 箇所に保つため。性能より意味論の一致を優先、SPEC-0031 §非機能要件)。
|
|
45
|
+
"""
|
|
46
|
+
vec = np.vectorize(fn, otypes=[float] * n_out)
|
|
47
|
+
out = vec(*np.broadcast_arrays(*arrays))
|
|
48
|
+
if n_out == 1:
|
|
49
|
+
return (np.asarray(out, dtype=float),)
|
|
50
|
+
return tuple(np.asarray(o, dtype=float) for o in out)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class LookupTable1D(ElementwiseMixin, Block):
|
|
35
54
|
"""1 次元ルックアップテーブル ``y = f(u)``。
|
|
36
55
|
|
|
37
56
|
ブレークポイント配列 ``breakpoints`` (厳密単調増加) と対応するテーブル値
|
|
@@ -237,14 +256,23 @@ class LookupTable1D(Block):
|
|
|
237
256
|
return edge_y + slope * (val - edge_x)
|
|
238
257
|
|
|
239
258
|
def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
|
240
|
-
|
|
259
|
+
return np.array([self._eval(float(np.asarray(u).reshape(-1)[0]))])
|
|
260
|
+
|
|
261
|
+
def _kernel(
|
|
262
|
+
self, t: float, x: npt.NDArray[Any], u: tuple[npt.NDArray[Any], ...]
|
|
263
|
+
) -> tuple[npt.NDArray[Any], ...]:
|
|
264
|
+
return _elementwise(self._eval, u[0])
|
|
265
|
+
|
|
266
|
+
def _eval(self, val: float) -> float:
|
|
267
|
+
"""スカラ核 ``y = f(val)`` (SM-A ``output`` と SM-T ``_kernel`` が共有)。"""
|
|
268
|
+
val = float(val)
|
|
241
269
|
|
|
242
270
|
# ±inf × linear 外挿: scipy 1.18+ の interp1d は外挿を lerp 形式
|
|
243
271
|
# ``y_lo*(1-t) + y_hi*t`` で計算するため inf - inf = nan (+ RuntimeWarning)
|
|
244
272
|
# になる (~1.17 は point-slope 形式で ±inf を返していた)。scipy を呼ばず
|
|
245
273
|
# 全補間方式で端点 slope から極限値を直接計算する。
|
|
246
274
|
if self.extrapolation == "linear" and np.isinf(val):
|
|
247
|
-
return
|
|
275
|
+
return self._extrapolate_from_edge(val)
|
|
248
276
|
|
|
249
277
|
try:
|
|
250
278
|
y = float(self._interp(val))
|
|
@@ -267,10 +295,10 @@ class LookupTable1D(Block):
|
|
|
267
295
|
):
|
|
268
296
|
y = self._extrapolate_from_edge(val)
|
|
269
297
|
|
|
270
|
-
return
|
|
298
|
+
return y
|
|
271
299
|
|
|
272
300
|
|
|
273
|
-
class LookupTable2D(Block):
|
|
301
|
+
class LookupTable2D(ElementwiseMixin, Block):
|
|
274
302
|
"""2 次元ルックアップテーブル ``y = f(u[0], u[1])``。
|
|
275
303
|
|
|
276
304
|
2 つのブレークポイント配列 (``breakpoints_row`` / ``breakpoints_col``、
|
|
@@ -496,8 +524,17 @@ class LookupTable2D(Block):
|
|
|
496
524
|
|
|
497
525
|
def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
|
498
526
|
u_arr = np.asarray(u).reshape(-1)
|
|
499
|
-
|
|
500
|
-
|
|
527
|
+
return np.array([self._eval(float(u_arr[0]), float(u_arr[1]))])
|
|
528
|
+
|
|
529
|
+
def _kernel(
|
|
530
|
+
self, t: float, x: npt.NDArray[Any], u: tuple[npt.NDArray[Any], ...]
|
|
531
|
+
) -> tuple[npt.NDArray[Any], ...]:
|
|
532
|
+
return _elementwise(self._eval, u[0], u[1])
|
|
533
|
+
|
|
534
|
+
def _eval(self, u0: float, u1: float) -> float:
|
|
535
|
+
"""スカラ核 ``y = f(u0, u1)`` (SM-A ``output`` と SM-T ``_kernel`` が共有)。"""
|
|
536
|
+
u0 = float(u0)
|
|
537
|
+
u1 = float(u1)
|
|
501
538
|
|
|
502
539
|
# nan は比較が全て False になるため、in_row/in_col は False となり
|
|
503
540
|
# extrapolation 経路に入る (== 1-D と同方針)。
|
|
@@ -516,8 +553,7 @@ class LookupTable2D(Block):
|
|
|
516
553
|
)
|
|
517
554
|
if self.extrapolation == "linear":
|
|
518
555
|
# nan は伝播 (np.isnan で early return、interp1d が nan を返すため不要)
|
|
519
|
-
|
|
520
|
-
return np.array([y])
|
|
556
|
+
return self._linear_extrapolate(u0, u1)
|
|
521
557
|
# extrapolation == "clip": 端点に飽和してから補間
|
|
522
558
|
u0 = float(np.clip(u0, self._bp_row[0], self._bp_row[-1]))
|
|
523
559
|
u1 = float(np.clip(u1, self._bp_col[0], self._bp_col[-1]))
|
|
@@ -525,16 +561,14 @@ class LookupTable2D(Block):
|
|
|
525
561
|
if self.interpolation == "flat":
|
|
526
562
|
# nan が clip 後に残るのは clip(nan)=nan の場合 (ありうる)
|
|
527
563
|
if np.isnan(u0) or np.isnan(u1):
|
|
528
|
-
return
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
y = float(self._interp(np.array([[u0, u1]])).item())
|
|
534
|
-
return np.array([y])
|
|
564
|
+
return float("nan")
|
|
565
|
+
return self._flat_lookup(u0, u1)
|
|
566
|
+
# RegularGridInterpolator は shape (npts, ndim) を期待 → (1, 2) で渡す。
|
|
567
|
+
# 戻り値は shape (1,) なので .item() で scalar 抽出 (numpy 2.0 警告回避)。
|
|
568
|
+
return float(self._interp(np.array([[u0, u1]])).item())
|
|
535
569
|
|
|
536
570
|
|
|
537
|
-
class Prelookup(Block):
|
|
571
|
+
class Prelookup(ElementwiseMixin, Block):
|
|
538
572
|
"""1-D Prelookup: 入力 ``u`` から ``(k, f)`` を分離出力する (SPEC-0019)。
|
|
539
573
|
|
|
540
574
|
breakpoints 検索結果を `(k=index, f=fraction)` の 2 出力に分離することで、
|
|
@@ -612,13 +646,23 @@ class Prelookup(Block):
|
|
|
612
646
|
}
|
|
613
647
|
|
|
614
648
|
def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
|
615
|
-
|
|
649
|
+
k, f = self._eval(float(np.asarray(u).reshape(-1)[0]))
|
|
650
|
+
return np.array([k, f])
|
|
651
|
+
|
|
652
|
+
def _kernel(
|
|
653
|
+
self, t: float, x: npt.NDArray[Any], u: tuple[npt.NDArray[Any], ...]
|
|
654
|
+
) -> tuple[npt.NDArray[Any], ...]:
|
|
655
|
+
return _elementwise(self._eval, u[0], n_out=2)
|
|
656
|
+
|
|
657
|
+
def _eval(self, val: float) -> tuple[float, float]:
|
|
658
|
+
"""スカラ核 ``(k, f) = g(val)`` (SM-A ``output`` と SM-T ``_kernel`` が共有)。"""
|
|
659
|
+
val = float(val)
|
|
616
660
|
bp = self._breakpoints
|
|
617
661
|
n = bp.size
|
|
618
662
|
|
|
619
663
|
if np.isnan(val):
|
|
620
664
|
# nan は 1-D LookupTable と同方針で伝播
|
|
621
|
-
return
|
|
665
|
+
return float("nan"), float("nan")
|
|
622
666
|
|
|
623
667
|
in_domain = bool(bp[0] <= val <= bp[-1])
|
|
624
668
|
|
|
@@ -636,17 +680,17 @@ class Prelookup(Block):
|
|
|
636
680
|
else: # val > bp[-1] (inf 含む)
|
|
637
681
|
k = n - 2
|
|
638
682
|
f = (val - float(bp[k])) / (float(bp[k + 1]) - float(bp[k]))
|
|
639
|
-
return
|
|
683
|
+
return float(k), f
|
|
640
684
|
# extrapolation == "clip": 端点に飽和
|
|
641
685
|
val = float(np.clip(val, bp[0], bp[-1]))
|
|
642
686
|
|
|
643
687
|
# 定義域内 (or clip 後): 純 numpy 検索
|
|
644
688
|
k = int(np.clip(np.searchsorted(bp, val, side="right") - 1, 0, n - 2))
|
|
645
689
|
f = (val - float(bp[k])) / (float(bp[k + 1]) - float(bp[k]))
|
|
646
|
-
return
|
|
690
|
+
return float(k), f
|
|
647
691
|
|
|
648
692
|
|
|
649
|
-
class InterpolationUsingPrelookup(Block):
|
|
693
|
+
class InterpolationUsingPrelookup(ElementwiseMixin, Block):
|
|
650
694
|
"""1-D Prelookup を用いた補間 (SPEC-0019)。
|
|
651
695
|
|
|
652
696
|
:class:`Prelookup` の出力 ``(k, f)`` を受け取り、内部 ``table`` から
|
|
@@ -726,30 +770,37 @@ class InterpolationUsingPrelookup(Block):
|
|
|
726
770
|
|
|
727
771
|
def output(self, t: float, x: npt.NDArray[Any], u: npt.NDArray[Any]) -> npt.NDArray[Any]:
|
|
728
772
|
u_arr = np.asarray(u).reshape(-1)
|
|
729
|
-
|
|
730
|
-
|
|
773
|
+
return np.array([self._eval(float(u_arr[0]), float(u_arr[1]))])
|
|
774
|
+
|
|
775
|
+
def _kernel(
|
|
776
|
+
self, t: float, x: npt.NDArray[Any], u: tuple[npt.NDArray[Any], ...]
|
|
777
|
+
) -> tuple[npt.NDArray[Any], ...]:
|
|
778
|
+
return _elementwise(self._eval, u[0], u[1])
|
|
779
|
+
|
|
780
|
+
def _eval(self, k_raw: float, f: float) -> float:
|
|
781
|
+
"""スカラ核 ``y = h(k, f)`` (SM-A ``output`` と SM-T ``_kernel`` が共有)。"""
|
|
782
|
+
k_raw = float(k_raw)
|
|
783
|
+
f = float(f)
|
|
731
784
|
tbl = self._table
|
|
732
785
|
n = tbl.size
|
|
733
786
|
|
|
734
787
|
# k / f の nan は伝播 (= nan 出力)。
|
|
735
788
|
if np.isnan(k_raw) or np.isnan(f):
|
|
736
|
-
return
|
|
789
|
+
return float("nan")
|
|
737
790
|
# k は黙って clip ([0, n-2])。Prelookup の出力域だが上流ブロック自由なので保険。
|
|
738
791
|
# ``np.clip`` を ``int()`` より先に評価することで ``k_raw=±inf`` でも
|
|
739
792
|
# OverflowError を避けて n-2 / 0 に飽和させる (ADR-0056 構造化エラー protocol)。
|
|
740
793
|
k = int(np.clip(k_raw, 0, n - 2))
|
|
741
794
|
|
|
742
795
|
if self.interpolation == "linear":
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
796
|
+
return float(tbl[k]) + f * (float(tbl[k + 1]) - float(tbl[k]))
|
|
797
|
+
if self.interpolation == "nearest":
|
|
798
|
+
return float(tbl[k + 1]) if f >= 0.5 else float(tbl[k])
|
|
799
|
+
# "flat"
|
|
800
|
+
return float(tbl[k])
|
|
748
801
|
|
|
749
|
-
return np.array([y])
|
|
750
802
|
|
|
751
|
-
|
|
752
|
-
class LookupTableND(Block):
|
|
803
|
+
class LookupTableND(ElementwiseMixin, Block):
|
|
753
804
|
"""n 次元ルックアップテーブル ``y = f(u[0], u[1], ..., u[n-1])`` (SPEC-0018)。
|
|
754
805
|
|
|
755
806
|
軸数 n は ``len(breakpoints_axes)`` で動的に決まる。``LookupTable1D`` (n=1) /
|
|
@@ -978,6 +1029,16 @@ class LookupTableND(Block):
|
|
|
978
1029
|
f"LookupTableND[{self.name}]: expected {self._n_axes} inputs, got {u_arr.size}",
|
|
979
1030
|
block_id=self.id,
|
|
980
1031
|
)
|
|
1032
|
+
return np.array([self._eval(*u_arr)])
|
|
1033
|
+
|
|
1034
|
+
def _kernel(
|
|
1035
|
+
self, t: float, x: npt.NDArray[Any], u: tuple[npt.NDArray[Any], ...]
|
|
1036
|
+
) -> tuple[npt.NDArray[Any], ...]:
|
|
1037
|
+
return _elementwise(self._eval, *u)
|
|
1038
|
+
|
|
1039
|
+
def _eval(self, *vals: float) -> float:
|
|
1040
|
+
"""スカラ核 ``y = f(u[0], …, u[n-1])`` (SM-A ``output`` と SM-T ``_kernel`` が共有)。"""
|
|
1041
|
+
u_arr = np.asarray(vals, dtype=float)
|
|
981
1042
|
|
|
982
1043
|
# 各軸の定義域内判定 (nan は <=/>= で False になり外挿経路へ)
|
|
983
1044
|
in_domain_flags = [
|
|
@@ -999,8 +1060,7 @@ class LookupTableND(Block):
|
|
|
999
1060
|
)
|
|
1000
1061
|
if self.extrapolation == "linear":
|
|
1001
1062
|
# nan 含むときは結果も nan (interp1d が nan 伝播)
|
|
1002
|
-
|
|
1003
|
-
return np.array([y])
|
|
1063
|
+
return self._linear_extrapolate(u_arr)
|
|
1004
1064
|
# extrapolation == "clip": 各軸独立に端点飽和
|
|
1005
1065
|
u_arr = np.array(
|
|
1006
1066
|
[
|
|
@@ -1017,9 +1077,7 @@ class LookupTableND(Block):
|
|
|
1017
1077
|
|
|
1018
1078
|
if self.interpolation == "flat":
|
|
1019
1079
|
if bool(np.any(np.isnan(u_arr))):
|
|
1020
|
-
return
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
y = float(self._interp(u_arr.reshape(1, -1)).item())
|
|
1025
|
-
return np.array([y])
|
|
1080
|
+
return float("nan")
|
|
1081
|
+
return self._flat_lookup(u_arr)
|
|
1082
|
+
# RegularGridInterpolator は shape (npts, ndim) を期待 → (1, n_axes)。
|
|
1083
|
+
return float(self._interp(u_arr.reshape(1, -1)).item())
|