pySimBlocks 0.1.0__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.
- examples/advanced/hardware/block_diagram.py +162 -0
- examples/advanced/hardware/camera.py +93 -0
- examples/advanced/hardware/gui_emio.py +231 -0
- examples/advanced/hardware/lmi.py +69 -0
- examples/advanced/hardware/main.py +57 -0
- examples/advanced/hardware/parameters.py +54 -0
- examples/advanced/qp_problem/params.py +10 -0
- examples/advanced/qp_problem/run.py +14 -0
- examples/advanced/sofa/gui/finger/Finger.py +108 -0
- examples/advanced/sofa/gui/finger/FingerController.py +53 -0
- examples/advanced/sofa/gui/finger/parameters_auto.py +22 -0
- examples/advanced/sofa/gui/sofa_plant/run.py +14 -0
- examples/advanced/sofa/python/Finger.py +107 -0
- examples/advanced/sofa/python/FingerController.py +48 -0
- examples/advanced/sofa/python/closedloop.py +64 -0
- examples/advanced/sofa/python/openloop.py +58 -0
- examples/basics/algebraic_function/g.py +7 -0
- examples/basics/algebraic_function/run.py +14 -0
- examples/basics/dc_motor/gui/run.py +14 -0
- examples/basics/dc_motor/multiple_order.py +91 -0
- examples/basics/dc_motor/openloop.py +62 -0
- examples/basics/dc_motor/pi.py +99 -0
- examples/basics/dc_motor/pid.py +118 -0
- examples/basics/dc_motor/pid_block.py +82 -0
- examples/basics/dc_motor/pid_block_vs_manual.py +143 -0
- examples/basics/external/parameters.py +4 -0
- examples/basics/external/run.py +14 -0
- examples/basics/file_sources/temp.py +16 -0
- examples/basics/observer.py +76 -0
- examples/basics/state_feedback/gui/parameters.py +9 -0
- examples/basics/state_feedback/gui/run.py +14 -0
- examples/basics/state_feedback/state_feedback.py +102 -0
- examples/basics/state_space/linear/gui/run.py +14 -0
- examples/basics/state_space/linear/ss_block.py +89 -0
- examples/basics/state_space/linear/ss_manual.py +105 -0
- examples/basics/state_space/non_linear/nlss.py +47 -0
- examples/basics/state_space/non_linear/run.py +14 -0
- examples/basics/state_space/polytope/params.py +39 -0
- examples/basics/state_space/polytope/weights.py +12 -0
- examples/comparison/simulink/convert.py +29 -0
- examples/comparison/simulink/openloop.py +127 -0
- examples/comparison/simulink/pi.py +148 -0
- examples/comparison/simulink/pid.py +154 -0
- examples/comparison/simulink/pid_backward.py +155 -0
- examples/quick_start/filter.py +35 -0
- examples/quick_start/gui/run.py +14 -0
- examples/tutorials/tutorial_1_python/main.py +99 -0
- examples/tutorials/tutorial_3_sofa/finger/Finger.py +120 -0
- examples/tutorials/tutorial_3_sofa/finger/FingerController.py +37 -0
- pySimBlocks/__init__.py +35 -0
- pySimBlocks/blocks/__init__.py +68 -0
- pySimBlocks/blocks/controllers/__init__.py +27 -0
- pySimBlocks/blocks/controllers/pid.py +250 -0
- pySimBlocks/blocks/controllers/state_feedback.py +141 -0
- pySimBlocks/blocks/interfaces/__init__.py +27 -0
- pySimBlocks/blocks/interfaces/external_input.py +107 -0
- pySimBlocks/blocks/interfaces/external_output.py +113 -0
- pySimBlocks/blocks/observers/__init__.py +23 -0
- pySimBlocks/blocks/observers/luenberger.py +175 -0
- pySimBlocks/blocks/operators/__init__.py +47 -0
- pySimBlocks/blocks/operators/algebraic_function.py +258 -0
- pySimBlocks/blocks/operators/dead_zone.py +186 -0
- pySimBlocks/blocks/operators/delay.py +254 -0
- pySimBlocks/blocks/operators/demux.py +123 -0
- pySimBlocks/blocks/operators/discrete_derivator.py +237 -0
- pySimBlocks/blocks/operators/discrete_integrator.py +265 -0
- pySimBlocks/blocks/operators/gain.py +294 -0
- pySimBlocks/blocks/operators/mux.py +131 -0
- pySimBlocks/blocks/operators/product.py +194 -0
- pySimBlocks/blocks/operators/rate_limiter.py +214 -0
- pySimBlocks/blocks/operators/saturation.py +178 -0
- pySimBlocks/blocks/operators/sum.py +188 -0
- pySimBlocks/blocks/operators/zero_order_hold.py +141 -0
- pySimBlocks/blocks/optimizers/__init__.py +23 -0
- pySimBlocks/blocks/optimizers/quadratic_program.py +326 -0
- pySimBlocks/blocks/sources/__init__.py +37 -0
- pySimBlocks/blocks/sources/chirp.py +168 -0
- pySimBlocks/blocks/sources/constant.py +82 -0
- pySimBlocks/blocks/sources/file_source.py +294 -0
- pySimBlocks/blocks/sources/function_source.py +197 -0
- pySimBlocks/blocks/sources/ramp.py +99 -0
- pySimBlocks/blocks/sources/sinusoidal.py +113 -0
- pySimBlocks/blocks/sources/step.py +138 -0
- pySimBlocks/blocks/sources/white_noise.py +107 -0
- pySimBlocks/blocks/systems/__init__.py +27 -0
- pySimBlocks/blocks/systems/linear_state_space.py +180 -0
- pySimBlocks/blocks/systems/non_linear_state_space.py +309 -0
- pySimBlocks/blocks/systems/polytopic_state_space.py +193 -0
- pySimBlocks/blocks/systems/sofa/__init__.py +30 -0
- pySimBlocks/blocks/systems/sofa/sofa_controller.py +472 -0
- pySimBlocks/blocks/systems/sofa/sofa_exchange_i_o.py +120 -0
- pySimBlocks/blocks/systems/sofa/sofa_plant.py +306 -0
- pySimBlocks/cli.py +127 -0
- pySimBlocks/core/__init__.py +32 -0
- pySimBlocks/core/block.py +167 -0
- pySimBlocks/core/block_source.py +94 -0
- pySimBlocks/core/config.py +90 -0
- pySimBlocks/core/fixed_time_manager.py +46 -0
- pySimBlocks/core/model.py +250 -0
- pySimBlocks/core/scheduler.py +30 -0
- pySimBlocks/core/simulator.py +329 -0
- pySimBlocks/core/task.py +73 -0
- pySimBlocks/docs/blocks/controllers/pid.md +78 -0
- pySimBlocks/docs/blocks/controllers/state_feedback.md +60 -0
- pySimBlocks/docs/blocks/interfaces/external_input.md +39 -0
- pySimBlocks/docs/blocks/interfaces/external_output.md +39 -0
- pySimBlocks/docs/blocks/observers/luenberger.md +70 -0
- pySimBlocks/docs/blocks/operators/algebraic_function.md +76 -0
- pySimBlocks/docs/blocks/operators/dead_zone.md +67 -0
- pySimBlocks/docs/blocks/operators/delay.md +76 -0
- pySimBlocks/docs/blocks/operators/demux.md +64 -0
- pySimBlocks/docs/blocks/operators/discrete_derivator.md +61 -0
- pySimBlocks/docs/blocks/operators/discrete_integrator.md +87 -0
- pySimBlocks/docs/blocks/operators/gain.md +97 -0
- pySimBlocks/docs/blocks/operators/mux.md +64 -0
- pySimBlocks/docs/blocks/operators/product.md +51 -0
- pySimBlocks/docs/blocks/operators/rate_limiter.md +70 -0
- pySimBlocks/docs/blocks/operators/saturation.md +62 -0
- pySimBlocks/docs/blocks/operators/sum.md +49 -0
- pySimBlocks/docs/blocks/operators/zero_order_hold.md +61 -0
- pySimBlocks/docs/blocks/optimizers/quadratic_program.md +98 -0
- pySimBlocks/docs/blocks/sources/chirp.md +102 -0
- pySimBlocks/docs/blocks/sources/constant.md +53 -0
- pySimBlocks/docs/blocks/sources/file_source.md +51 -0
- pySimBlocks/docs/blocks/sources/function_source.md +52 -0
- pySimBlocks/docs/blocks/sources/ramp.md +58 -0
- pySimBlocks/docs/blocks/sources/sinusoidal.md +60 -0
- pySimBlocks/docs/blocks/sources/step.md +63 -0
- pySimBlocks/docs/blocks/sources/white_noise.md +58 -0
- pySimBlocks/docs/blocks/systems/linear_state_space.md +66 -0
- pySimBlocks/docs/blocks/systems/non_linear_state_space.md +90 -0
- pySimBlocks/docs/blocks/systems/polytopic_state_space.md +72 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_exchange_i_o.md +63 -0
- pySimBlocks/docs/blocks/systems/sofa/sofa_plant.md +62 -0
- pySimBlocks/gui/__init__.py +20 -0
- pySimBlocks/gui/addons/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/__init__.py +19 -0
- pySimBlocks/gui/addons/sofa/sofa_dialog.py +190 -0
- pySimBlocks/gui/addons/sofa/sofa_service.py +209 -0
- pySimBlocks/gui/blocks/block_dialog_session.py +47 -0
- pySimBlocks/gui/blocks/block_meta.py +408 -0
- pySimBlocks/gui/blocks/controllers/pid.py +116 -0
- pySimBlocks/gui/blocks/controllers/state_feedback.py +80 -0
- pySimBlocks/gui/blocks/interfaces/external_input.py +64 -0
- pySimBlocks/gui/blocks/interfaces/external_output.py +63 -0
- pySimBlocks/gui/blocks/observers/luenberger.py +108 -0
- pySimBlocks/gui/blocks/operators/algebraic_function.py +232 -0
- pySimBlocks/gui/blocks/operators/dead_zone.py +79 -0
- pySimBlocks/gui/blocks/operators/delay.py +79 -0
- pySimBlocks/gui/blocks/operators/demux.py +98 -0
- pySimBlocks/gui/blocks/operators/discrete_derivator.py +67 -0
- pySimBlocks/gui/blocks/operators/discrete_integrator.py +80 -0
- pySimBlocks/gui/blocks/operators/gain.py +79 -0
- pySimBlocks/gui/blocks/operators/mux.py +100 -0
- pySimBlocks/gui/blocks/operators/product.py +109 -0
- pySimBlocks/gui/blocks/operators/rate_limiter.py +79 -0
- pySimBlocks/gui/blocks/operators/saturation.py +72 -0
- pySimBlocks/gui/blocks/operators/sum.py +91 -0
- pySimBlocks/gui/blocks/operators/zero_order_hold.py +64 -0
- pySimBlocks/gui/blocks/optimizers/quadratic_program.py +128 -0
- pySimBlocks/gui/blocks/parameter_meta.py +34 -0
- pySimBlocks/gui/blocks/port_meta.py +31 -0
- pySimBlocks/gui/blocks/sources/chirp.py +146 -0
- pySimBlocks/gui/blocks/sources/constant.py +63 -0
- pySimBlocks/gui/blocks/sources/file_source.py +196 -0
- pySimBlocks/gui/blocks/sources/function_source.py +202 -0
- pySimBlocks/gui/blocks/sources/ramp.py +73 -0
- pySimBlocks/gui/blocks/sources/sinusoidal.py +81 -0
- pySimBlocks/gui/blocks/sources/step.py +83 -0
- pySimBlocks/gui/blocks/sources/white_noise.py +70 -0
- pySimBlocks/gui/blocks/systems/linear_state_space.py +102 -0
- pySimBlocks/gui/blocks/systems/non_linear_state_space.py +233 -0
- pySimBlocks/gui/blocks/systems/polytopic_state_space.py +109 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py +221 -0
- pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py +220 -0
- pySimBlocks/gui/dialogs/__init__.py +19 -0
- pySimBlocks/gui/dialogs/block_dialog.py +125 -0
- pySimBlocks/gui/dialogs/display_yaml_dialog.py +89 -0
- pySimBlocks/gui/dialogs/help_dialog.py +39 -0
- pySimBlocks/gui/dialogs/plot_dialog.py +230 -0
- pySimBlocks/gui/dialogs/settings/plots.py +187 -0
- pySimBlocks/gui/dialogs/settings/project.py +146 -0
- pySimBlocks/gui/dialogs/settings/simulation.py +137 -0
- pySimBlocks/gui/dialogs/settings_dialog.py +88 -0
- pySimBlocks/gui/dialogs/unsaved_dialog.py +92 -0
- pySimBlocks/gui/editor.py +49 -0
- pySimBlocks/gui/graphics/__init__.py +29 -0
- pySimBlocks/gui/graphics/block_item.py +289 -0
- pySimBlocks/gui/graphics/connection_item.py +266 -0
- pySimBlocks/gui/graphics/port_item.py +153 -0
- pySimBlocks/gui/graphics/theme.py +119 -0
- pySimBlocks/gui/main_window.py +162 -0
- pySimBlocks/gui/models/__init__.py +31 -0
- pySimBlocks/gui/models/block_instance.py +114 -0
- pySimBlocks/gui/models/connection_instance.py +43 -0
- pySimBlocks/gui/models/port_instance.py +52 -0
- pySimBlocks/gui/models/project_simulation_params.py +50 -0
- pySimBlocks/gui/models/project_state.py +114 -0
- pySimBlocks/gui/project_controller.py +302 -0
- pySimBlocks/gui/services/__init__.py +20 -0
- pySimBlocks/gui/services/project_loader.py +381 -0
- pySimBlocks/gui/services/project_saver.py +68 -0
- pySimBlocks/gui/services/simulation_runner.py +66 -0
- pySimBlocks/gui/services/yaml_tools.py +261 -0
- pySimBlocks/gui/widgets/__init__.py +19 -0
- pySimBlocks/gui/widgets/block_list.py +131 -0
- pySimBlocks/gui/widgets/diagram_view.py +312 -0
- pySimBlocks/gui/widgets/toolbar_view.py +161 -0
- pySimBlocks/project/__init__.py +35 -0
- pySimBlocks/project/build_model.py +92 -0
- pySimBlocks/project/generate_run_script.py +109 -0
- pySimBlocks/project/generate_sofa_controller.py +215 -0
- pySimBlocks/project/load_project_config.py +197 -0
- pySimBlocks/project/load_simulation_config.py +166 -0
- pySimBlocks/project/load_simulator.py +45 -0
- pySimBlocks/project/plot_from_config.py +174 -0
- pySimBlocks/project/pySimBlocks_blocks_index.yaml +106 -0
- pySimBlocks/real_time/__init__.py +23 -0
- pySimBlocks/real_time/real_time_runner.py +148 -0
- pySimBlocks/tools/__init__.py +19 -0
- pySimBlocks/tools/blocks_registry.py +141 -0
- pySimBlocks/tools/generate_blocks_index.py +124 -0
- pysimblocks-0.1.0.dist-info/METADATA +180 -0
- pysimblocks-0.1.0.dist-info/RECORD +262 -0
- pysimblocks-0.1.0.dist-info/WHEEL +5 -0
- pysimblocks-0.1.0.dist-info/entry_points.txt +2 -0
- pysimblocks-0.1.0.dist-info/licenses/LICENSE.md +165 -0
- pysimblocks-0.1.0.dist-info/top_level.txt +3 -0
- tests/blocks/controllers/test_pid.py +143 -0
- tests/blocks/controllers/test_state_feedback.py +193 -0
- tests/blocks/observers/test_luenberger.py +225 -0
- tests/blocks/operators/test_algebraic_function.py +230 -0
- tests/blocks/operators/test_dead_zone.py +106 -0
- tests/blocks/operators/test_delay.py +167 -0
- tests/blocks/operators/test_demux.py +111 -0
- tests/blocks/operators/test_discrete_derivator.py +160 -0
- tests/blocks/operators/test_discrete_integrator.py +149 -0
- tests/blocks/operators/test_gain.py +182 -0
- tests/blocks/operators/test_mux.py +101 -0
- tests/blocks/operators/test_product.py +171 -0
- tests/blocks/operators/test_rate_limiter.py +166 -0
- tests/blocks/operators/test_saturation.py +157 -0
- tests/blocks/operators/test_sum.py +183 -0
- tests/blocks/operators/test_zero_order_hold.py +106 -0
- tests/blocks/sources/test_chirp.py +157 -0
- tests/blocks/sources/test_constant.py +55 -0
- tests/blocks/sources/test_file_source.py +185 -0
- tests/blocks/sources/test_function_source.py +135 -0
- tests/blocks/sources/test_ramp.py +110 -0
- tests/blocks/sources/test_sinusoidal.py +113 -0
- tests/blocks/sources/test_step.py +76 -0
- tests/blocks/sources/test_white_noise.py +108 -0
- tests/blocks/systems/test_linear_state_space.py +135 -0
- tests/blocks/systems/test_polytopic_state_space.py +133 -0
- tests/core/test_core_errors_and_determinism.py +82 -0
- tests/core/test_core_multirate_tasks.py +88 -0
- tests/core/test_core_two_phase_and_propagation.py +132 -0
- tests/gui/main_window_open_test.py +162 -0
- tests/gui/test_block_meta_param_cache.py +16 -0
- tests/gui/test_file_source_meta.py +17 -0
- tests/gui/test_function_source_meta.py +28 -0
- tests/regression/test_simulink_dc_motor.py +217 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
# tests/blocks/observers/test_luenberger.py
|
|
2
|
+
|
|
3
|
+
import numpy as np
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from pySimBlocks.core import Model, Simulator, SimulationConfig
|
|
7
|
+
from pySimBlocks.blocks.sources.constant import Constant
|
|
8
|
+
from pySimBlocks.blocks.sources.step import Step
|
|
9
|
+
from pySimBlocks.blocks.observers.luenberger import Luenberger
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# ------------------------------------------------------------
|
|
13
|
+
# Helpers
|
|
14
|
+
# ------------------------------------------------------------
|
|
15
|
+
def run_sim(obs: Luenberger, u_src, y_src, dt=0.1, T=0.3, logging=None):
|
|
16
|
+
m = Model()
|
|
17
|
+
m.add_block(u_src)
|
|
18
|
+
m.add_block(y_src)
|
|
19
|
+
m.add_block(obs)
|
|
20
|
+
|
|
21
|
+
m.connect(u_src.name, "out", obs.name, "u")
|
|
22
|
+
m.connect(y_src.name, "out", obs.name, "y")
|
|
23
|
+
|
|
24
|
+
if logging is None:
|
|
25
|
+
logging = [
|
|
26
|
+
f"{obs.name}.outputs.y_hat",
|
|
27
|
+
f"{obs.name}.outputs.x_hat",
|
|
28
|
+
f"{obs.name}.state.x_hat",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
sim_cfg = SimulationConfig(dt, T, logging=logging)
|
|
32
|
+
sim = Simulator(m, sim_cfg)
|
|
33
|
+
logs = sim.run()
|
|
34
|
+
return logs
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def manual_observer_rollout(A, B, C, L, x0, u_seq, y_seq):
|
|
38
|
+
"""
|
|
39
|
+
Returns:
|
|
40
|
+
y_hat_seq: list of y_hat[k] = C x[k]
|
|
41
|
+
x_next_seq: list of x[k+1]
|
|
42
|
+
"""
|
|
43
|
+
x = x0.copy()
|
|
44
|
+
y_hat_seq = []
|
|
45
|
+
x_next_seq = []
|
|
46
|
+
|
|
47
|
+
for (u, y) in zip(u_seq, y_seq):
|
|
48
|
+
y_hat = C @ x
|
|
49
|
+
x_next = A @ x + B @ u + L @ (y - y_hat)
|
|
50
|
+
|
|
51
|
+
y_hat_seq.append(y_hat.copy())
|
|
52
|
+
x_next_seq.append(x_next.copy())
|
|
53
|
+
x = x_next
|
|
54
|
+
|
|
55
|
+
return y_hat_seq, x_next_seq
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------
|
|
59
|
+
# 1) Basic correctness (state + output timing)
|
|
60
|
+
# ------------------------------------------------------------
|
|
61
|
+
def test_luenberger_basic_update_and_logging_timing():
|
|
62
|
+
# Dimensions: n=2, m=1, p=1
|
|
63
|
+
A = np.array([[0.9, 0.1],
|
|
64
|
+
[0.0, 0.95]], dtype=float)
|
|
65
|
+
B = np.array([[0.1],
|
|
66
|
+
[0.05]], dtype=float)
|
|
67
|
+
C = np.array([[1.0, 0.0]], dtype=float)
|
|
68
|
+
L = np.array([[0.5],
|
|
69
|
+
[0.1]], dtype=float)
|
|
70
|
+
|
|
71
|
+
obs = Luenberger("obs", A=A, B=B, C=C, L=L, x0=None)
|
|
72
|
+
|
|
73
|
+
# Use constants so u[k] and y[k] are stable and easy to predict
|
|
74
|
+
u_src = Constant("u_src", [[1.0]]) # u = 1
|
|
75
|
+
y_src = Constant("y_src", [[0.0]]) # y = 0
|
|
76
|
+
|
|
77
|
+
dt = 0.1
|
|
78
|
+
T = 0.3 # 3 steps -> logs at k=0,1,2
|
|
79
|
+
|
|
80
|
+
logs = run_sim(
|
|
81
|
+
obs,
|
|
82
|
+
u_src=u_src,
|
|
83
|
+
y_src=y_src,
|
|
84
|
+
dt=dt,
|
|
85
|
+
T=T,
|
|
86
|
+
logging=[
|
|
87
|
+
"obs.outputs.y_hat",
|
|
88
|
+
"obs.state.x_hat",
|
|
89
|
+
],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
# Manual reference:
|
|
93
|
+
# log index k corresponds to:
|
|
94
|
+
# - y_hat[k] computed from x[k]
|
|
95
|
+
# - state.x_hat already committed to x[k+1]
|
|
96
|
+
x0 = np.zeros((2, 1), dtype=float)
|
|
97
|
+
u_seq = [np.array([[1.0]]) for _ in range(3)]
|
|
98
|
+
y_seq = [np.array([[0.0]]) for _ in range(3)]
|
|
99
|
+
y_hat_ref, x_next_ref = manual_observer_rollout(A, B, C, L, x0, u_seq, y_seq)
|
|
100
|
+
|
|
101
|
+
# outputs.y_hat
|
|
102
|
+
assert np.allclose(logs["obs.outputs.y_hat"][0], y_hat_ref[0])
|
|
103
|
+
assert np.allclose(logs["obs.outputs.y_hat"][1], y_hat_ref[1])
|
|
104
|
+
assert np.allclose(logs["obs.outputs.y_hat"][2], y_hat_ref[2])
|
|
105
|
+
|
|
106
|
+
# state.x_hat is x[k+1]
|
|
107
|
+
assert np.allclose(logs["obs.state.x_hat"][0], x_next_ref[0])
|
|
108
|
+
assert np.allclose(logs["obs.state.x_hat"][1], x_next_ref[1])
|
|
109
|
+
assert np.allclose(logs["obs.state.x_hat"][2], x_next_ref[2])
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# ------------------------------------------------------------
|
|
113
|
+
# 2) Provided x0 should be respected
|
|
114
|
+
# ------------------------------------------------------------
|
|
115
|
+
def test_luenberger_respects_x0():
|
|
116
|
+
A = np.eye(2)
|
|
117
|
+
B = np.zeros((2, 1))
|
|
118
|
+
C = np.array([[1.0, 0.0]])
|
|
119
|
+
L = np.zeros((2, 1))
|
|
120
|
+
|
|
121
|
+
x0 = np.array([[2.0], [3.0]])
|
|
122
|
+
obs = Luenberger("obs", A=A, B=B, C=C, L=L, x0=x0)
|
|
123
|
+
|
|
124
|
+
u_src = Constant("u_src", [[0.0]])
|
|
125
|
+
y_src = Constant("y_src", [[0.0]])
|
|
126
|
+
|
|
127
|
+
logs = run_sim(
|
|
128
|
+
obs,
|
|
129
|
+
u_src=u_src,
|
|
130
|
+
y_src=y_src,
|
|
131
|
+
dt=0.1,
|
|
132
|
+
T=0.1,
|
|
133
|
+
logging=["obs.outputs.y_hat", "obs.state.x_hat"],
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# At k=0: y_hat[0] = C x0 = 2
|
|
137
|
+
assert np.allclose(logs["obs.outputs.y_hat"][0], [[2.0]])
|
|
138
|
+
# state after first step is x1 = A x0 = x0 (since everything else is zero)
|
|
139
|
+
assert np.allclose(logs["obs.state.x_hat"][0], x0)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
# ------------------------------------------------------------
|
|
143
|
+
# 3) Missing inputs should raise (at runtime state_update)
|
|
144
|
+
# ------------------------------------------------------------
|
|
145
|
+
def test_luenberger_missing_u_raises():
|
|
146
|
+
A = np.eye(1)
|
|
147
|
+
B = np.ones((1, 1))
|
|
148
|
+
C = np.ones((1, 1))
|
|
149
|
+
L = np.ones((1, 1))
|
|
150
|
+
|
|
151
|
+
obs = Luenberger("obs", A=A, B=B, C=C, L=L)
|
|
152
|
+
y_src = Constant("y_src", [[0.0]])
|
|
153
|
+
|
|
154
|
+
m = Model()
|
|
155
|
+
m.add_block(y_src)
|
|
156
|
+
m.add_block(obs)
|
|
157
|
+
# only connect y
|
|
158
|
+
m.connect("y_src", "out", "obs", "y")
|
|
159
|
+
|
|
160
|
+
sim = Simulator(m, SimulationConfig(0.1, 0.1, logging=["obs.outputs.y_hat"]))
|
|
161
|
+
sim.initialize()
|
|
162
|
+
with pytest.raises(RuntimeError, match="Input 'u'"):
|
|
163
|
+
sim.step()
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_luenberger_missing_y_raises():
|
|
167
|
+
A = np.eye(1)
|
|
168
|
+
B = np.ones((1, 1))
|
|
169
|
+
C = np.ones((1, 1))
|
|
170
|
+
L = np.ones((1, 1))
|
|
171
|
+
|
|
172
|
+
obs = Luenberger("obs", A=A, B=B, C=C, L=L)
|
|
173
|
+
u_src = Constant("u_src", [[0.0]])
|
|
174
|
+
|
|
175
|
+
m = Model()
|
|
176
|
+
m.add_block(u_src)
|
|
177
|
+
m.add_block(obs)
|
|
178
|
+
# only connect u
|
|
179
|
+
m.connect("u_src", "out", "obs", "u")
|
|
180
|
+
|
|
181
|
+
sim = Simulator(m, SimulationConfig(0.1, 0.1, logging=["obs.outputs.y_hat"]))
|
|
182
|
+
sim.initialize()
|
|
183
|
+
with pytest.raises(RuntimeError, match="Input 'y'"):
|
|
184
|
+
sim.step()
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
# ------------------------------------------------------------
|
|
188
|
+
# 4) Invalid matrix dimensions should raise at construction
|
|
189
|
+
# ------------------------------------------------------------
|
|
190
|
+
def test_luenberger_invalid_dimensions():
|
|
191
|
+
A = np.eye(2)
|
|
192
|
+
B = np.ones((3, 1)) # wrong rows
|
|
193
|
+
C = np.ones((1, 2))
|
|
194
|
+
L = np.ones((2, 1))
|
|
195
|
+
|
|
196
|
+
with pytest.raises(ValueError):
|
|
197
|
+
Luenberger("obs", A=A, B=B, C=C, L=L)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
# ------------------------------------------------------------
|
|
201
|
+
# 5) Bad input shape (wrong number of elements) triggers reshape error
|
|
202
|
+
# ------------------------------------------------------------
|
|
203
|
+
def test_luenberger_bad_input_size_raises_value_error():
|
|
204
|
+
A = np.eye(2)
|
|
205
|
+
B = np.ones((2, 1))
|
|
206
|
+
C = np.ones((1, 2))
|
|
207
|
+
L = np.ones((2, 1))
|
|
208
|
+
obs = Luenberger("obs", A=A, B=B, C=C, L=L)
|
|
209
|
+
|
|
210
|
+
# u must be (1,1) -> OK, y must be (1,1) -> but we provide 2 elements
|
|
211
|
+
u_src = Constant("u_src", [[0.0]])
|
|
212
|
+
y_src = Constant("y_src", [[1.0], [2.0]]) # wrong total size for (1,1)
|
|
213
|
+
|
|
214
|
+
m = Model()
|
|
215
|
+
m.add_block(u_src)
|
|
216
|
+
m.add_block(y_src)
|
|
217
|
+
m.add_block(obs)
|
|
218
|
+
m.connect("u_src", "out", "obs", "u")
|
|
219
|
+
m.connect("y_src", "out", "obs", "y")
|
|
220
|
+
|
|
221
|
+
sim = Simulator(m, SimulationConfig(0.1, 0.1, logging=["obs.outputs.y_hat"]))
|
|
222
|
+
sim.initialize()
|
|
223
|
+
with pytest.raises(ValueError):
|
|
224
|
+
sim.step()
|
|
225
|
+
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
|
|
2
|
+
import numpy as np
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from pySimBlocks.blocks.operators.algebraic_function import AlgebraicFunction
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
# ---------------------------------------------------------------------
|
|
9
|
+
# Helpers
|
|
10
|
+
# ---------------------------------------------------------------------
|
|
11
|
+
def make_block(func, input_keys=("u",), output_keys=("y",)):
|
|
12
|
+
return AlgebraicFunction(
|
|
13
|
+
name="F",
|
|
14
|
+
function=func,
|
|
15
|
+
input_keys=list(input_keys),
|
|
16
|
+
output_keys=list(output_keys),
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# ---------------------------------------------------------------------
|
|
21
|
+
# 1) Basic pass-through (scalar as (1,1))
|
|
22
|
+
# ---------------------------------------------------------------------
|
|
23
|
+
def test_algebraic_function_pass_through_scalar():
|
|
24
|
+
def f(t, dt, u):
|
|
25
|
+
if u is None:
|
|
26
|
+
return {"y": np.zeros((1, 1))}
|
|
27
|
+
return {"y": u}
|
|
28
|
+
|
|
29
|
+
blk = make_block(f, input_keys=("u",), output_keys=("y",))
|
|
30
|
+
blk.initialize(0.0)
|
|
31
|
+
|
|
32
|
+
blk.inputs["u"] = np.array([[2.0]])
|
|
33
|
+
blk.output_update(0.0, 0.1)
|
|
34
|
+
|
|
35
|
+
assert np.allclose(blk.outputs["y"], [[2.0]])
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
# ---------------------------------------------------------------------
|
|
39
|
+
# 2) Matrix support
|
|
40
|
+
# ---------------------------------------------------------------------
|
|
41
|
+
def test_algebraic_function_matrix_support():
|
|
42
|
+
def f(t, dt, A, B):
|
|
43
|
+
if A is None or B is None:
|
|
44
|
+
return {"Y": np.zeros((1, 1))}
|
|
45
|
+
return {"Y": A @ B}
|
|
46
|
+
|
|
47
|
+
blk = make_block(f, input_keys=("A", "B"), output_keys=("Y",))
|
|
48
|
+
blk.initialize(0.0)
|
|
49
|
+
|
|
50
|
+
A = np.array([[1.0, 2.0],
|
|
51
|
+
[3.0, 4.0]])
|
|
52
|
+
B = np.array([[5.0],
|
|
53
|
+
[6.0]])
|
|
54
|
+
|
|
55
|
+
blk.inputs["A"] = A
|
|
56
|
+
blk.inputs["B"] = B
|
|
57
|
+
blk.output_update(0.0, 0.1)
|
|
58
|
+
|
|
59
|
+
assert np.allclose(blk.outputs["Y"], A @ B)
|
|
60
|
+
assert blk.outputs["Y"].shape == (2, 1)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------
|
|
64
|
+
# 3) Multiple outputs
|
|
65
|
+
# ---------------------------------------------------------------------
|
|
66
|
+
def test_algebraic_function_multiple_outputs():
|
|
67
|
+
def f(t, dt, u):
|
|
68
|
+
if u is None:
|
|
69
|
+
return {"y1": np.zeros((1, 1)), "y2": np.zeros((1, 1))}
|
|
70
|
+
return {"y1": u, "y2": 2.0 * u}
|
|
71
|
+
|
|
72
|
+
blk = make_block(f, input_keys=("u",), output_keys=("y1", "y2"))
|
|
73
|
+
blk.initialize(0.0)
|
|
74
|
+
|
|
75
|
+
u = np.array([[3.0], [4.0]])
|
|
76
|
+
blk.inputs["u"] = u
|
|
77
|
+
blk.output_update(0.0, 0.1)
|
|
78
|
+
|
|
79
|
+
assert np.allclose(blk.outputs["y1"], u)
|
|
80
|
+
assert np.allclose(blk.outputs["y2"], 2.0 * u)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
# ---------------------------------------------------------------------
|
|
84
|
+
# 4) Missing input -> RuntimeError
|
|
85
|
+
# ---------------------------------------------------------------------
|
|
86
|
+
def test_algebraic_function_missing_input_raises():
|
|
87
|
+
def f(t, dt, u):
|
|
88
|
+
if u is None:
|
|
89
|
+
return {"y": np.zeros((1, 1))}
|
|
90
|
+
return {"y": u}
|
|
91
|
+
|
|
92
|
+
blk = make_block(f, input_keys=("u",), output_keys=("y",))
|
|
93
|
+
blk.initialize(0.0)
|
|
94
|
+
|
|
95
|
+
blk.inputs["u"] = None
|
|
96
|
+
with pytest.raises(RuntimeError) as err:
|
|
97
|
+
blk.output_update(0.0, 0.1)
|
|
98
|
+
|
|
99
|
+
assert "not set" in str(err.value).lower()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ---------------------------------------------------------------------
|
|
103
|
+
# 5) Signature mismatch (input_keys != function args) -> ValueError in initialize
|
|
104
|
+
# ---------------------------------------------------------------------
|
|
105
|
+
def test_algebraic_function_signature_mismatch_raises():
|
|
106
|
+
# function declares 'u', but block expects 'x'
|
|
107
|
+
def f(t, dt, u):
|
|
108
|
+
if u is None:
|
|
109
|
+
return {"y": np.zeros((1, 1))}
|
|
110
|
+
return {"y": u}
|
|
111
|
+
|
|
112
|
+
blk = make_block(f, input_keys=("x",), output_keys=("y",))
|
|
113
|
+
with pytest.raises(ValueError) as err:
|
|
114
|
+
blk.initialize(0.0)
|
|
115
|
+
|
|
116
|
+
assert "arguments mismatch" in str(err.value).lower()
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
# ---------------------------------------------------------------------
|
|
120
|
+
# 6) Function must return a dict
|
|
121
|
+
# ---------------------------------------------------------------------
|
|
122
|
+
def test_algebraic_function_return_not_dict_raises():
|
|
123
|
+
def f(t, dt, u):
|
|
124
|
+
if u is None:
|
|
125
|
+
return {"y": np.zeros((1, 1))}
|
|
126
|
+
return u # invalid
|
|
127
|
+
|
|
128
|
+
blk = make_block(f, input_keys=("u",), output_keys=("y",))
|
|
129
|
+
blk.initialize(0.0)
|
|
130
|
+
|
|
131
|
+
blk.inputs["u"] = np.array([[1.0]])
|
|
132
|
+
with pytest.raises(RuntimeError) as err:
|
|
133
|
+
blk.output_update(0.0, 0.1)
|
|
134
|
+
|
|
135
|
+
assert "must return a dict" in str(err.value).lower()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# ---------------------------------------------------------------------
|
|
139
|
+
# 7) Output keys mismatch
|
|
140
|
+
# ---------------------------------------------------------------------
|
|
141
|
+
def test_algebraic_function_output_keys_mismatch_raises():
|
|
142
|
+
def f(t, dt, u):
|
|
143
|
+
if u is None:
|
|
144
|
+
return {"y": np.zeros((1, 1))}
|
|
145
|
+
return {"z": u} # wrong key
|
|
146
|
+
|
|
147
|
+
blk = make_block(f, input_keys=("u",), output_keys=("y",))
|
|
148
|
+
blk.initialize(0.0)
|
|
149
|
+
|
|
150
|
+
blk.inputs["u"] = np.array([[1.0]])
|
|
151
|
+
with pytest.raises(RuntimeError) as err:
|
|
152
|
+
blk.output_update(0.0, 0.1)
|
|
153
|
+
|
|
154
|
+
assert "missing output keys" in str(err.value).lower()
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
# ---------------------------------------------------------------------
|
|
158
|
+
# 8) Output must be 2D
|
|
159
|
+
# ---------------------------------------------------------------------
|
|
160
|
+
def test_algebraic_function_output_must_be_2d():
|
|
161
|
+
def f(t, dt, u):
|
|
162
|
+
return {"y": np.array([1.0, 2.0])} # 1D invalid
|
|
163
|
+
|
|
164
|
+
blk = make_block(f, input_keys=("u",), output_keys=("y",))
|
|
165
|
+
blk.initialize(0.0)
|
|
166
|
+
|
|
167
|
+
blk.inputs["u"] = np.array([[1.0]])
|
|
168
|
+
with pytest.raises(ValueError) as err:
|
|
169
|
+
blk.output_update(0.0, 0.1)
|
|
170
|
+
|
|
171
|
+
assert "must be a 2d" in str(err.value).lower()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# ---------------------------------------------------------------------
|
|
175
|
+
# 9) Input shape freeze: once seen, shape cannot change
|
|
176
|
+
# ---------------------------------------------------------------------
|
|
177
|
+
def test_algebraic_function_input_shape_change_raises():
|
|
178
|
+
def f(t, dt, u):
|
|
179
|
+
if u is None:
|
|
180
|
+
return {"y": np.zeros((1, 1))}
|
|
181
|
+
return {"y": u}
|
|
182
|
+
|
|
183
|
+
blk = make_block(f, input_keys=("u",), output_keys=("y",))
|
|
184
|
+
blk.initialize(0.0)
|
|
185
|
+
|
|
186
|
+
blk.inputs["u"] = np.array([[1.0]]) # (1,1)
|
|
187
|
+
blk.output_update(0.0, 0.1)
|
|
188
|
+
|
|
189
|
+
blk.inputs["u"] = np.array([[1.0, 2.0],
|
|
190
|
+
[3.0, 4.0]]) # (2,2) -> shape change
|
|
191
|
+
with pytest.raises(ValueError) as err:
|
|
192
|
+
blk.output_update(0.1, 0.1)
|
|
193
|
+
|
|
194
|
+
assert "shape changed" in str(err.value).lower()
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
# ---------------------------------------------------------------------
|
|
198
|
+
# 10) Output shape freeze: once produced, shape cannot change
|
|
199
|
+
# ---------------------------------------------------------------------
|
|
200
|
+
def test_algebraic_function_output_shape_change_raises():
|
|
201
|
+
# function changes output shape depending on time -> should raise at second call
|
|
202
|
+
def f(t, dt, u):
|
|
203
|
+
if t < 0.1:
|
|
204
|
+
return {"y": np.array([[1.0]])} # (1,1)
|
|
205
|
+
return {"y": np.array([[1.0, 2.0]])} # (1,2) -> shape change
|
|
206
|
+
|
|
207
|
+
blk = make_block(f, input_keys=("u",), output_keys=("y",))
|
|
208
|
+
blk.initialize(0.0)
|
|
209
|
+
|
|
210
|
+
blk.inputs["u"] = np.array([[0.0]]) # dummy input (shape fixed too)
|
|
211
|
+
blk.output_update(0.0, 0.1)
|
|
212
|
+
|
|
213
|
+
with pytest.raises(ValueError) as err:
|
|
214
|
+
blk.output_update(0.1, 0.1)
|
|
215
|
+
|
|
216
|
+
assert "shape changed" in str(err.value).lower()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def test_algebraic_function_func_error():
|
|
220
|
+
def f(t, dt, u):
|
|
221
|
+
return 2 * u # invalid
|
|
222
|
+
|
|
223
|
+
blk = make_block(f, input_keys=("u",), output_keys=("y",))
|
|
224
|
+
with pytest.raises(RuntimeError) as err:
|
|
225
|
+
blk.initialize(0.0)
|
|
226
|
+
|
|
227
|
+
assert "function call error" in str(err.value).lower()
|
|
228
|
+
|
|
229
|
+
if __name__ == "__main__":
|
|
230
|
+
test_algebraic_function_func_error()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from pySimBlocks.core import Model, Simulator, SimulationConfig
|
|
5
|
+
from pySimBlocks.blocks.sources.constant import Constant
|
|
6
|
+
from pySimBlocks.blocks.operators.dead_zone import DeadZone
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_sim(src, dz, dt=0.1, T=0.1):
|
|
10
|
+
m = Model()
|
|
11
|
+
m.add_block(src)
|
|
12
|
+
m.add_block(dz)
|
|
13
|
+
m.connect(src.name, "out", dz.name, "in")
|
|
14
|
+
|
|
15
|
+
sim_cfg = SimulationConfig(dt, T, logging=[f"{dz.name}.outputs.out"])
|
|
16
|
+
sim = Simulator(m, sim_cfg)
|
|
17
|
+
logs = sim.run()
|
|
18
|
+
return logs[f"{dz.name}.outputs.out"]
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def test_dead_zone_identity_default():
|
|
22
|
+
src = Constant("src", 2.0)
|
|
23
|
+
dz = DeadZone("dz")
|
|
24
|
+
logs = run_sim(src, dz)
|
|
25
|
+
assert np.allclose(logs[0], [[2.0]])
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_dead_zone_inside_zone():
|
|
29
|
+
src = Constant("src", 0.2)
|
|
30
|
+
dz = DeadZone("dz", lower_bound=-0.5, upper_bound=0.5)
|
|
31
|
+
logs = run_sim(src, dz)
|
|
32
|
+
assert np.allclose(logs[0], [[0.0]])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_dead_zone_above():
|
|
36
|
+
src = Constant("src", 2.0)
|
|
37
|
+
dz = DeadZone("dz", lower_bound=-0.5, upper_bound=0.5)
|
|
38
|
+
logs = run_sim(src, dz)
|
|
39
|
+
assert np.allclose(logs[0], [[1.5]])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_dead_zone_below():
|
|
43
|
+
src = Constant("src", -2.0)
|
|
44
|
+
dz = DeadZone("dz", lower_bound=-0.5, upper_bound=0.5)
|
|
45
|
+
logs = run_sim(src, dz)
|
|
46
|
+
assert np.allclose(logs[0], [[-1.5]])
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_dead_zone_vector():
|
|
50
|
+
src = Constant("src", [[0.2], [1.0], [-1.0]])
|
|
51
|
+
dz = DeadZone("dz", lower_bound=-0.3, upper_bound=0.3)
|
|
52
|
+
logs = run_sim(src, dz)
|
|
53
|
+
assert np.allclose(logs[0], [[0.0], [0.7], [-0.7]])
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def test_dead_zone_matrix_scalar_bounds():
|
|
57
|
+
src = Constant("src", [[0.2, 1.0],
|
|
58
|
+
[-1.0, 0.0]])
|
|
59
|
+
dz = DeadZone("dz", lower_bound=-0.3, upper_bound=0.3)
|
|
60
|
+
logs = run_sim(src, dz)
|
|
61
|
+
|
|
62
|
+
# Apply component-wise:
|
|
63
|
+
# 0.2 -> 0
|
|
64
|
+
# 1.0 -> 1.0 - 0.3 = 0.7
|
|
65
|
+
# -1.0 -> -1.0 - (-0.3) = -0.7
|
|
66
|
+
# 0.0 -> 0
|
|
67
|
+
expected = np.array([[0.0, 0.7],
|
|
68
|
+
[-0.7, 0.0]])
|
|
69
|
+
assert np.allclose(logs[0], expected)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_dead_zone_matrix_vector_bounds_broadcast_columns():
|
|
73
|
+
src = Constant("src", [[1.0, -1.0, 0.0],
|
|
74
|
+
[2.0, -2.0, 0.1]]) # (2,3)
|
|
75
|
+
|
|
76
|
+
# (2,1) bounds via 1D -> broadcast on 3 cols
|
|
77
|
+
dz = DeadZone("dz", lower_bound=[-0.5, -1.0], upper_bound=[0.5, 1.0])
|
|
78
|
+
logs = run_sim(src, dz)
|
|
79
|
+
|
|
80
|
+
# Row1 bounds [-0.5,0.5]:
|
|
81
|
+
# [1.0 -> 0.5], [-1.0 -> -0.5], [0.0 -> 0]
|
|
82
|
+
# Row2 bounds [-1,1]:
|
|
83
|
+
# [2 -> 1], [-2 -> -1], [0.1 -> 0]
|
|
84
|
+
expected = np.array([[0.5, -0.5, 0.0],
|
|
85
|
+
[1.0, -1.0, 0.0]])
|
|
86
|
+
assert np.allclose(logs[0], expected)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_dead_zone_invalid_bounds():
|
|
90
|
+
# Must include zero: lower <= 0 <= upper, and lower <= upper
|
|
91
|
+
with pytest.raises(ValueError):
|
|
92
|
+
dz = DeadZone("dz", lower_bound=1.0, upper_bound=0.0)
|
|
93
|
+
dz.inputs["in"] = np.array([[0.0]])
|
|
94
|
+
dz.initialize(0.0)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def test_dead_zone_missing_input():
|
|
98
|
+
dz = DeadZone("dz")
|
|
99
|
+
m = Model()
|
|
100
|
+
m.add_block(dz)
|
|
101
|
+
|
|
102
|
+
sim_cfg = SimulationConfig(0.1, 1.0)
|
|
103
|
+
sim = Simulator(m, sim_cfg)
|
|
104
|
+
|
|
105
|
+
with pytest.raises(RuntimeError):
|
|
106
|
+
sim.initialize()
|