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,108 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from pySimBlocks.blocks.sources.white_noise import WhiteNoise
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# ----------------------------------------------------------
|
|
8
|
+
# 1) Scalar noise, deterministic with seed
|
|
9
|
+
# ----------------------------------------------------------
|
|
10
|
+
def test_white_noise_scalar_reproducible():
|
|
11
|
+
s1 = WhiteNoise("n", mean=0.0, std=1.0, seed=42)
|
|
12
|
+
s2 = WhiteNoise("n", mean=0.0, std=1.0, seed=42)
|
|
13
|
+
|
|
14
|
+
s1.initialize(0.0)
|
|
15
|
+
s2.initialize(0.0)
|
|
16
|
+
|
|
17
|
+
assert np.allclose(s1.outputs["out"], s2.outputs["out"])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_white_noise_scalar_update_changes():
|
|
21
|
+
s = WhiteNoise("n", mean=2.0, std=0.5, seed=0)
|
|
22
|
+
s.initialize(0.0)
|
|
23
|
+
first = s.outputs["out"].copy()
|
|
24
|
+
s.output_update(0.1, 0.1)
|
|
25
|
+
second = s.outputs["out"]
|
|
26
|
+
|
|
27
|
+
assert not np.allclose(first, second)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
# ----------------------------------------------------------
|
|
31
|
+
# 2) Vector parameters + broadcasting (still valid)
|
|
32
|
+
# ----------------------------------------------------------
|
|
33
|
+
def test_white_noise_vector_params_shape():
|
|
34
|
+
s = WhiteNoise("n", mean=[1.0, 2.0], std=[0.1, 0.2], seed=10)
|
|
35
|
+
s.initialize(0.0)
|
|
36
|
+
assert s.outputs["out"].shape == (2, 1)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_white_noise_broadcast_scalar_mean():
|
|
40
|
+
s = WhiteNoise("n", mean=0.0, std=[1.0, 2.0, 3.0], seed=1)
|
|
41
|
+
s.initialize(0.0)
|
|
42
|
+
assert s.mean.shape == (3, 1)
|
|
43
|
+
assert s.std.shape == (3, 1)
|
|
44
|
+
assert s.outputs["out"].shape == (3, 1)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ----------------------------------------------------------
|
|
48
|
+
# 3) Matrix parameters (Option B)
|
|
49
|
+
# ----------------------------------------------------------
|
|
50
|
+
def test_white_noise_matrix_params_shape():
|
|
51
|
+
mean = np.array([[0.0, 1.0],
|
|
52
|
+
[2.0, 3.0]])
|
|
53
|
+
std = np.array([[1.0, 2.0],
|
|
54
|
+
[3.0, 4.0]])
|
|
55
|
+
s = WhiteNoise("n", mean=mean, std=std, seed=123)
|
|
56
|
+
s.initialize(0.0)
|
|
57
|
+
assert s.outputs["out"].shape == (2, 2)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_white_noise_matrix_broadcast_scalar_std():
|
|
61
|
+
mean = np.array([[0.0, 1.0],
|
|
62
|
+
[2.0, 3.0]])
|
|
63
|
+
s = WhiteNoise("n", mean=mean, std=1.0, seed=123) # scalar broadcast
|
|
64
|
+
s.initialize(0.0)
|
|
65
|
+
assert s.mean.shape == (2, 2)
|
|
66
|
+
assert s.std.shape == (2, 2)
|
|
67
|
+
assert s.outputs["out"].shape == (2, 2)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_white_noise_matrix_reproducible_same_seed():
|
|
71
|
+
mean = np.array([[0.0, 1.0],
|
|
72
|
+
[2.0, 3.0]])
|
|
73
|
+
std = np.array([[1.0, 1.0],
|
|
74
|
+
[1.0, 1.0]])
|
|
75
|
+
|
|
76
|
+
s1 = WhiteNoise("n", mean=mean, std=std, seed=999)
|
|
77
|
+
s2 = WhiteNoise("n", mean=mean, std=std, seed=999)
|
|
78
|
+
|
|
79
|
+
s1.initialize(0.0)
|
|
80
|
+
s2.initialize(0.0)
|
|
81
|
+
assert np.allclose(s1.outputs["out"], s2.outputs["out"])
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
# ----------------------------------------------------------
|
|
85
|
+
# 4) Error cases
|
|
86
|
+
# ----------------------------------------------------------
|
|
87
|
+
def test_white_noise_inconsistent_shapes():
|
|
88
|
+
with pytest.raises(ValueError):
|
|
89
|
+
WhiteNoise(
|
|
90
|
+
"n",
|
|
91
|
+
mean=np.zeros((2, 2)),
|
|
92
|
+
std=np.zeros((2, 3)), # mismatch among non-scalars
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_white_noise_negative_std_scalar():
|
|
97
|
+
with pytest.raises(ValueError):
|
|
98
|
+
WhiteNoise("n", mean=0.0, std=-1.0)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_white_noise_negative_std_matrix_elementwise():
|
|
102
|
+
with pytest.raises(ValueError):
|
|
103
|
+
WhiteNoise("n", mean=np.zeros((2, 2)), std=np.array([[1.0, -1.0], [1.0, 1.0]]))
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_white_noise_illegal_ndim():
|
|
107
|
+
with pytest.raises(ValueError):
|
|
108
|
+
WhiteNoise("n", mean=np.zeros((2, 2, 2)), std=1.0)
|
|
@@ -0,0 +1,135 @@
|
|
|
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.systems.linear_state_space import LinearStateSpace
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ------------------------------------------------------------
|
|
10
|
+
def run_sim(src, sys, dt=0.1, T=0.3):
|
|
11
|
+
m = Model()
|
|
12
|
+
m.add_block(src)
|
|
13
|
+
m.add_block(sys)
|
|
14
|
+
m.connect(src.name, "out", sys.name, "u")
|
|
15
|
+
|
|
16
|
+
sim_cfg = SimulationConfig(dt, T, logging=[f"{sys.name}.outputs.y", f"{sys.name}.outputs.x"])
|
|
17
|
+
sim = Simulator(m, sim_cfg)
|
|
18
|
+
logs = sim.run()
|
|
19
|
+
return logs
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# ------------------------------------------------------------
|
|
23
|
+
def test_lss_scalar_1state_basic():
|
|
24
|
+
# x[k+1] = 0.9 x[k] + 1*u[k], y = x
|
|
25
|
+
A = [[0.9]]
|
|
26
|
+
B = [[1.0]]
|
|
27
|
+
C = [[1.0]]
|
|
28
|
+
|
|
29
|
+
src = Constant("u", [[1.0]])
|
|
30
|
+
sys = LinearStateSpace("sys", A=A, B=B, C=C, x0=[[0.0]])
|
|
31
|
+
|
|
32
|
+
logs = run_sim(src, sys, dt=0.1, T=0.3)
|
|
33
|
+
|
|
34
|
+
y = logs["sys.outputs.y"]
|
|
35
|
+
x = logs["sys.outputs.x"]
|
|
36
|
+
|
|
37
|
+
# At t=0: x0=0 -> y0=0
|
|
38
|
+
assert np.allclose(y[0], [[0.0]])
|
|
39
|
+
assert np.allclose(x[0], [[0.0]])
|
|
40
|
+
|
|
41
|
+
# Then x1 = 0 + 0.1*? (No: discrete system is already discrete; dt not used in state update)
|
|
42
|
+
# Here: x[k+1]=0.9 x[k] + 1*u[k]
|
|
43
|
+
# Step-by-step with u=1:
|
|
44
|
+
# x1 = 0.9*0 + 1 = 1
|
|
45
|
+
# x2 = 0.9*1 + 1 = 1.9
|
|
46
|
+
# y[k] = x[k]
|
|
47
|
+
assert np.allclose(y[1], [[1.0]])
|
|
48
|
+
assert np.allclose(y[2], [[1.9]])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ------------------------------------------------------------
|
|
52
|
+
def test_lss_vector_dims():
|
|
53
|
+
# n=2, m=1, p=1
|
|
54
|
+
A = [[1.0, 0.1],
|
|
55
|
+
[0.0, 1.0]]
|
|
56
|
+
B = [[0.0],
|
|
57
|
+
[1.0]]
|
|
58
|
+
C = [[1.0, 0.0]]
|
|
59
|
+
|
|
60
|
+
src = Constant("u", [[2.0]])
|
|
61
|
+
sys = LinearStateSpace("sys", A=A, B=B, C=C, x0=[[0.0], [0.0]])
|
|
62
|
+
|
|
63
|
+
logs = run_sim(src, sys, dt=0.1, T=0.3)
|
|
64
|
+
|
|
65
|
+
y = logs["sys.outputs.y"]
|
|
66
|
+
x = logs["sys.outputs.x"]
|
|
67
|
+
|
|
68
|
+
# k=0: x0=[0,0], y0=0
|
|
69
|
+
assert np.allclose(y[0], [[0.0]])
|
|
70
|
+
|
|
71
|
+
# k=1: x1 = A x0 + B u = [0, 2]
|
|
72
|
+
assert np.allclose(x[1], [[0.0], [2.0]])
|
|
73
|
+
assert np.allclose(y[1], [[0.0]])
|
|
74
|
+
|
|
75
|
+
# k=2: x2 = A x1 + B u = [0 + 0.1*2, 2] + [0,2] = [0.2, 4]
|
|
76
|
+
assert np.allclose(x[2], [[0.2], [4.0]])
|
|
77
|
+
assert np.allclose(y[2], [[0.2]])
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
# ------------------------------------------------------------
|
|
81
|
+
def test_lss_missing_input_raises():
|
|
82
|
+
A = [[1.0]]
|
|
83
|
+
B = [[1.0]]
|
|
84
|
+
C = [[1.0]]
|
|
85
|
+
sys = LinearStateSpace("sys", A=A, B=B, C=C)
|
|
86
|
+
|
|
87
|
+
m = Model()
|
|
88
|
+
m.add_block(sys)
|
|
89
|
+
|
|
90
|
+
sim_cfg = SimulationConfig(0.1, 0.1, logging=["sys.outputs.y"])
|
|
91
|
+
sim = Simulator(m, sim_cfg)
|
|
92
|
+
sim.initialize()
|
|
93
|
+
|
|
94
|
+
with pytest.raises(RuntimeError):
|
|
95
|
+
sim.run()
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ------------------------------------------------------------
|
|
99
|
+
def test_lss_rejects_non_vector_u():
|
|
100
|
+
A = [[1.0]]
|
|
101
|
+
B = [[1.0]]
|
|
102
|
+
C = [[1.0]]
|
|
103
|
+
sys = LinearStateSpace("sys", A=A, B=B, C=C)
|
|
104
|
+
|
|
105
|
+
# Provide u as 1x2 matrix via Constant -> invalid for u (must be (m,1)=(1,1))
|
|
106
|
+
src = Constant("u", [[1.0, 2.0]])
|
|
107
|
+
|
|
108
|
+
m = Model()
|
|
109
|
+
m.add_block(src)
|
|
110
|
+
m.add_block(sys)
|
|
111
|
+
m.connect("u", "out", "sys", "u")
|
|
112
|
+
|
|
113
|
+
sim_cfg = SimulationConfig(0.1, 0.1, logging=["sys.outputs.y"])
|
|
114
|
+
sim = Simulator(m, sim_cfg)
|
|
115
|
+
|
|
116
|
+
with pytest.raises(ValueError) as err:
|
|
117
|
+
sim.run()
|
|
118
|
+
|
|
119
|
+
assert "must be a column vector" in str(err.value) or "must have shape" in str(err.value)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# ------------------------------------------------------------
|
|
123
|
+
def test_lss_invalid_matrix_dimensions():
|
|
124
|
+
# A is 2x2, B has wrong rows
|
|
125
|
+
A = np.eye(2)
|
|
126
|
+
B = np.ones((3, 1)) # wrong
|
|
127
|
+
C = np.ones((1, 2))
|
|
128
|
+
|
|
129
|
+
with pytest.raises(ValueError):
|
|
130
|
+
LinearStateSpace("sys", A=A, B=B, C=C)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
if __name__ == "__main__":
|
|
134
|
+
pytest.main([__file__, "-v"])
|
|
135
|
+
|
|
@@ -0,0 +1,133 @@
|
|
|
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.systems.polytopic_state_space import PolytopicStateSpace
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def run_sim(src_w, src_u, sys, dt=0.1, T=0.3):
|
|
10
|
+
m = Model()
|
|
11
|
+
m.add_block(src_w)
|
|
12
|
+
m.add_block(src_u)
|
|
13
|
+
m.add_block(sys)
|
|
14
|
+
m.connect(src_w.name, "out", sys.name, "w")
|
|
15
|
+
m.connect(src_u.name, "out", sys.name, "u")
|
|
16
|
+
|
|
17
|
+
sim_cfg = SimulationConfig(dt, T, logging=[f"{sys.name}.outputs.y", f"{sys.name}.outputs.x"])
|
|
18
|
+
sim = Simulator(m, sim_cfg)
|
|
19
|
+
logs = sim.run()
|
|
20
|
+
return logs
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_poly_scalar_basic():
|
|
24
|
+
# r=2, nx=1, nu=1, ny=1
|
|
25
|
+
A = [[0.8, 0.5]] # [A1 A2]
|
|
26
|
+
B = [[1.0, 2.0]] # [B1 B2]
|
|
27
|
+
C = [[1.0]]
|
|
28
|
+
|
|
29
|
+
src_w = Constant("w", [[0.25], [0.75]])
|
|
30
|
+
src_u = Constant("u", [[2.0]])
|
|
31
|
+
sys = PolytopicStateSpace("sys", A=A, B=B, C=C, x0=[[0.0]])
|
|
32
|
+
|
|
33
|
+
logs = run_sim(src_w, src_u, sys)
|
|
34
|
+
y = logs["sys.outputs.y"]
|
|
35
|
+
x = logs["sys.outputs.x"]
|
|
36
|
+
|
|
37
|
+
# x[k+1] = 0.575*x[k] + 3.5
|
|
38
|
+
assert np.allclose(y[0], [[0.0]])
|
|
39
|
+
assert np.allclose(x[1], [[3.5]])
|
|
40
|
+
assert np.allclose(y[1], [[3.5]])
|
|
41
|
+
assert np.allclose(x[2], [[5.5125]])
|
|
42
|
+
assert np.allclose(y[2], [[5.5125]])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_poly_vector_dimensions():
|
|
46
|
+
# r=2, nx=2, nu=1, ny=1
|
|
47
|
+
A1 = np.array([[1.0, 0.1], [0.0, 1.0]])
|
|
48
|
+
A2 = np.array([[0.9, 0.0], [0.0, 0.95]])
|
|
49
|
+
A = np.hstack([A1, A2])
|
|
50
|
+
|
|
51
|
+
B1 = np.array([[0.0], [1.0]])
|
|
52
|
+
B2 = np.array([[1.0], [0.0]])
|
|
53
|
+
B = np.hstack([B1, B2])
|
|
54
|
+
C = np.array([[1.0, 0.0]])
|
|
55
|
+
|
|
56
|
+
src_w = Constant("w", [[0.2], [0.8]])
|
|
57
|
+
src_u = Constant("u", [[2.0]])
|
|
58
|
+
sys = PolytopicStateSpace("sys", A=A, B=B, C=C, x0=[[0.0], [0.0]])
|
|
59
|
+
|
|
60
|
+
logs = run_sim(src_w, src_u, sys)
|
|
61
|
+
x = logs["sys.outputs.x"]
|
|
62
|
+
y = logs["sys.outputs.y"]
|
|
63
|
+
|
|
64
|
+
assert np.allclose(x[1], [[1.6], [0.4]])
|
|
65
|
+
assert np.allclose(y[1], [[1.6]])
|
|
66
|
+
assert np.allclose(x[2], [[3.08], [0.784]])
|
|
67
|
+
assert np.allclose(y[2], [[3.08]])
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_poly_missing_w_input_raises():
|
|
71
|
+
A = [[1.0, 0.0]]
|
|
72
|
+
B = [[1.0, 1.0]]
|
|
73
|
+
C = [[1.0]]
|
|
74
|
+
|
|
75
|
+
src_u = Constant("u", [[1.0]])
|
|
76
|
+
sys = PolytopicStateSpace("sys", A=A, B=B, C=C)
|
|
77
|
+
|
|
78
|
+
m = Model()
|
|
79
|
+
m.add_block(src_u)
|
|
80
|
+
m.add_block(sys)
|
|
81
|
+
m.connect("u", "out", "sys", "u")
|
|
82
|
+
|
|
83
|
+
sim_cfg = SimulationConfig(0.1, 0.1, logging=["sys.outputs.y"])
|
|
84
|
+
sim = Simulator(m, sim_cfg)
|
|
85
|
+
sim.initialize()
|
|
86
|
+
|
|
87
|
+
with pytest.raises(RuntimeError):
|
|
88
|
+
sim.run()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def test_poly_rejects_non_column_w():
|
|
92
|
+
A = [[1.0, 0.0]]
|
|
93
|
+
B = [[1.0, 1.0]]
|
|
94
|
+
C = [[1.0]]
|
|
95
|
+
|
|
96
|
+
src_w = Constant("w", [[0.2, 0.8], [0.1, 0.9]]) # 2x2, not column vector
|
|
97
|
+
src_u = Constant("u", [[1.0]])
|
|
98
|
+
sys = PolytopicStateSpace("sys", A=A, B=B, C=C)
|
|
99
|
+
|
|
100
|
+
m = Model()
|
|
101
|
+
m.add_block(src_w)
|
|
102
|
+
m.add_block(src_u)
|
|
103
|
+
m.add_block(sys)
|
|
104
|
+
m.connect("w", "out", "sys", "w")
|
|
105
|
+
m.connect("u", "out", "sys", "u")
|
|
106
|
+
|
|
107
|
+
sim_cfg = SimulationConfig(0.1, 0.1, logging=["sys.outputs.y"])
|
|
108
|
+
sim = Simulator(m, sim_cfg)
|
|
109
|
+
|
|
110
|
+
with pytest.raises(ValueError) as err:
|
|
111
|
+
sim.run()
|
|
112
|
+
|
|
113
|
+
assert "must be a column vector" in str(err.value)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def test_poly_invalid_matrix_dimensions():
|
|
117
|
+
C = [[1.0]]
|
|
118
|
+
|
|
119
|
+
# A has nx=1 but 3 columns -> not (nx, r*nx)
|
|
120
|
+
with pytest.raises(ValueError):
|
|
121
|
+
PolytopicStateSpace("sys1", A=[[1.0, 2.0, 3.0]], B=[[1.0, 1.0]], C=C)
|
|
122
|
+
|
|
123
|
+
# A => nx=1, r=2, so B must have 2*nu columns
|
|
124
|
+
with pytest.raises(ValueError):
|
|
125
|
+
PolytopicStateSpace("sys2", A=[[1.0, 2.0]], B=[[1.0, 2.0, 3.0]], C=C)
|
|
126
|
+
|
|
127
|
+
# C has wrong number of columns
|
|
128
|
+
with pytest.raises(ValueError):
|
|
129
|
+
PolytopicStateSpace("sys3", A=[[1.0, 2.0]], B=[[1.0, 1.0]], C=[[1.0, 2.0]])
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
if __name__ == "__main__":
|
|
133
|
+
pytest.main([__file__, "-v"])
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from pySimBlocks.core.block import Block
|
|
5
|
+
from pySimBlocks.core.model import Model
|
|
6
|
+
from pySimBlocks.core.config import SimulationConfig
|
|
7
|
+
from pySimBlocks.core.simulator import Simulator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RequiresInput(Block):
|
|
11
|
+
"""
|
|
12
|
+
Block that explicitly requires an input port 'u'.
|
|
13
|
+
Used to validate that missing inputs raise RuntimeError at evaluation time.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def initialize(self, t0: float):
|
|
17
|
+
self.outputs["y"] = np.array([[0.0]])
|
|
18
|
+
|
|
19
|
+
def output_update(self, t: float, dt: float):
|
|
20
|
+
if "u" not in self.inputs:
|
|
21
|
+
raise RuntimeError(f"[{self.name}] missing input 'u'")
|
|
22
|
+
self.outputs["y"] = np.array(self.inputs["u"])
|
|
23
|
+
|
|
24
|
+
def state_update(self, t: float, dt: float):
|
|
25
|
+
self.next_state = self.state
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PureSource(Block):
|
|
29
|
+
def __init__(self, name: str, value: float, sample_time: float | None = None):
|
|
30
|
+
super().__init__(name=name, sample_time=sample_time)
|
|
31
|
+
self.value = float(value)
|
|
32
|
+
|
|
33
|
+
def initialize(self, t0: float):
|
|
34
|
+
self.outputs["y"] = np.array([[self.value]])
|
|
35
|
+
|
|
36
|
+
def output_update(self, t: float, dt: float):
|
|
37
|
+
self.outputs["y"] = np.array([[self.value]])
|
|
38
|
+
|
|
39
|
+
def state_update(self, t: float, dt: float):
|
|
40
|
+
self.next_state = self.state
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _run(model: Model, cfg: SimulationConfig):
|
|
44
|
+
sim = Simulator(model=model, sim_cfg=cfg, verbose=False)
|
|
45
|
+
sim.run()
|
|
46
|
+
return sim.logs
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_missing_input_raises_runtime_error(capsys):
|
|
50
|
+
m = Model(name="missing_input_test")
|
|
51
|
+
m.add_block(RequiresInput("a"))
|
|
52
|
+
|
|
53
|
+
cfg = SimulationConfig(dt=0.01, T=0.01, t0=0.0, solver="fixed", logging=["a.outputs.y"])
|
|
54
|
+
|
|
55
|
+
with pytest.raises(RuntimeError):
|
|
56
|
+
_run(m, cfg)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_determinism_same_model_same_logs(capsys):
|
|
60
|
+
"""
|
|
61
|
+
Runs the same model twice and asserts identical logs.
|
|
62
|
+
This freezes the determinism expectation of the core.
|
|
63
|
+
"""
|
|
64
|
+
cfg = SimulationConfig(dt=0.01, T=0.03, t0=0.0, solver="fixed", logging=["p.outputs.y"])
|
|
65
|
+
|
|
66
|
+
m1 = Model(name="determinism_test_1")
|
|
67
|
+
m1.add_block(PureSource("s", value=2.0))
|
|
68
|
+
m1.add_block(RequiresInput("p"))
|
|
69
|
+
m1.connect("s", "y", "p", "u")
|
|
70
|
+
|
|
71
|
+
logs1 = _run(m1, cfg)
|
|
72
|
+
|
|
73
|
+
m2 = Model(name="determinism_test_2")
|
|
74
|
+
m2.add_block(PureSource("s", value=2.0))
|
|
75
|
+
m2.add_block(RequiresInput("p"))
|
|
76
|
+
m2.connect("s", "y", "p", "u")
|
|
77
|
+
|
|
78
|
+
logs2 = _run(m2, cfg)
|
|
79
|
+
|
|
80
|
+
assert len(logs1["p.outputs.y"]) == len(logs2["p.outputs.y"])
|
|
81
|
+
for v1, v2 in zip(logs1["p.outputs.y"], logs2["p.outputs.y"]):
|
|
82
|
+
assert np.allclose(v1, v2)
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from pySimBlocks.core.block import Block
|
|
5
|
+
from pySimBlocks.core.model import Model
|
|
6
|
+
from pySimBlocks.core.config import SimulationConfig
|
|
7
|
+
from pySimBlocks.core.simulator import Simulator
|
|
8
|
+
from pySimBlocks.core.task import Task
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class RateCounter(Block):
|
|
12
|
+
"""
|
|
13
|
+
Stateful counter incremented on each activation (i.e., when the task runs).
|
|
14
|
+
|
|
15
|
+
State:
|
|
16
|
+
count[k+1] = count[k] + 1
|
|
17
|
+
|
|
18
|
+
Output:
|
|
19
|
+
y[k] = count[k]
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def initialize(self, t0: float):
|
|
23
|
+
self.state["count"] = np.array([[0.0]])
|
|
24
|
+
self.outputs["y"] = np.array([[0.0]])
|
|
25
|
+
|
|
26
|
+
def output_update(self, t: float, dt: float):
|
|
27
|
+
self.outputs["y"] = np.array(self.state["count"])
|
|
28
|
+
# print(self.outputs["y"])
|
|
29
|
+
|
|
30
|
+
def state_update(self, t: float, dt: float):
|
|
31
|
+
self.next_state["count"] = self.state["count"] + 1.0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def test_task_get_dt_semantics(capsys):
|
|
35
|
+
"""
|
|
36
|
+
Contract test for Task.get_dt():
|
|
37
|
+
- first activation returns Ts
|
|
38
|
+
- subsequent calls return (t - last_activation)
|
|
39
|
+
|
|
40
|
+
This test is isolated from Simulator (unit test of Task).
|
|
41
|
+
"""
|
|
42
|
+
task = Task(sample_time=0.1, blocks=[], global_output_order=[])
|
|
43
|
+
|
|
44
|
+
assert task.get_dt(0.0) == pytest.approx(0.1)
|
|
45
|
+
|
|
46
|
+
# Emulate one advance cycle (as Simulator would do)
|
|
47
|
+
task.advance() # last_activation becomes 0.0
|
|
48
|
+
assert task.last_activation == pytest.approx(0.0)
|
|
49
|
+
|
|
50
|
+
assert task.get_dt(0.3) == pytest.approx(0.3 - 0.0)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_multirate_activation_and_hold(capsys):
|
|
54
|
+
"""
|
|
55
|
+
Validates that task activation controls execution:
|
|
56
|
+
- fast block executes every dt (Ts = dt)
|
|
57
|
+
- slow block executes every 2*dt (Ts = 2*dt)
|
|
58
|
+
|
|
59
|
+
We log the slow state at every global tick; it must change only on its activations.
|
|
60
|
+
"""
|
|
61
|
+
dt = 0.01
|
|
62
|
+
T = 0.04 # logs at t = 0, 0.01, 0.02, 0.03, 0.04
|
|
63
|
+
|
|
64
|
+
m = Model(name="multirate_test")
|
|
65
|
+
m.add_block(RateCounter("fast", sample_time=dt))
|
|
66
|
+
m.add_block(RateCounter("slow", sample_time=2 * dt))
|
|
67
|
+
|
|
68
|
+
cfg = SimulationConfig(
|
|
69
|
+
dt=dt,
|
|
70
|
+
T=T,
|
|
71
|
+
t0=0.0,
|
|
72
|
+
solver="fixed",
|
|
73
|
+
logging=["slow.outputs.y", "fast.outputs.y"],
|
|
74
|
+
)
|
|
75
|
+
sim = Simulator(model=m, sim_cfg=cfg, verbose=False)
|
|
76
|
+
logs = sim.run()
|
|
77
|
+
|
|
78
|
+
slow_count = np.array(logs["slow.outputs.y"]).flatten()
|
|
79
|
+
fast_count = np.array(logs["fast.outputs.y"]).flatten()
|
|
80
|
+
|
|
81
|
+
# # # fast executes every tick -> after each step, count increases by 1
|
|
82
|
+
assert np.allclose(fast_count, np.arange(0, int(round(T/dt))+1))
|
|
83
|
+
|
|
84
|
+
# # slow executes at t = 0, 0.02, 0.04 -> state after commit should be:
|
|
85
|
+
# # t=0: 1, t=0.01: 1, t=0.02: 2, t=0.03: 2, t=0.04: 3
|
|
86
|
+
expected = np.array([0, 0, 1, 1, 2], dtype=float)
|
|
87
|
+
assert len(slow_count) == len(expected)
|
|
88
|
+
assert np.allclose(slow_count, expected)
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from pySimBlocks.core.block import Block
|
|
5
|
+
from pySimBlocks.core.model import Model
|
|
6
|
+
from pySimBlocks.core.config import SimulationConfig
|
|
7
|
+
from pySimBlocks.core.simulator import Simulator
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SourceSetsOne(Block):
|
|
11
|
+
"""
|
|
12
|
+
Test source that sets output to 0 at initialize and to 1 at each output_update.
|
|
13
|
+
This is used to validate *immediate propagation* within Phase 1.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def initialize(self, t0: float):
|
|
17
|
+
self.outputs["y"] = np.array([[0.0]])
|
|
18
|
+
|
|
19
|
+
def output_update(self, t: float, dt: float):
|
|
20
|
+
self.outputs["y"] = np.array([[1.0]])
|
|
21
|
+
|
|
22
|
+
def state_update(self, t: float, dt: float):
|
|
23
|
+
# Stateless
|
|
24
|
+
self.next_state = self.state
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class PassThrough(Block):
|
|
28
|
+
"""
|
|
29
|
+
Test operator that forwards input u to output y.
|
|
30
|
+
Raises if u is missing, to make propagation expectations explicit.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def initialize(self, t0: float):
|
|
34
|
+
self.outputs["y"] = np.array([[0.0]])
|
|
35
|
+
|
|
36
|
+
def output_update(self, t: float, dt: float):
|
|
37
|
+
if "u" not in self.inputs:
|
|
38
|
+
raise RuntimeError(f"[{self.name}] missing input 'u'")
|
|
39
|
+
self.outputs["y"] = np.array(self.inputs["u"])
|
|
40
|
+
|
|
41
|
+
def state_update(self, t: float, dt: float):
|
|
42
|
+
self.next_state = self.state
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class StatefulCounter(Block):
|
|
46
|
+
"""
|
|
47
|
+
Test stateful block:
|
|
48
|
+
- output y[k] = x[k]
|
|
49
|
+
- next state x[k+1] = x[k] + 1
|
|
50
|
+
|
|
51
|
+
Used to validate two-phase update semantics (outputs computed before state commit).
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def initialize(self, t0: float):
|
|
55
|
+
self.state["x"] = np.array([[0.0]])
|
|
56
|
+
self.outputs["y"] = np.array([[0.0]])
|
|
57
|
+
|
|
58
|
+
def output_update(self, t: float, dt: float):
|
|
59
|
+
self.outputs["y"] = np.array(self.state["x"])
|
|
60
|
+
|
|
61
|
+
def state_update(self, t: float, dt: float):
|
|
62
|
+
self.next_state["x"] = self.state["x"] + 1.0
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _run(model: Model, dt: float, T: float, logging: list[str]):
|
|
66
|
+
cfg = SimulationConfig(dt=dt, T=T, t0=0.0, solver="fixed", logging=logging)
|
|
67
|
+
sim = Simulator(model=model, sim_cfg=cfg, verbose=False)
|
|
68
|
+
sim.run() # uses cfg.logging by default
|
|
69
|
+
return sim.logs
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def test_phase1_immediate_propagation(capsys):
|
|
73
|
+
"""
|
|
74
|
+
Validates that outputs are propagated immediately after each output_update,
|
|
75
|
+
not only at the end of Phase 1.
|
|
76
|
+
|
|
77
|
+
Setup:
|
|
78
|
+
src: outputs y=0 at initialize, y=1 at output_update
|
|
79
|
+
dst: pass-through (y = u)
|
|
80
|
+
connection: src.y -> dst.u
|
|
81
|
+
|
|
82
|
+
Expectation:
|
|
83
|
+
At the first tick (t=0), dst must see u=1 (the updated output), not u=0.
|
|
84
|
+
"""
|
|
85
|
+
m = Model(name="propagation_test")
|
|
86
|
+
m.add_block(SourceSetsOne("src"))
|
|
87
|
+
m.add_block(PassThrough("dst"))
|
|
88
|
+
m.connect("src", "y", "dst", "u")
|
|
89
|
+
|
|
90
|
+
logs = _run(
|
|
91
|
+
model=m,
|
|
92
|
+
dt=0.01,
|
|
93
|
+
T=0.01,
|
|
94
|
+
logging=["dst.outputs.y", "src.outputs.y"],
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# First logged sample corresponds to t_step = 0.0
|
|
98
|
+
dst_y0 = float(logs["dst.outputs.y"][0][0, 0])
|
|
99
|
+
src_y0 = float(logs["src.outputs.y"][0][0, 0])
|
|
100
|
+
|
|
101
|
+
assert src_y0 == 1.0
|
|
102
|
+
assert dst_y0 == 1.0
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_two_phase_outputs_vs_state_commit(capsys):
|
|
106
|
+
"""
|
|
107
|
+
Validates the strict two-phase semantics:
|
|
108
|
+
- outputs at time t are computed from x[k]
|
|
109
|
+
- state is committed to x[k+1] at end of step
|
|
110
|
+
|
|
111
|
+
We log, per tick:
|
|
112
|
+
- c.outputs.y (expected: x[k])
|
|
113
|
+
- c.state.x (expected: x[k+1])
|
|
114
|
+
|
|
115
|
+
Therefore, at each logged time:
|
|
116
|
+
state_x == output_y + 1
|
|
117
|
+
"""
|
|
118
|
+
m = Model(name="two_phase_test")
|
|
119
|
+
m.add_block(StatefulCounter("c"))
|
|
120
|
+
|
|
121
|
+
logs = _run(
|
|
122
|
+
model=m,
|
|
123
|
+
dt=0.01,
|
|
124
|
+
T=0.04, # gives samples at t = 0, 0.01, 0.02, 0.03, 0.04
|
|
125
|
+
logging=["c.outputs.y", "c.state.x"],
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
y = np.array(logs["c.outputs.y"]).flatten()
|
|
129
|
+
x = np.array(logs["c.state.x"]).flatten()
|
|
130
|
+
|
|
131
|
+
assert len(y) == len(x) >= 3
|
|
132
|
+
assert np.allclose(x, y + 1.0)
|