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,182 @@
|
|
|
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.gain import Gain
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ------------------------------------------------------------
|
|
10
|
+
# Helper : run a tiny simulation and return last output sample
|
|
11
|
+
# ------------------------------------------------------------
|
|
12
|
+
def run_sim(const_value, gain_block, dt=0.1):
|
|
13
|
+
m = Model()
|
|
14
|
+
|
|
15
|
+
src = Constant("src", const_value)
|
|
16
|
+
m.add_block(src)
|
|
17
|
+
m.add_block(gain_block)
|
|
18
|
+
|
|
19
|
+
m.connect("src", "out", gain_block.name, "in")
|
|
20
|
+
|
|
21
|
+
sim_cfg = SimulationConfig(dt, dt, logging=[f"{gain_block.name}.outputs.out"])
|
|
22
|
+
sim = Simulator(m, sim_cfg)
|
|
23
|
+
logs = sim.run()
|
|
24
|
+
return logs[f"{gain_block.name}.outputs.out"][-1]
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ------------------------------------------------------------
|
|
28
|
+
# 1) Element-wise mode (default)
|
|
29
|
+
# ------------------------------------------------------------
|
|
30
|
+
def test_gain_elementwise_scalar():
|
|
31
|
+
out = run_sim([[2.0]], Gain("G", gain=3.0))
|
|
32
|
+
assert np.allclose(out, [[6.0]])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_gain_elementwise_vector_row_scaling():
|
|
36
|
+
# gain vector scales rows: y = [2,3]^T * u
|
|
37
|
+
# u must have shape (2, ncols)
|
|
38
|
+
out = run_sim([[1.0], [1.0]], Gain("G", gain=[2.0, 3.0]))
|
|
39
|
+
assert np.allclose(out, [[2.0], [3.0]])
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def test_gain_elementwise_vector_bad_u_rows():
|
|
43
|
+
g = Gain("G", gain=[1.0, 2.0]) # len=2, expects u.shape[0]==2
|
|
44
|
+
|
|
45
|
+
m = Model()
|
|
46
|
+
src = Constant("src", [[1.0], [2.0], [3.0]]) # shape (3,1) incompatible
|
|
47
|
+
m.add_block(src)
|
|
48
|
+
m.add_block(g)
|
|
49
|
+
m.connect("src", "out", "G", "in")
|
|
50
|
+
|
|
51
|
+
sim_cfg = SimulationConfig(0.1, 0.1)
|
|
52
|
+
sim = Simulator(m, sim_cfg)
|
|
53
|
+
with pytest.raises(RuntimeError) as err:
|
|
54
|
+
sim.initialize()
|
|
55
|
+
|
|
56
|
+
# Simulator wraps the exception message during initialization
|
|
57
|
+
assert "Element-wise mode requires u.shape[0] == len(gain)" in str(err.value)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_gain_elementwise_matrix_same_shape():
|
|
61
|
+
K = np.array([[2.0, 0.0],
|
|
62
|
+
[1.0, -1.0]])
|
|
63
|
+
u = np.array([[3.0, 4.0],
|
|
64
|
+
[1.0, 2.0]])
|
|
65
|
+
out = run_sim(u, Gain("G", gain=K))
|
|
66
|
+
assert np.allclose(out, K * u)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_gain_elementwise_matrix_bad_shape():
|
|
70
|
+
K = np.zeros((2, 2))
|
|
71
|
+
g = Gain("G", gain=K)
|
|
72
|
+
|
|
73
|
+
m = Model()
|
|
74
|
+
src = Constant("src", np.zeros((2, 3))) # mismatch
|
|
75
|
+
m.add_block(src)
|
|
76
|
+
m.add_block(g)
|
|
77
|
+
m.connect("src", "out", "G", "in")
|
|
78
|
+
|
|
79
|
+
sim_cfg = SimulationConfig(0.1, 0.1)
|
|
80
|
+
sim = Simulator(m, sim_cfg)
|
|
81
|
+
with pytest.raises(RuntimeError) as err:
|
|
82
|
+
sim.initialize()
|
|
83
|
+
|
|
84
|
+
assert "Element-wise mode with matrix gain requires u.shape == gain.shape" in str(err.value)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# ------------------------------------------------------------
|
|
88
|
+
# 2) Matrix (K @ u)
|
|
89
|
+
# ------------------------------------------------------------
|
|
90
|
+
def test_gain_left_matrix_product():
|
|
91
|
+
K = [[2.0, 0.0],
|
|
92
|
+
[1.0, -1.0]]
|
|
93
|
+
G = Gain("G", gain=K, multiplication="Matrix (K @ u)")
|
|
94
|
+
|
|
95
|
+
out = run_sim([[3.0], [1.0]], G)
|
|
96
|
+
# Expected: [[2*3 + 0*1], [1*3 + -1*1]] = [[6], [2]]
|
|
97
|
+
assert np.allclose(out, [[6.0], [2.0]])
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_gain_left_matrix_bad_dimensions():
|
|
101
|
+
K = [[1.0, 2.0]] # shape (1,2)
|
|
102
|
+
g = Gain("G", gain=K, multiplication="Matrix (K @ u)")
|
|
103
|
+
|
|
104
|
+
m = Model()
|
|
105
|
+
src = Constant("src", [[1.0], [2.0], [3.0]]) # shape (3,1), incompatible
|
|
106
|
+
m.add_block(src)
|
|
107
|
+
m.add_block(g)
|
|
108
|
+
m.connect("src", "out", "G", "in")
|
|
109
|
+
|
|
110
|
+
sim_cfg = SimulationConfig(0.1, 0.1)
|
|
111
|
+
sim = Simulator(m, sim_cfg)
|
|
112
|
+
with pytest.raises(RuntimeError) as err:
|
|
113
|
+
sim.initialize()
|
|
114
|
+
|
|
115
|
+
assert "Left matrix product requires u.shape[0] == gain.shape[1]" in str(err.value)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def test_gain_left_matrix_requires_matrix_gain():
|
|
119
|
+
g = Gain("G", gain=2.0, multiplication="Matrix (K @ u)") # scalar gain not allowed here
|
|
120
|
+
m = Model()
|
|
121
|
+
src = Constant("src", [[1.0]])
|
|
122
|
+
m.add_block(src)
|
|
123
|
+
m.add_block(g)
|
|
124
|
+
m.connect("src", "out", "G", "in")
|
|
125
|
+
|
|
126
|
+
sim_cfg = SimulationConfig(0.1, 0.1)
|
|
127
|
+
sim = Simulator(m, sim_cfg)
|
|
128
|
+
with pytest.raises(RuntimeError) as err:
|
|
129
|
+
sim.initialize()
|
|
130
|
+
|
|
131
|
+
assert "requires a 2D matrix gain" in str(err.value)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ------------------------------------------------------------
|
|
135
|
+
# 3) Matrix (u @ K)
|
|
136
|
+
# ------------------------------------------------------------
|
|
137
|
+
def test_gain_right_matrix_product():
|
|
138
|
+
# u shape (1,2), K shape (2,3) => out shape (1,3)
|
|
139
|
+
K = np.array([[1.0, 2.0, 3.0],
|
|
140
|
+
[4.0, 5.0, 6.0]])
|
|
141
|
+
G = Gain("G", gain=K, multiplication="Matrix (u @ K)")
|
|
142
|
+
|
|
143
|
+
u = np.array([[10.0, 1.0]])
|
|
144
|
+
out = run_sim(u, G)
|
|
145
|
+
expected = (u @ K).T
|
|
146
|
+
assert np.allclose(out, expected)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def test_gain_right_matrix_bad_dimensions():
|
|
150
|
+
K = np.zeros((2, 3))
|
|
151
|
+
g = Gain("G", gain=K, multiplication="Matrix (u @ K)")
|
|
152
|
+
|
|
153
|
+
m = Model()
|
|
154
|
+
src = Constant("src", np.zeros((2, 4))) # u.shape[1]=4, expected 2
|
|
155
|
+
m.add_block(src)
|
|
156
|
+
m.add_block(g)
|
|
157
|
+
m.connect("src", "out", "G", "in")
|
|
158
|
+
|
|
159
|
+
sim_cfg = SimulationConfig(0.1, 0.1)
|
|
160
|
+
sim = Simulator(m, sim_cfg)
|
|
161
|
+
with pytest.raises(RuntimeError) as err:
|
|
162
|
+
sim.initialize()
|
|
163
|
+
|
|
164
|
+
assert "Right matrix product requires u.shape[1] == gain.shape[0]" in str(err.value)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
# ------------------------------------------------------------
|
|
168
|
+
# 4) Initialization sanity
|
|
169
|
+
# ------------------------------------------------------------
|
|
170
|
+
def test_initialization_output_elementwise_default():
|
|
171
|
+
G = Gain("G", gain=2.0) # default: Element wise
|
|
172
|
+
m = Model()
|
|
173
|
+
src = Constant("src", [[1.5]])
|
|
174
|
+
m.add_block(src)
|
|
175
|
+
m.add_block(G)
|
|
176
|
+
m.connect("src", "out", "G", "in")
|
|
177
|
+
|
|
178
|
+
sim_cfg = SimulationConfig(0.1, 0.1)
|
|
179
|
+
sim = Simulator(m, sim_cfg)
|
|
180
|
+
sim.initialize(0.0)
|
|
181
|
+
|
|
182
|
+
assert np.allclose(G.outputs["out"], [[3.0]])
|
|
@@ -0,0 +1,101 @@
|
|
|
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.mux import Mux
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# ------------------------------------------------------------
|
|
10
|
+
def run_sim(values, mux):
|
|
11
|
+
m = Model()
|
|
12
|
+
|
|
13
|
+
const_blocks = []
|
|
14
|
+
for i, val in enumerate(values):
|
|
15
|
+
c = Constant(f"c{i+1}", val)
|
|
16
|
+
m.add_block(c)
|
|
17
|
+
const_blocks.append(c)
|
|
18
|
+
|
|
19
|
+
m.add_block(mux)
|
|
20
|
+
for i, c in enumerate(const_blocks):
|
|
21
|
+
m.connect(c.name, "out", mux.name, f"in{i+1}")
|
|
22
|
+
|
|
23
|
+
sim_cfg = SimulationConfig(0.1, 0.1, logging=[f"{mux.name}.outputs.out"])
|
|
24
|
+
sim = Simulator(m, sim_cfg)
|
|
25
|
+
logs = sim.run()
|
|
26
|
+
return logs[f"{mux.name}.outputs.out"][-1]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------
|
|
30
|
+
def test_mux_basic_concatenation():
|
|
31
|
+
mux = Mux("M", num_inputs=2)
|
|
32
|
+
out = run_sim([[[1.0]], [[2.0]]], mux)
|
|
33
|
+
assert np.allclose(out, [[1.0], [2.0]])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# ------------------------------------------------------------
|
|
37
|
+
def test_mux_multi_dimensional_inputs():
|
|
38
|
+
mux = Mux("M", num_inputs=3)
|
|
39
|
+
out = run_sim(
|
|
40
|
+
[
|
|
41
|
+
[[1.0], [2.0]], # (2,1)
|
|
42
|
+
[[3.0]], # (1,1)
|
|
43
|
+
[[4.0], [5.0], [6.0]], # (3,1)
|
|
44
|
+
],
|
|
45
|
+
mux,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
expected = np.array([[1.0], [2.0], [3.0], [4.0], [5.0], [6.0]])
|
|
49
|
+
assert np.allclose(out, expected)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
# ------------------------------------------------------------
|
|
53
|
+
def test_mux_scalar_inputs_allowed():
|
|
54
|
+
mux = Mux("M", num_inputs=3)
|
|
55
|
+
out = run_sim([1.0, 2.0, 3.0], mux) # scalars -> (1,1) each
|
|
56
|
+
expected = np.array([[1.0], [2.0], [3.0]])
|
|
57
|
+
assert np.allclose(out, expected)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ------------------------------------------------------------
|
|
61
|
+
def test_mux_missing_input_raises():
|
|
62
|
+
m = Model()
|
|
63
|
+
c1 = Constant("c1", [[1.0]])
|
|
64
|
+
mux = Mux("M", num_inputs=2)
|
|
65
|
+
|
|
66
|
+
m.add_block(c1)
|
|
67
|
+
m.add_block(mux)
|
|
68
|
+
|
|
69
|
+
m.connect("c1", "out", "M", "in1")
|
|
70
|
+
# in2 not connected
|
|
71
|
+
|
|
72
|
+
sim_cfg = SimulationConfig(0.1, 0.1)
|
|
73
|
+
sim = Simulator(m, sim_cfg)
|
|
74
|
+
|
|
75
|
+
with pytest.raises(RuntimeError) as err:
|
|
76
|
+
sim.run(T=0.1)
|
|
77
|
+
|
|
78
|
+
assert "not connected or not set" in str(err.value)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------
|
|
82
|
+
def test_mux_invalid_shape_rejected():
|
|
83
|
+
# in2 is a matrix (2,2) -> must raise
|
|
84
|
+
m = Model()
|
|
85
|
+
c1 = Constant("c1", [[1.0]]) # OK
|
|
86
|
+
c2 = Constant("c2", [[1.0, 2.0], [3.0, 4]])
|
|
87
|
+
|
|
88
|
+
mux = Mux("M", num_inputs=2)
|
|
89
|
+
m.add_block(c1)
|
|
90
|
+
m.add_block(c2)
|
|
91
|
+
m.add_block(mux)
|
|
92
|
+
|
|
93
|
+
m.connect("c1", "out", "M", "in1")
|
|
94
|
+
m.connect("c2", "out", "M", "in2")
|
|
95
|
+
|
|
96
|
+
sim_cfg = SimulationConfig(0.1, 0.1)
|
|
97
|
+
sim = Simulator(m, sim_cfg)
|
|
98
|
+
with pytest.raises(RuntimeError) as err:
|
|
99
|
+
sim.run(T=0.1)
|
|
100
|
+
|
|
101
|
+
assert "must be a column vector" in str(err.value)
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from pySimBlocks.core.model import Model
|
|
5
|
+
from pySimBlocks.core.simulator import Simulator
|
|
6
|
+
from pySimBlocks.core.config import SimulationConfig
|
|
7
|
+
from pySimBlocks.blocks.sources.constant import Constant
|
|
8
|
+
from pySimBlocks.blocks.operators.product import Product
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ------------------------------------------------------------
|
|
12
|
+
# Helper
|
|
13
|
+
# ------------------------------------------------------------
|
|
14
|
+
def run_two_inputs(v1, v2, operations="*", multiplication="Element-wise (*)", dt=0.1):
|
|
15
|
+
m = Model()
|
|
16
|
+
|
|
17
|
+
s1 = Constant("s1", v1)
|
|
18
|
+
s2 = Constant("s2", v2)
|
|
19
|
+
|
|
20
|
+
m.add_block(s1)
|
|
21
|
+
m.add_block(s2)
|
|
22
|
+
|
|
23
|
+
pr = Product("P", operations=operations, multiplication=multiplication)
|
|
24
|
+
m.add_block(pr)
|
|
25
|
+
|
|
26
|
+
m.connect("s1", "out", "P", "in1")
|
|
27
|
+
m.connect("s2", "out", "P", "in2")
|
|
28
|
+
|
|
29
|
+
sim_cfg = SimulationConfig(dt, dt, logging=["P.outputs.out"])
|
|
30
|
+
sim = Simulator(m, sim_cfg)
|
|
31
|
+
logs = sim.run()
|
|
32
|
+
return logs["P.outputs.out"][-1]
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ------------------------------------------------------------
|
|
36
|
+
# 1) Basic product: u1 * u2
|
|
37
|
+
# ------------------------------------------------------------
|
|
38
|
+
def test_product_basic():
|
|
39
|
+
out = run_two_inputs([[2.0]], [[3.0]])
|
|
40
|
+
assert np.allclose(out, [[6.0]])
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ------------------------------------------------------------
|
|
44
|
+
# 2) Division: u1 / u2
|
|
45
|
+
# ------------------------------------------------------------
|
|
46
|
+
def test_product_division():
|
|
47
|
+
out = run_two_inputs([[6.0]], [[3.0]], operations="/")
|
|
48
|
+
assert np.allclose(out, [[2.0]])
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# ------------------------------------------------------------
|
|
52
|
+
# 3) Scalar × vector (broadcast scalar (1,1) only)
|
|
53
|
+
# ------------------------------------------------------------
|
|
54
|
+
def test_product_scalar_vector():
|
|
55
|
+
out = run_two_inputs([[2.0]], [[1.0], [3.0], [4.0]])
|
|
56
|
+
assert np.allclose(out, [[2.0], [6.0], [8.0]])
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
# ------------------------------------------------------------
|
|
60
|
+
# 4) Vector × vector (same shape)
|
|
61
|
+
# ------------------------------------------------------------
|
|
62
|
+
def test_product_vector_vector():
|
|
63
|
+
out = run_two_inputs([[2.0], [3.0]], [[4.0], [5.0]])
|
|
64
|
+
assert np.allclose(out, [[8.0], [15.0]])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ------------------------------------------------------------
|
|
68
|
+
# 5) Matrix element-wise product (same shape)
|
|
69
|
+
# ------------------------------------------------------------
|
|
70
|
+
def test_product_matrix_elementwise():
|
|
71
|
+
A = [[1.0, 2.0], [3.0, 4.0]]
|
|
72
|
+
B = [[2.0, 0.5], [1.0, 2.0]]
|
|
73
|
+
out = run_two_inputs(A, B, multiplication="Element-wise (*)")
|
|
74
|
+
assert np.allclose(out, np.array(A) * np.array(B))
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# ------------------------------------------------------------
|
|
78
|
+
# 6) Scalar × matrix (element-wise scalar broadcast)
|
|
79
|
+
# ------------------------------------------------------------
|
|
80
|
+
def test_product_scalar_matrix_elementwise():
|
|
81
|
+
out = run_two_inputs([[2.0]], [[1.0, 2.0], [3.0, 4.0]], multiplication="Element-wise (*)")
|
|
82
|
+
assert np.allclose(out, 2.0 * np.array([[1.0, 2.0], [3.0, 4.0]]))
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ------------------------------------------------------------
|
|
86
|
+
# 7) Incompatible shapes in element-wise must raise
|
|
87
|
+
# ------------------------------------------------------------
|
|
88
|
+
def test_product_shape_mismatch_elementwise():
|
|
89
|
+
m = Model()
|
|
90
|
+
|
|
91
|
+
s1 = Constant("s1", [[1.0], [2.0]]) # (2,1)
|
|
92
|
+
s2 = Constant("s2", [[3.0, 4.0, 5.0]]) # (1,3) -> mismatch
|
|
93
|
+
pr = Product("P", operations="*", multiplication="Element-wise (*)")
|
|
94
|
+
|
|
95
|
+
m.add_block(s1)
|
|
96
|
+
m.add_block(s2)
|
|
97
|
+
m.add_block(pr)
|
|
98
|
+
|
|
99
|
+
m.connect("s1", "out", "P", "in1")
|
|
100
|
+
m.connect("s2", "out", "P", "in2")
|
|
101
|
+
|
|
102
|
+
sim_cfg = SimulationConfig(0.1, 0.1, logging=["P.outputs.out"])
|
|
103
|
+
sim = Simulator(m, sim_cfg)
|
|
104
|
+
|
|
105
|
+
with pytest.raises(RuntimeError) as err:
|
|
106
|
+
sim.run(T=0.1)
|
|
107
|
+
|
|
108
|
+
assert "incompatible input shapes" in str(err.value).lower()
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
# ------------------------------------------------------------
|
|
112
|
+
# 8) Matrix multiplication (@) basic
|
|
113
|
+
# ------------------------------------------------------------
|
|
114
|
+
def test_product_matrix_multiplication():
|
|
115
|
+
A = [[1.0, 2.0],
|
|
116
|
+
[3.0, 4.0]] # (2,2)
|
|
117
|
+
B = [[5.0],
|
|
118
|
+
[6.0]] # (2,1)
|
|
119
|
+
|
|
120
|
+
out = run_two_inputs(A, B, operations="*", multiplication="Matrix (@)")
|
|
121
|
+
assert np.allclose(out, np.array(A) @ np.array(B))
|
|
122
|
+
assert out.shape == (2, 1)
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
# ------------------------------------------------------------
|
|
126
|
+
# 9) Matrix multiplication rejects division
|
|
127
|
+
# ------------------------------------------------------------
|
|
128
|
+
def test_product_matrix_mode_rejects_division():
|
|
129
|
+
with pytest.raises(ValueError):
|
|
130
|
+
Product("P", operations="/", multiplication="Matrix (@)")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
# ------------------------------------------------------------
|
|
134
|
+
# 10) Invalid operations string
|
|
135
|
+
# ------------------------------------------------------------
|
|
136
|
+
def test_product_invalid_operations():
|
|
137
|
+
with pytest.raises(ValueError):
|
|
138
|
+
Product("P", operations="*+")
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ------------------------------------------------------------
|
|
142
|
+
# 11) Inferred num_inputs from operations
|
|
143
|
+
# ------------------------------------------------------------
|
|
144
|
+
def test_product_infer_num_inputs_from_operations():
|
|
145
|
+
pr = Product("P", operations="*/")
|
|
146
|
+
assert pr.num_inputs == 3
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------
|
|
150
|
+
# 12) Check output initialization
|
|
151
|
+
# ------------------------------------------------------------
|
|
152
|
+
def test_product_initial_output():
|
|
153
|
+
m = Model()
|
|
154
|
+
|
|
155
|
+
s1 = Constant("s1", [[2.0]])
|
|
156
|
+
s2 = Constant("s2", [[3.0]])
|
|
157
|
+
|
|
158
|
+
pr = Product("P", operations="*", multiplication="Element-wise (*)")
|
|
159
|
+
|
|
160
|
+
m.add_block(s1)
|
|
161
|
+
m.add_block(s2)
|
|
162
|
+
m.add_block(pr)
|
|
163
|
+
|
|
164
|
+
m.connect("s1", "out", "P", "in1")
|
|
165
|
+
m.connect("s2", "out", "P", "in2")
|
|
166
|
+
|
|
167
|
+
sim_cfg = SimulationConfig(0.1, 0.1, logging=["P.outputs.out"])
|
|
168
|
+
sim = Simulator(m, sim_cfg)
|
|
169
|
+
sim.initialize(0.0)
|
|
170
|
+
|
|
171
|
+
assert np.allclose(pr.outputs["out"], [[6.0]])
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pytest
|
|
3
|
+
|
|
4
|
+
from pySimBlocks.core import Model, Simulator, SimulationConfig
|
|
5
|
+
from pySimBlocks.blocks.sources.step import Step
|
|
6
|
+
from pySimBlocks.blocks.sources.constant import Constant
|
|
7
|
+
from pySimBlocks.blocks.sources.ramp import Ramp
|
|
8
|
+
from pySimBlocks.blocks.operators.rate_limiter import RateLimiter
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
# ------------------------------------------------------------
|
|
12
|
+
def run_sim(src_block, rate_block, dt=0.1, T=0.3):
|
|
13
|
+
m = Model()
|
|
14
|
+
m.add_block(src_block)
|
|
15
|
+
m.add_block(rate_block)
|
|
16
|
+
m.connect(src_block.name, "out", rate_block.name, "in")
|
|
17
|
+
|
|
18
|
+
sim_cfg = SimulationConfig(dt, T, logging=[f"{rate_block.name}.outputs.out"])
|
|
19
|
+
sim = Simulator(m, sim_cfg)
|
|
20
|
+
logs = sim.run()
|
|
21
|
+
return logs[f"{rate_block.name}.outputs.out"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# ------------------------------------------------------------
|
|
25
|
+
def test_rate_limiter_passthrough_without_initial_output():
|
|
26
|
+
src = Step("src", start_time=0.0, value_before=0.0, value_after=10.0)
|
|
27
|
+
R = RateLimiter("R", rising_slope=1.0, falling_slope=-1.0)
|
|
28
|
+
|
|
29
|
+
logs = run_sim(src, R, dt=0.1, T=0.2)
|
|
30
|
+
|
|
31
|
+
assert np.allclose(logs[0], [[10.0]])
|
|
32
|
+
assert np.allclose(logs[1], [[10.0]])
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
# ------------------------------------------------------------
|
|
36
|
+
def test_rate_limiter_scalar_with_initial_output():
|
|
37
|
+
src = Constant("src", 10.0)
|
|
38
|
+
R = RateLimiter("R", rising_slope=1.0, falling_slope=-1.0, initial_output=0.0)
|
|
39
|
+
|
|
40
|
+
logs = run_sim(src, R, dt=0.1, T=0.3)
|
|
41
|
+
|
|
42
|
+
assert np.allclose(logs[0], [[0.1]])
|
|
43
|
+
assert np.allclose(logs[1], [[0.2]])
|
|
44
|
+
assert np.allclose(logs[2], [[0.3]])
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
# ------------------------------------------------------------
|
|
48
|
+
def test_rate_limiter_no_active_limitation():
|
|
49
|
+
src = Ramp("src", slope=0.5, offset=1.0)
|
|
50
|
+
R = RateLimiter("R", rising_slope=5.0, falling_slope=-5.0)
|
|
51
|
+
|
|
52
|
+
logs = run_sim(src, R, dt=0.1, T=0.3)
|
|
53
|
+
|
|
54
|
+
for k in range(1, len(logs)):
|
|
55
|
+
assert np.allclose(logs[k] - logs[k - 1], [[0.05]])
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# ------------------------------------------------------------
|
|
59
|
+
def test_rate_limiter_vector():
|
|
60
|
+
src = Step(
|
|
61
|
+
"src",
|
|
62
|
+
start_time=0.0,
|
|
63
|
+
value_before=[[0.0], [0.0]],
|
|
64
|
+
value_after=[[4.0], [-4.0]],
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
R = RateLimiter(
|
|
68
|
+
"R",
|
|
69
|
+
rising_slope=[1.0, 0.5],
|
|
70
|
+
falling_slope=[-1.0, -0.5],
|
|
71
|
+
initial_output=[[0.0], [0.0]],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
logs = run_sim(src, R, dt=0.1, T=0.2)
|
|
75
|
+
|
|
76
|
+
assert np.allclose(logs[0], [[0.1], [-0.05]])
|
|
77
|
+
assert np.allclose(logs[1], [[0.2], [-0.1]])
|
|
78
|
+
assert np.allclose(logs[2], [[0.3], [-0.15]])
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# ------------------------------------------------------------
|
|
82
|
+
def test_rate_limiter_matrix_scalar_slopes():
|
|
83
|
+
src = Step(
|
|
84
|
+
"src",
|
|
85
|
+
start_time=0.0,
|
|
86
|
+
value_before=[[0.0, 0.0],
|
|
87
|
+
[0.0, 0.0]],
|
|
88
|
+
value_after=[[10.0, -10.0],
|
|
89
|
+
[5.0, -5.0]],
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
R = RateLimiter("R", rising_slope=1.0, falling_slope=-1.0, initial_output=0.0)
|
|
93
|
+
|
|
94
|
+
logs = run_sim(src, R, dt=0.1, T=0.1)
|
|
95
|
+
|
|
96
|
+
# dt=0.1, slopes +/-1 => max delta per step = +/-0.1 everywhere
|
|
97
|
+
expected = np.array([[0.1, -0.1],
|
|
98
|
+
[0.1, -0.1]])
|
|
99
|
+
assert np.allclose(logs[0], expected)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# ------------------------------------------------------------
|
|
103
|
+
def test_rate_limiter_matrix_vector_slopes_broadcast_columns():
|
|
104
|
+
# input is (2,3)
|
|
105
|
+
src = Step(
|
|
106
|
+
"src",
|
|
107
|
+
start_time=0.0,
|
|
108
|
+
value_before=[[0.0, 0.0, 0.0],
|
|
109
|
+
[0.0, 0.0, 0.0]],
|
|
110
|
+
value_after=[[10.0, -10.0, 2.0],
|
|
111
|
+
[5.0, 6.0, -7.0]],
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
# slopes as (2,1) via 1D -> (2,1); should broadcast across 3 columns
|
|
115
|
+
R = RateLimiter(
|
|
116
|
+
"R",
|
|
117
|
+
rising_slope=[1.0, 0.5],
|
|
118
|
+
falling_slope=[-1.0, -0.5],
|
|
119
|
+
initial_output=0.0,
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
logs = run_sim(src, R, dt=0.1, T=0.1)
|
|
123
|
+
|
|
124
|
+
# row1 limited to +/-0.1, row2 limited to +/-0.05
|
|
125
|
+
expected = np.array([[0.1, -0.1, 0.1],
|
|
126
|
+
[0.05, 0.05, -0.05]])
|
|
127
|
+
assert np.allclose(logs[0], expected)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
# ------------------------------------------------------------
|
|
131
|
+
def test_rate_limiter_missing_input():
|
|
132
|
+
R = RateLimiter("R", rising_slope=1.0, falling_slope=-1.0)
|
|
133
|
+
m = Model()
|
|
134
|
+
m.add_block(R)
|
|
135
|
+
|
|
136
|
+
sim_cfg = SimulationConfig(0.1, 0.1)
|
|
137
|
+
sim = Simulator(m, sim_cfg)
|
|
138
|
+
|
|
139
|
+
with pytest.raises(RuntimeError):
|
|
140
|
+
sim.initialize()
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# ------------------------------------------------------------
|
|
144
|
+
def test_rate_limiter_invalid_slopes():
|
|
145
|
+
with pytest.raises(ValueError):
|
|
146
|
+
RateLimiter("R", rising_slope=-1.0, falling_slope=-1.0)
|
|
147
|
+
|
|
148
|
+
with pytest.raises(ValueError):
|
|
149
|
+
RateLimiter("R", rising_slope=1.0, falling_slope=1.0)
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
# ------------------------------------------------------------
|
|
153
|
+
def test_rate_limiter_input_shape_change_raises():
|
|
154
|
+
# Direct block test: once initialized with (1,1), shape cannot change
|
|
155
|
+
R = RateLimiter("R", rising_slope=1.0, falling_slope=-1.0, initial_output=0.0)
|
|
156
|
+
|
|
157
|
+
R.inputs["in"] = np.array([[1.0]])
|
|
158
|
+
R.initialize(0.0)
|
|
159
|
+
R.output_update(0.0, 0.1)
|
|
160
|
+
|
|
161
|
+
R.inputs["in"] = np.array([[1.0, 2.0],
|
|
162
|
+
[3.0, 4.0]])
|
|
163
|
+
with pytest.raises(ValueError) as err:
|
|
164
|
+
R.output_update(0.1, 0.1)
|
|
165
|
+
|
|
166
|
+
assert "shape changed" in str(err.value) or "shape" in str(err.value)
|