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,237 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from numpy.typing import ArrayLike
|
|
23
|
+
|
|
24
|
+
from pySimBlocks.core.block import Block
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DiscreteDerivator(Block):
|
|
28
|
+
"""
|
|
29
|
+
Discrete-time differentiator block.
|
|
30
|
+
|
|
31
|
+
Summary:
|
|
32
|
+
Estimates the discrete-time derivative of the input signal using a
|
|
33
|
+
backward finite difference:
|
|
34
|
+
y[k] = (u[k] - u[k-1]) / dt
|
|
35
|
+
|
|
36
|
+
Parameters:
|
|
37
|
+
initial_output : scalar or array-like, optional
|
|
38
|
+
Output used at the first execution step.
|
|
39
|
+
If provided, it also FIXES the signal shape permanently.
|
|
40
|
+
sample_time : float, optional
|
|
41
|
+
Block execution period.
|
|
42
|
+
|
|
43
|
+
Inputs:
|
|
44
|
+
in : array (m,n)
|
|
45
|
+
Input signal (must be 2D).
|
|
46
|
+
|
|
47
|
+
Outputs:
|
|
48
|
+
out : array (m,n)
|
|
49
|
+
Estimated discrete-time derivative.
|
|
50
|
+
|
|
51
|
+
Notes:
|
|
52
|
+
- Stateful block.
|
|
53
|
+
- Direct feedthrough.
|
|
54
|
+
- Shape is frozen as soon as known (initial_output or first input).
|
|
55
|
+
- No implicit vector reshape; matrices are supported.
|
|
56
|
+
- Policy:
|
|
57
|
+
- Never propagate None: output is always at least (1,1) zeros.
|
|
58
|
+
- Shape is unresolved while only scalar placeholder (1,1) is seen and no
|
|
59
|
+
explicit initial_output is provided.
|
|
60
|
+
- If initial_output is provided -> shape is frozen immediately (including (1,1)).
|
|
61
|
+
- If no initial_output -> shape is frozen as soon as a non-scalar input arrives.
|
|
62
|
+
- Once shape is frozen -> any non-scalar mismatch raises ValueError.
|
|
63
|
+
- If shape frozen to (m,n) and input is scalar (1,1) -> broadcast scalar to (m,n).
|
|
64
|
+
- When shape is frozen from a first non-scalar input, u_prev is initialized to
|
|
65
|
+
that input to avoid bogus derivative spikes.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
direct_feedthrough = True
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
name: str,
|
|
73
|
+
initial_output: ArrayLike | None = None,
|
|
74
|
+
sample_time: float | None = None,
|
|
75
|
+
):
|
|
76
|
+
super().__init__(name, sample_time)
|
|
77
|
+
|
|
78
|
+
self.inputs["in"] = None
|
|
79
|
+
self.outputs["out"] = None
|
|
80
|
+
|
|
81
|
+
self.state["u_prev"] = None
|
|
82
|
+
self.next_state["u_prev"] = None
|
|
83
|
+
|
|
84
|
+
self._resolved_shape: tuple[int, int] | None = None
|
|
85
|
+
self._first_output = True
|
|
86
|
+
|
|
87
|
+
# Never None: placeholder output at minimum
|
|
88
|
+
self._placeholder = np.zeros((1, 1), dtype=float)
|
|
89
|
+
|
|
90
|
+
self._initial_output_raw: np.ndarray | None = None
|
|
91
|
+
if initial_output is not None:
|
|
92
|
+
y0 = self._to_2d_array("initial_output", initial_output).astype(float)
|
|
93
|
+
self._initial_output_raw = y0.copy()
|
|
94
|
+
|
|
95
|
+
# initial_output freezes shape (including (1,1))
|
|
96
|
+
self._resolved_shape = y0.shape
|
|
97
|
+
self.outputs["out"] = y0.copy()
|
|
98
|
+
else:
|
|
99
|
+
self.outputs["out"] = self._placeholder.copy()
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
# --------------------------------------------------------------------------
|
|
103
|
+
# Public methods
|
|
104
|
+
# --------------------------------------------------------------------------
|
|
105
|
+
def initialize(self, t0: float) -> None:
|
|
106
|
+
"""
|
|
107
|
+
Never propagate None:
|
|
108
|
+
- output already set (initial_output or placeholder (1,1)).
|
|
109
|
+
- if input exists, we can set u_prev consistently.
|
|
110
|
+
- if input missing, keep u_prev=None (or already set if frozen on first non-scalar later).
|
|
111
|
+
"""
|
|
112
|
+
u = self.inputs["in"]
|
|
113
|
+
|
|
114
|
+
if u is None:
|
|
115
|
+
# Keep output as-is (initial_output or placeholder)
|
|
116
|
+
self.state["u_prev"] = None
|
|
117
|
+
self.next_state["u_prev"] = None
|
|
118
|
+
self._first_output = True
|
|
119
|
+
return
|
|
120
|
+
|
|
121
|
+
u_arr = self._normalize_input(u)
|
|
122
|
+
|
|
123
|
+
# If initial_output froze shape, _normalize_input enforces it.
|
|
124
|
+
# Store u_prev to support derivative at next step.
|
|
125
|
+
self.state["u_prev"] = u_arr.copy()
|
|
126
|
+
self.next_state["u_prev"] = u_arr.copy()
|
|
127
|
+
self._first_output = True
|
|
128
|
+
|
|
129
|
+
# -------------------------------------------------------
|
|
130
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
131
|
+
"""
|
|
132
|
+
First call policy:
|
|
133
|
+
- If initial_output provided: keep it for first output_update.
|
|
134
|
+
- Else: keep zero output for first call.
|
|
135
|
+
|
|
136
|
+
Afterwards:
|
|
137
|
+
y = (u - u_prev) / dt
|
|
138
|
+
"""
|
|
139
|
+
u_arr = self._normalize_input(self.inputs["in"])
|
|
140
|
+
|
|
141
|
+
if self._first_output:
|
|
142
|
+
self._first_output = False
|
|
143
|
+
# output already set (initial_output or placeholder); ensure shape if frozen
|
|
144
|
+
if self._resolved_shape is not None and self.outputs["out"] is not None:
|
|
145
|
+
y = np.asarray(self.outputs["out"], dtype=float)
|
|
146
|
+
if y.shape == (1, 1) and self._resolved_shape != (1, 1):
|
|
147
|
+
self.outputs["out"] = np.full(self._resolved_shape, float(y[0, 0]), dtype=float)
|
|
148
|
+
return
|
|
149
|
+
|
|
150
|
+
u_prev = self.state["u_prev"]
|
|
151
|
+
if u_prev is None:
|
|
152
|
+
# No previous value -> define derivative as zero (same shape as u)
|
|
153
|
+
self.outputs["out"] = np.zeros_like(u_arr)
|
|
154
|
+
return
|
|
155
|
+
|
|
156
|
+
# If shape frozen and u_prev is scalar, broadcast it
|
|
157
|
+
u_prev_arr = np.asarray(u_prev, dtype=float)
|
|
158
|
+
if self._resolved_shape is not None and u_prev_arr.shape == (1, 1) and self._resolved_shape != (1, 1):
|
|
159
|
+
u_prev_arr = np.full(self._resolved_shape, float(u_prev_arr[0, 0]), dtype=float)
|
|
160
|
+
|
|
161
|
+
if u_prev_arr.shape != u_arr.shape:
|
|
162
|
+
raise ValueError(
|
|
163
|
+
f"[{self.name}] Previous input shape mismatch: u_prev={u_prev_arr.shape}, u={u_arr.shape}."
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
self.outputs["out"] = (u_arr - u_prev_arr) / dt
|
|
167
|
+
|
|
168
|
+
# -------------------------------------------------------
|
|
169
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
170
|
+
u_arr = self._normalize_input(self.inputs["in"])
|
|
171
|
+
self.next_state["u_prev"] = u_arr.copy()
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
# --------------------------------------------------------------------------
|
|
175
|
+
# Private methods
|
|
176
|
+
# --------------------------------------------------------------------------
|
|
177
|
+
def _maybe_freeze_shape_from(self, u: np.ndarray) -> None:
|
|
178
|
+
"""
|
|
179
|
+
Freeze shape if:
|
|
180
|
+
- no initial_output has already frozen it
|
|
181
|
+
- shape unresolved
|
|
182
|
+
- u is non-scalar (shape != (1,1))
|
|
183
|
+
"""
|
|
184
|
+
if u.ndim != 2:
|
|
185
|
+
raise ValueError(
|
|
186
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# If shape is already frozen (by initial_output), nothing to do
|
|
190
|
+
if self._resolved_shape is not None:
|
|
191
|
+
return
|
|
192
|
+
|
|
193
|
+
# Only freeze when a non-scalar appears
|
|
194
|
+
if u.shape != (1, 1):
|
|
195
|
+
self._resolved_shape = u.shape
|
|
196
|
+
|
|
197
|
+
# Upgrade current output placeholder to correct shape (keep current scalar value)
|
|
198
|
+
y = np.asarray(self.outputs["out"], dtype=float)
|
|
199
|
+
if y.shape == (1, 1):
|
|
200
|
+
scalar = float(y[0, 0])
|
|
201
|
+
self.outputs["out"] = np.full(self._resolved_shape, scalar, dtype=float)
|
|
202
|
+
|
|
203
|
+
# Initialize u_prev to current u to avoid a derivative spike
|
|
204
|
+
self.state["u_prev"] = u.copy()
|
|
205
|
+
self.next_state["u_prev"] = u.copy()
|
|
206
|
+
|
|
207
|
+
# -------------------------------------------------------
|
|
208
|
+
def _normalize_input(self, u: ArrayLike | None) -> np.ndarray:
|
|
209
|
+
"""
|
|
210
|
+
Normalize u to 2D, apply freezing rule and scalar broadcasting.
|
|
211
|
+
If u is None: return zeros of resolved shape if known, else (1,1) zeros.
|
|
212
|
+
"""
|
|
213
|
+
if u is None:
|
|
214
|
+
if self._resolved_shape is not None:
|
|
215
|
+
return np.zeros(self._resolved_shape, dtype=float)
|
|
216
|
+
return self._placeholder.copy()
|
|
217
|
+
|
|
218
|
+
u_arr = np.asarray(u, dtype=float)
|
|
219
|
+
if u_arr.ndim != 2:
|
|
220
|
+
raise ValueError(
|
|
221
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u_arr.ndim} with shape {u_arr.shape}."
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
# If shape not frozen yet and u is non-scalar -> freeze now
|
|
225
|
+
self._maybe_freeze_shape_from(u_arr)
|
|
226
|
+
|
|
227
|
+
# If shape is frozen: enforce or broadcast
|
|
228
|
+
if self._resolved_shape is not None:
|
|
229
|
+
if u_arr.shape == (1, 1) and self._resolved_shape != (1, 1):
|
|
230
|
+
return np.full(self._resolved_shape, float(u_arr[0, 0]), dtype=float)
|
|
231
|
+
|
|
232
|
+
if u_arr.shape != self._resolved_shape:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"[{self.name}] Input 'in' shape changed: expected {self._resolved_shape}, got {u_arr.shape}."
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
return u_arr
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
# ******************************************************************************
|
|
2
|
+
# pySimBlocks
|
|
3
|
+
# Copyright (c) 2026 Université de Lille & INRIA
|
|
4
|
+
# ******************************************************************************
|
|
5
|
+
# This program is free software: you can redistribute it and/or modify it
|
|
6
|
+
# under the terms of the GNU Lesser General Public License as published by
|
|
7
|
+
# the Free Software Foundation, either version 3 of the License, or (at your
|
|
8
|
+
# option) any later version.
|
|
9
|
+
#
|
|
10
|
+
# This program is distributed in the hope that it will be useful, but WITHOUT
|
|
11
|
+
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
|
12
|
+
# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License
|
|
13
|
+
# for more details.
|
|
14
|
+
#
|
|
15
|
+
# You should have received a copy of the GNU Lesser General Public License
|
|
16
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
17
|
+
# ******************************************************************************
|
|
18
|
+
# Authors: see Authors.txt
|
|
19
|
+
# ******************************************************************************
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from numpy.typing import ArrayLike
|
|
23
|
+
|
|
24
|
+
from pySimBlocks.core.block import Block
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DiscreteIntegrator(Block):
|
|
28
|
+
"""
|
|
29
|
+
Discrete-time integrator block.
|
|
30
|
+
|
|
31
|
+
Summary:
|
|
32
|
+
Integrates an input signal over time using a discrete-time numerical
|
|
33
|
+
integration scheme.
|
|
34
|
+
|
|
35
|
+
Parameters:
|
|
36
|
+
initial_state : scalar or array-like, optional
|
|
37
|
+
Initial value of the integrated state. If provided, it FIXES the signal shape.
|
|
38
|
+
method : str
|
|
39
|
+
Numerical integration method: "euler forward" or "euler backward".
|
|
40
|
+
sample_time : float, optional
|
|
41
|
+
Block execution period.
|
|
42
|
+
|
|
43
|
+
Inputs:
|
|
44
|
+
in : array (m,n)
|
|
45
|
+
Signal to integrate (must be 2D).
|
|
46
|
+
|
|
47
|
+
Outputs:
|
|
48
|
+
out : array (m,n)
|
|
49
|
+
Integrated signal.
|
|
50
|
+
|
|
51
|
+
Notes:
|
|
52
|
+
- Stateful block.
|
|
53
|
+
- Direct feedthrough depends on method:
|
|
54
|
+
* euler forward -> False
|
|
55
|
+
* euler backward -> True
|
|
56
|
+
- Shape is frozen as soon as known (initial_state or first input).
|
|
57
|
+
- No implicit vector reshape; matrices are supported.
|
|
58
|
+
- Policy:
|
|
59
|
+
+ Never propagate None: output is always a 2D array (at least (1,1)).
|
|
60
|
+
+ Shape is NOT frozen while we are in placeholder shape (1,1) and no explicit
|
|
61
|
+
non-scalar initial_state was given.
|
|
62
|
+
+ As soon as a non-scalar input appears (shape != (1,1)), the shape becomes frozen.
|
|
63
|
+
+ If initial_state is provided and non-scalar, it freezes the shape immediately.
|
|
64
|
+
If initial_state is scalar (1,1), it is treated as a placeholder: can be upgraded
|
|
65
|
+
once a non-scalar input appears.
|
|
66
|
+
+ After shape is frozen, any non-scalar input shape mismatch raises ValueError.
|
|
67
|
+
+ If shape is frozen to (m,n), scalar input (1,1) is broadcast to (m,n).
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
def __init__(
|
|
71
|
+
self,
|
|
72
|
+
name: str,
|
|
73
|
+
initial_state: ArrayLike | None = None,
|
|
74
|
+
method: str = "euler forward",
|
|
75
|
+
sample_time: float | None = None,
|
|
76
|
+
):
|
|
77
|
+
super().__init__(name, sample_time)
|
|
78
|
+
|
|
79
|
+
self.method = method.lower()
|
|
80
|
+
if self.method not in ("euler forward", "euler backward"):
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"[{self.name}] Unsupported method '{method}'. "
|
|
83
|
+
f"Allowed: 'euler forward', 'euler backward'."
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
# direct feedthrough policy
|
|
87
|
+
self.direct_feedthrough = (self.method == "euler backward")
|
|
88
|
+
|
|
89
|
+
# ports
|
|
90
|
+
self.inputs["in"] = None
|
|
91
|
+
self.outputs["out"] = None
|
|
92
|
+
|
|
93
|
+
# shape policy
|
|
94
|
+
self._resolved_shape: tuple[int, int] | None = None
|
|
95
|
+
|
|
96
|
+
# state
|
|
97
|
+
self.state["x"] = None
|
|
98
|
+
self.next_state["x"] = None
|
|
99
|
+
|
|
100
|
+
# Placeholder initialization (never None)
|
|
101
|
+
self._placeholder = np.zeros((1, 1), dtype=float)
|
|
102
|
+
|
|
103
|
+
self._initial_state_raw: np.ndarray | None = None
|
|
104
|
+
if initial_state is not None:
|
|
105
|
+
x0 = self._to_2d_array("initial_state", initial_state).astype(float)
|
|
106
|
+
self._initial_state_raw = x0.copy()
|
|
107
|
+
|
|
108
|
+
# If non-scalar: freeze immediately. If scalar (1,1): keep unfrozen placeholder semantics.
|
|
109
|
+
if x0.shape != (1, 1):
|
|
110
|
+
self._resolved_shape = x0.shape
|
|
111
|
+
|
|
112
|
+
self.state["x"] = x0.copy()
|
|
113
|
+
self.next_state["x"] = x0.copy()
|
|
114
|
+
self.outputs["out"] = x0.copy()
|
|
115
|
+
else:
|
|
116
|
+
self.state["x"] = self._placeholder.copy()
|
|
117
|
+
self.next_state["x"] = self._placeholder.copy()
|
|
118
|
+
self.outputs["out"] = self._placeholder.copy()
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# --------------------------------------------------------------------------
|
|
122
|
+
# Public methods
|
|
123
|
+
# --------------------------------------------------------------------------
|
|
124
|
+
def initialize(self, t0: float) -> None:
|
|
125
|
+
# Never propagate None: guarantee placeholders even when no initial_state.
|
|
126
|
+
if self._initial_state_raw is not None:
|
|
127
|
+
x0 = self._initial_state_raw.copy()
|
|
128
|
+
|
|
129
|
+
# If initial_state is non-scalar, freeze is already set in __init__.
|
|
130
|
+
# If scalar, keep unresolved unless later a non-scalar input appears.
|
|
131
|
+
self.state["x"] = x0.copy()
|
|
132
|
+
self.next_state["x"] = x0.copy()
|
|
133
|
+
|
|
134
|
+
# output at init:
|
|
135
|
+
# forward -> y=x
|
|
136
|
+
# backward -> y=x + dt*u, but u may be unknown at init; we take u=0 (consistent with "no None")
|
|
137
|
+
if self.method == "euler forward":
|
|
138
|
+
self.outputs["out"] = x0.copy()
|
|
139
|
+
else:
|
|
140
|
+
self.outputs["out"] = x0.copy()
|
|
141
|
+
|
|
142
|
+
else:
|
|
143
|
+
self.state["x"] = self._placeholder.copy()
|
|
144
|
+
self.next_state["x"] = self._placeholder.copy()
|
|
145
|
+
self.outputs["out"] = self._placeholder.copy()
|
|
146
|
+
|
|
147
|
+
# ------------------------------------------------------------------
|
|
148
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
149
|
+
x = self._normalize_state()
|
|
150
|
+
|
|
151
|
+
if self.method == "euler forward":
|
|
152
|
+
self.outputs["out"] = x.copy()
|
|
153
|
+
return
|
|
154
|
+
|
|
155
|
+
# euler backward: y = x + dt*u
|
|
156
|
+
u = self._normalize_input(self.inputs["in"])
|
|
157
|
+
self.outputs["out"] = x + dt * u
|
|
158
|
+
|
|
159
|
+
# ------------------------------------------------------------------
|
|
160
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
161
|
+
# Even if input is not available due to execution order, do not crash:
|
|
162
|
+
# treat missing as zeros (same idea: "0 if not defined").
|
|
163
|
+
u = self._normalize_input(self.inputs["in"])
|
|
164
|
+
|
|
165
|
+
x = self._normalize_state()
|
|
166
|
+
|
|
167
|
+
# ensure x matches u when shape resolved by u
|
|
168
|
+
if self._resolved_shape is not None:
|
|
169
|
+
if x.shape != u.shape:
|
|
170
|
+
raise ValueError(
|
|
171
|
+
f"[{self.name}] Shape mismatch between state and input: x={x.shape}, u={u.shape}."
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
self.next_state["x"] = x + dt * u
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
# --------------------------------------------------------------------------
|
|
178
|
+
# Private methods
|
|
179
|
+
# --------------------------------------------------------------------------
|
|
180
|
+
def _maybe_freeze_shape_from(self, u: np.ndarray) -> None:
|
|
181
|
+
"""
|
|
182
|
+
Freeze shape if:
|
|
183
|
+
- shape not resolved yet
|
|
184
|
+
- and u is non-scalar (shape != (1,1))
|
|
185
|
+
"""
|
|
186
|
+
if u.ndim != 2:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
if self._resolved_shape is None and u.shape != (1, 1):
|
|
192
|
+
self._resolved_shape = u.shape
|
|
193
|
+
|
|
194
|
+
# Upgrade state/output from placeholder scalar -> matrix if needed
|
|
195
|
+
if self.state["x"] is None:
|
|
196
|
+
self.state["x"] = np.zeros(self._resolved_shape, dtype=float)
|
|
197
|
+
else:
|
|
198
|
+
x = np.asarray(self.state["x"], dtype=float)
|
|
199
|
+
if x.shape == (1, 1):
|
|
200
|
+
scalar = float(x[0, 0])
|
|
201
|
+
self.state["x"] = np.full(self._resolved_shape, scalar, dtype=float)
|
|
202
|
+
|
|
203
|
+
# keep next_state consistent
|
|
204
|
+
self.next_state["x"] = np.asarray(self.state["x"], dtype=float).copy()
|
|
205
|
+
|
|
206
|
+
y = np.asarray(self.outputs["out"], dtype=float)
|
|
207
|
+
if y.shape == (1, 1):
|
|
208
|
+
scalar = float(y[0, 0])
|
|
209
|
+
self.outputs["out"] = np.full(self._resolved_shape, scalar, dtype=float)
|
|
210
|
+
|
|
211
|
+
# ------------------------------------------------------------------
|
|
212
|
+
def _normalize_input(self, u: ArrayLike | None) -> np.ndarray:
|
|
213
|
+
"""
|
|
214
|
+
Normalize input to 2D array, apply shape policy and scalar broadcasting.
|
|
215
|
+
|
|
216
|
+
If u is None:
|
|
217
|
+
- return zeros of resolved shape if known,
|
|
218
|
+
- else return (1,1) zeros (placeholder), without freezing.
|
|
219
|
+
"""
|
|
220
|
+
if u is None:
|
|
221
|
+
if self._resolved_shape is not None:
|
|
222
|
+
return np.zeros(self._resolved_shape, dtype=float)
|
|
223
|
+
return self._placeholder.copy()
|
|
224
|
+
|
|
225
|
+
u_arr = np.asarray(u, dtype=float)
|
|
226
|
+
if u_arr.ndim != 2:
|
|
227
|
+
raise ValueError(
|
|
228
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u_arr.ndim} with shape {u_arr.shape}."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
# Potentially freeze shape when a non-scalar appears
|
|
232
|
+
self._maybe_freeze_shape_from(u_arr)
|
|
233
|
+
|
|
234
|
+
# If shape is frozen and input is scalar -> broadcast
|
|
235
|
+
if self._resolved_shape is not None:
|
|
236
|
+
if u_arr.shape == (1, 1) and self._resolved_shape != (1, 1):
|
|
237
|
+
return np.full(self._resolved_shape, float(u_arr[0, 0]), dtype=float)
|
|
238
|
+
|
|
239
|
+
if u_arr.shape != self._resolved_shape:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
f"[{self.name}] Input 'in' shape changed: expected {self._resolved_shape}, got {u_arr.shape}."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return u_arr
|
|
245
|
+
|
|
246
|
+
# ------------------------------------------------------------------
|
|
247
|
+
def _normalize_state(self) -> np.ndarray:
|
|
248
|
+
"""
|
|
249
|
+
Ensure state exists and matches resolved shape.
|
|
250
|
+
"""
|
|
251
|
+
x = np.asarray(self.state["x"], dtype=float)
|
|
252
|
+
|
|
253
|
+
# If shape resolved and x is scalar placeholder -> broadcast
|
|
254
|
+
if self._resolved_shape is not None and self._resolved_shape != (1, 1):
|
|
255
|
+
if x.shape == (1, 1):
|
|
256
|
+
scalar = float(x[0, 0])
|
|
257
|
+
x = np.full(self._resolved_shape, scalar, dtype=float)
|
|
258
|
+
self.state["x"] = x.copy()
|
|
259
|
+
|
|
260
|
+
if x.shape != self._resolved_shape:
|
|
261
|
+
raise ValueError(
|
|
262
|
+
f"[{self.name}] State shape mismatch: expected {self._resolved_shape}, got {x.shape}."
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
return x
|