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,214 @@
|
|
|
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 RateLimiter(Block):
|
|
28
|
+
"""
|
|
29
|
+
Discrete-time rate limiter block.
|
|
30
|
+
|
|
31
|
+
Summary:
|
|
32
|
+
Limits the rate of change of the output signal by constraining the
|
|
33
|
+
maximum allowed increase and decrease per time step.
|
|
34
|
+
|
|
35
|
+
Parameters:
|
|
36
|
+
rising_slope : scalar or array-like, optional
|
|
37
|
+
Maximum allowed positive rate of change (>= 0).
|
|
38
|
+
falling_slope : scalar or array-like, optional
|
|
39
|
+
Maximum allowed negative rate of change (<= 0).
|
|
40
|
+
initial_output : scalar or array-like, optional
|
|
41
|
+
Initial output y(-1). If not provided, y(-1) = u(0).
|
|
42
|
+
sample_time : float, optional
|
|
43
|
+
Block execution period.
|
|
44
|
+
|
|
45
|
+
Inputs:
|
|
46
|
+
in : array (m,n)
|
|
47
|
+
Input signal (must be 2D).
|
|
48
|
+
|
|
49
|
+
Outputs:
|
|
50
|
+
out : array (m,n)
|
|
51
|
+
Rate-limited output signal.
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- Stateful block.
|
|
55
|
+
- Direct feedthrough.
|
|
56
|
+
- Broadcasting rules (to match input shape (m,n)):
|
|
57
|
+
* scalar (1,1) broadcasts to (m,n)
|
|
58
|
+
* vector (m,1) broadcasts across columns to (m,n)
|
|
59
|
+
* matrix (m,n) must match exactly
|
|
60
|
+
- Once resolved, input shape must remain constant.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
direct_feedthrough = True
|
|
64
|
+
|
|
65
|
+
def __init__(
|
|
66
|
+
self,
|
|
67
|
+
name: str,
|
|
68
|
+
rising_slope: ArrayLike = np.inf,
|
|
69
|
+
falling_slope: ArrayLike = -np.inf,
|
|
70
|
+
initial_output: ArrayLike | None = None,
|
|
71
|
+
sample_time: float | None = None,
|
|
72
|
+
):
|
|
73
|
+
super().__init__(name, sample_time)
|
|
74
|
+
|
|
75
|
+
self.inputs["in"] = None
|
|
76
|
+
self.outputs["out"] = None
|
|
77
|
+
|
|
78
|
+
# Raw parameters (2D normalized)
|
|
79
|
+
self.rising_raw = self._to_2d_array("rising_slope", rising_slope)
|
|
80
|
+
self.falling_raw = self._to_2d_array("falling_slope", falling_slope)
|
|
81
|
+
self.y0_raw = None if initial_output is None else self._to_2d_array("initial_output", initial_output)
|
|
82
|
+
|
|
83
|
+
# Basic sign constraints (raw)
|
|
84
|
+
if np.any(self.rising_raw < 0):
|
|
85
|
+
raise ValueError(f"[{self.name}] rising_slope must be >= 0.")
|
|
86
|
+
if np.any(self.falling_raw > 0):
|
|
87
|
+
raise ValueError(f"[{self.name}] falling_slope must be <= 0.")
|
|
88
|
+
|
|
89
|
+
# Resolved parameters (broadcasted to input shape once known)
|
|
90
|
+
self.rising_slope: ArrayLike | None = None
|
|
91
|
+
self.falling_slope: ArrayLike | None = None
|
|
92
|
+
self._resolved_shape: tuple[int, int] | None = None
|
|
93
|
+
|
|
94
|
+
# State
|
|
95
|
+
self.state["y"] = None
|
|
96
|
+
self.next_state["y"] = None
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# --------------------------------------------------------------------------
|
|
100
|
+
# Public methods
|
|
101
|
+
# --------------------------------------------------------------------------
|
|
102
|
+
def initialize(self, t0: float) -> None:
|
|
103
|
+
u = self.inputs["in"]
|
|
104
|
+
if u is None:
|
|
105
|
+
raise RuntimeError(f"[{self.name}] Input 'in' is None at initialization.")
|
|
106
|
+
|
|
107
|
+
u = np.asarray(u, dtype=float)
|
|
108
|
+
if u.ndim != 2:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
self._resolve_for_input(u)
|
|
114
|
+
|
|
115
|
+
if self.y0_raw is not None:
|
|
116
|
+
y0 = self._broadcast_param(self.y0_raw, u.shape, "initial_output")
|
|
117
|
+
else:
|
|
118
|
+
y0 = u.copy()
|
|
119
|
+
|
|
120
|
+
self.state["y"] = y0.copy()
|
|
121
|
+
self.outputs["out"] = y0.copy()
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
125
|
+
u = self.inputs["in"]
|
|
126
|
+
if u is None:
|
|
127
|
+
raise RuntimeError(f"[{self.name}] Input 'in' is None.")
|
|
128
|
+
|
|
129
|
+
u = np.asarray(u, dtype=float)
|
|
130
|
+
if u.ndim != 2:
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
if self.state["y"] is None:
|
|
136
|
+
raise RuntimeError(f"[{self.name}] RateLimiter not initialized (state 'y' is None).")
|
|
137
|
+
|
|
138
|
+
self._resolve_for_input(u)
|
|
139
|
+
|
|
140
|
+
y_prev = self.state["y"]
|
|
141
|
+
if y_prev.shape != u.shape:
|
|
142
|
+
# extra safety: state shape must match input shape
|
|
143
|
+
raise ValueError(
|
|
144
|
+
f"[{self.name}] Internal state shape mismatch: y has shape {y_prev.shape}, input has shape {u.shape}."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
du = u - y_prev
|
|
148
|
+
du_min = self.falling_slope * dt
|
|
149
|
+
du_max = self.rising_slope * dt
|
|
150
|
+
|
|
151
|
+
du_limited = np.clip(du, du_min, du_max)
|
|
152
|
+
self.outputs["out"] = y_prev + du_limited
|
|
153
|
+
|
|
154
|
+
# ------------------------------------------------------------------
|
|
155
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
156
|
+
self.next_state["y"] = None if self.outputs["out"] is None else self.outputs["out"].copy()
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# --------------------------------------------------------------------------
|
|
160
|
+
# Private methods
|
|
161
|
+
# --------------------------------------------------------------------------
|
|
162
|
+
def _broadcast_param(self, p: np.ndarray, target_shape: tuple[int, int], name: str) -> np.ndarray:
|
|
163
|
+
"""
|
|
164
|
+
Broadcast p to target_shape using explicit rules:
|
|
165
|
+
- scalar (1,1) -> (m,n)
|
|
166
|
+
- vector (m,1) -> repeat along columns to (m,n)
|
|
167
|
+
- matrix (m,n) -> exact match
|
|
168
|
+
"""
|
|
169
|
+
m, n = target_shape
|
|
170
|
+
|
|
171
|
+
if self._is_scalar_2d(p):
|
|
172
|
+
return np.full(target_shape, float(p[0, 0]), dtype=float)
|
|
173
|
+
|
|
174
|
+
if p.ndim == 2 and p.shape[1] == 1 and p.shape[0] == m:
|
|
175
|
+
if n == 1:
|
|
176
|
+
return p.astype(float, copy=False)
|
|
177
|
+
return np.repeat(p.astype(float, copy=False), n, axis=1)
|
|
178
|
+
|
|
179
|
+
if p.shape == target_shape:
|
|
180
|
+
return p.astype(float, copy=False)
|
|
181
|
+
|
|
182
|
+
raise ValueError(
|
|
183
|
+
f"[{self.name}] {name} has incompatible shape {p.shape} for input shape {target_shape}. "
|
|
184
|
+
f"Allowed: scalar (1,1), vector (m,1), or matrix (m,n)."
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
# ------------------------------------------------------------------
|
|
188
|
+
def _resolve_for_input(self, u: np.ndarray) -> None:
|
|
189
|
+
"""
|
|
190
|
+
Resolve (broadcast) slopes and initial_output to match the current input shape.
|
|
191
|
+
Done once. After that, input shape is fixed.
|
|
192
|
+
"""
|
|
193
|
+
if u.ndim != 2:
|
|
194
|
+
raise ValueError(
|
|
195
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if self._resolved_shape is None:
|
|
199
|
+
self._resolved_shape = u.shape
|
|
200
|
+
self.rising_slope = self._broadcast_param(self.rising_raw, u.shape, "rising_slope")
|
|
201
|
+
self.falling_slope = self._broadcast_param(self.falling_raw, u.shape, "falling_slope")
|
|
202
|
+
|
|
203
|
+
# Re-check signs after broadcasting (useful if vector/matrix provided)
|
|
204
|
+
if np.any(self.rising_slope < 0):
|
|
205
|
+
raise ValueError(f"[{self.name}] rising_slope must be >= 0.")
|
|
206
|
+
if np.any(self.falling_slope > 0):
|
|
207
|
+
raise ValueError(f"[{self.name}] falling_slope must be <= 0.")
|
|
208
|
+
return
|
|
209
|
+
|
|
210
|
+
if u.shape != self._resolved_shape:
|
|
211
|
+
raise ValueError(
|
|
212
|
+
f"[{self.name}] Input 'in' shape changed after initialization: "
|
|
213
|
+
f"expected {self._resolved_shape}, got {u.shape}."
|
|
214
|
+
)
|
|
@@ -0,0 +1,178 @@
|
|
|
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 Saturation(Block):
|
|
28
|
+
"""
|
|
29
|
+
Discrete-time saturation operator.
|
|
30
|
+
|
|
31
|
+
Summary:
|
|
32
|
+
Applies element-wise saturation to the input signal by enforcing
|
|
33
|
+
lower and upper bounds.
|
|
34
|
+
|
|
35
|
+
Parameters:
|
|
36
|
+
u_min : scalar or array-like, optional
|
|
37
|
+
Lower saturation bound.
|
|
38
|
+
Accepted: scalar -> (1,1), 1D -> (m,1), 2D -> (m,n).
|
|
39
|
+
Broadcasting rules are limited and explicit (see Notes).
|
|
40
|
+
u_max : scalar or array-like, optional
|
|
41
|
+
Upper saturation bound.
|
|
42
|
+
sample_time : float, optional
|
|
43
|
+
Block execution period.
|
|
44
|
+
|
|
45
|
+
Inputs:
|
|
46
|
+
in : array (m,n)
|
|
47
|
+
Input signal (must be 2D).
|
|
48
|
+
|
|
49
|
+
Outputs:
|
|
50
|
+
out : array (m,n)
|
|
51
|
+
Saturated output signal.
|
|
52
|
+
|
|
53
|
+
Notes:
|
|
54
|
+
- Stateless block.
|
|
55
|
+
- Direct feedthrough.
|
|
56
|
+
- Input must be 2D. No implicit reshape/flatten.
|
|
57
|
+
- Broadcasting rules for bounds (to match input shape (m,n)):
|
|
58
|
+
* scalar (1,1) broadcasts to (m,n)
|
|
59
|
+
* vector (m,1) broadcasts across columns to (m,n)
|
|
60
|
+
* matrix (m,n) must match exactly
|
|
61
|
+
- Any other shape mismatch raises ValueError.
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
direct_feedthrough = True
|
|
65
|
+
|
|
66
|
+
def __init__(
|
|
67
|
+
self,
|
|
68
|
+
name: str,
|
|
69
|
+
u_min: ArrayLike = -np.inf,
|
|
70
|
+
u_max: ArrayLike = np.inf,
|
|
71
|
+
sample_time: float | None = None,
|
|
72
|
+
):
|
|
73
|
+
super().__init__(name, sample_time)
|
|
74
|
+
|
|
75
|
+
self.inputs["in"] = None
|
|
76
|
+
self.outputs["out"] = None
|
|
77
|
+
|
|
78
|
+
self.u_min_raw = self._to_2d_array("u_min", u_min)
|
|
79
|
+
self.u_max_raw = self._to_2d_array("u_max", u_max)
|
|
80
|
+
|
|
81
|
+
# These will become "resolved" (broadcasted) once we know input shape
|
|
82
|
+
self.u_min = None
|
|
83
|
+
self.u_max = None
|
|
84
|
+
self._resolved_shape: tuple[int, int] | None = None
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# --------------------------------------------------------------------------
|
|
88
|
+
# Public methods
|
|
89
|
+
# --------------------------------------------------------------------------
|
|
90
|
+
def initialize(self, t0: float) -> None:
|
|
91
|
+
u = self.inputs["in"]
|
|
92
|
+
if u is None:
|
|
93
|
+
raise RuntimeError(f"[{self.name}] Input 'in' is None at initialization.")
|
|
94
|
+
|
|
95
|
+
u = np.asarray(u, dtype=float)
|
|
96
|
+
if u.ndim != 2:
|
|
97
|
+
raise ValueError(
|
|
98
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
self._resolve_bounds_for_input(u)
|
|
102
|
+
self.outputs["out"] = np.clip(u, self.u_min, self.u_max)
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
106
|
+
u = self.inputs["in"]
|
|
107
|
+
if u is None:
|
|
108
|
+
raise RuntimeError(f"[{self.name}] Input 'in' is None.")
|
|
109
|
+
|
|
110
|
+
u = np.asarray(u, dtype=float)
|
|
111
|
+
if u.ndim != 2:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
self._resolve_bounds_for_input(u)
|
|
117
|
+
self.outputs["out"] = np.clip(u, self.u_min, self.u_max)
|
|
118
|
+
|
|
119
|
+
# ------------------------------------------------------------------
|
|
120
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
121
|
+
return # stateless
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# --------------------------------------------------------------------------
|
|
125
|
+
# Private methods
|
|
126
|
+
# --------------------------------------------------------------------------
|
|
127
|
+
def _resolve_bounds_for_input(self, u: np.ndarray) -> None:
|
|
128
|
+
"""
|
|
129
|
+
Resolve (broadcast) u_min/u_max to match the current input shape.
|
|
130
|
+
Resolution is done once (first time input is available). After that,
|
|
131
|
+
input shape is expected to remain constant.
|
|
132
|
+
"""
|
|
133
|
+
if u.ndim != 2:
|
|
134
|
+
raise ValueError(
|
|
135
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
if self._resolved_shape is None:
|
|
139
|
+
self._resolved_shape = u.shape
|
|
140
|
+
|
|
141
|
+
self.u_min = self._broadcast_bound(self.u_min_raw, u.shape, "u_min")
|
|
142
|
+
self.u_max = self._broadcast_bound(self.u_max_raw, u.shape, "u_max")
|
|
143
|
+
|
|
144
|
+
if np.any(self.u_min > self.u_max):
|
|
145
|
+
raise ValueError(f"[{self.name}] u_min must be <= u_max for all components.")
|
|
146
|
+
return
|
|
147
|
+
|
|
148
|
+
# Already resolved => enforce constant input shape
|
|
149
|
+
if u.shape != self._resolved_shape:
|
|
150
|
+
raise ValueError(
|
|
151
|
+
f"[{self.name}] Input 'in' shape changed after bounds were resolved: "
|
|
152
|
+
f"expected {self._resolved_shape}, got {u.shape}."
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def _broadcast_bound(self, b: np.ndarray, target_shape: tuple[int, int], name: str) -> np.ndarray:
|
|
156
|
+
"""
|
|
157
|
+
Broadcast bound b to target_shape using explicit rules.
|
|
158
|
+
"""
|
|
159
|
+
m, n = target_shape
|
|
160
|
+
|
|
161
|
+
# scalar -> full matrix
|
|
162
|
+
if self._is_scalar_2d(b):
|
|
163
|
+
return np.full(target_shape, float(b[0, 0]), dtype=float)
|
|
164
|
+
|
|
165
|
+
# vector (m,1) -> broadcast across columns
|
|
166
|
+
if b.ndim == 2 and b.shape[1] == 1 and b.shape[0] == m:
|
|
167
|
+
if n == 1:
|
|
168
|
+
return b.astype(float, copy=False)
|
|
169
|
+
return np.repeat(b.astype(float, copy=False), n, axis=1)
|
|
170
|
+
|
|
171
|
+
# matrix -> must match exactly
|
|
172
|
+
if b.shape == target_shape:
|
|
173
|
+
return b.astype(float, copy=False)
|
|
174
|
+
|
|
175
|
+
raise ValueError(
|
|
176
|
+
f"[{self.name}] {name} has incompatible shape {b.shape} for input shape {target_shape}. "
|
|
177
|
+
f"Allowed: scalar (1,1), vector (m,1), or matrix (m,n)."
|
|
178
|
+
)
|
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
|
|
23
|
+
from pySimBlocks.core.block import Block
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Sum(Block):
|
|
27
|
+
"""
|
|
28
|
+
Multi-input summation block.
|
|
29
|
+
|
|
30
|
+
Summary:
|
|
31
|
+
Computes an element-wise sum/subtraction of multiple input signals.
|
|
32
|
+
|
|
33
|
+
Parameters:
|
|
34
|
+
signs : str
|
|
35
|
+
Sequence of '+' and '-' defining the sign of each input (e.g. '++-', '+-').
|
|
36
|
+
If None, defaults to '++' (two inputs).
|
|
37
|
+
sample_time : float, optional
|
|
38
|
+
Block execution period.
|
|
39
|
+
|
|
40
|
+
Inputs:
|
|
41
|
+
in1, in2, ..., inN : array (m,n)
|
|
42
|
+
Input signals (must be 2D).
|
|
43
|
+
Scalar (1,1) inputs can be broadcast to the common target shape.
|
|
44
|
+
|
|
45
|
+
Outputs:
|
|
46
|
+
out : array (m,n)
|
|
47
|
+
Element-wise signed sum.
|
|
48
|
+
|
|
49
|
+
Notes:
|
|
50
|
+
- Direct feedthrough.
|
|
51
|
+
- Stateless.
|
|
52
|
+
- Shape policy:
|
|
53
|
+
* all inputs must be 2D
|
|
54
|
+
* all non-scalar inputs must share exactly the same shape
|
|
55
|
+
* scalar (1,1) inputs can be broadcast to that shape
|
|
56
|
+
* no other broadcasting is allowed
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
direct_feedthrough = True
|
|
60
|
+
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
name: str,
|
|
64
|
+
signs: str | None = None,
|
|
65
|
+
sample_time: float | None = None,
|
|
66
|
+
):
|
|
67
|
+
super().__init__(name, sample_time)
|
|
68
|
+
|
|
69
|
+
if signs is None:
|
|
70
|
+
signs = "++"
|
|
71
|
+
|
|
72
|
+
if not isinstance(signs, str):
|
|
73
|
+
raise TypeError(f"[{self.name}] 'signs' must be a str.")
|
|
74
|
+
|
|
75
|
+
if len(signs) == 0:
|
|
76
|
+
raise ValueError(f"[{self.name}] 'signs' must not be empty.")
|
|
77
|
+
|
|
78
|
+
if any(s not in ("+", "-") for s in signs):
|
|
79
|
+
raise ValueError(f"[{self.name}] 'signs' must contain only '+' or '-'.")
|
|
80
|
+
|
|
81
|
+
self.signs = [1.0 if s == "+" else -1.0 for s in signs]
|
|
82
|
+
self.num_inputs = len(self.signs)
|
|
83
|
+
|
|
84
|
+
for i in range(self.num_inputs):
|
|
85
|
+
self.inputs[f"in{i+1}"] = None
|
|
86
|
+
|
|
87
|
+
self.outputs["out"] = None
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
# --------------------------------------------------------------------------
|
|
91
|
+
# Public methods
|
|
92
|
+
# --------------------------------------------------------------------------
|
|
93
|
+
def initialize(self, t0: float) -> None:
|
|
94
|
+
"""
|
|
95
|
+
If all inputs are already available, compute output.
|
|
96
|
+
Otherwise output stays None until first output_update().
|
|
97
|
+
"""
|
|
98
|
+
if any(self.inputs[f"in{i+1}"] is None for i in range(self.num_inputs)):
|
|
99
|
+
self.outputs["out"] = None
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
self.outputs["out"] = self._compute_output()
|
|
103
|
+
|
|
104
|
+
# ------------------------------------------------------------------
|
|
105
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
106
|
+
# Validate presence + 2D constraint
|
|
107
|
+
arrays = []
|
|
108
|
+
for i in range(self.num_inputs):
|
|
109
|
+
key = f"in{i+1}"
|
|
110
|
+
u = self.inputs[key]
|
|
111
|
+
if u is None:
|
|
112
|
+
raise RuntimeError(f"[{self.name}] Input '{key}' is not connected or not set.")
|
|
113
|
+
|
|
114
|
+
a = np.asarray(u, dtype=float)
|
|
115
|
+
if a.ndim != 2:
|
|
116
|
+
raise ValueError(
|
|
117
|
+
f"[{self.name}] Input '{key}' must be a 2D array. Got ndim={a.ndim} with shape {a.shape}."
|
|
118
|
+
)
|
|
119
|
+
arrays.append(a)
|
|
120
|
+
|
|
121
|
+
self.outputs["out"] = self._compute_output(prevalidated_arrays=arrays)
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
def state_update(self, t: float, dt: float) -> None:
|
|
125
|
+
return # stateless
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
# --------------------------------------------------------------------------
|
|
129
|
+
# Private methods
|
|
130
|
+
# --------------------------------------------------------------------------
|
|
131
|
+
def _resolve_common_shape(self, arrays: list[np.ndarray]) -> tuple[int, int]:
|
|
132
|
+
"""
|
|
133
|
+
Determine target shape among inputs.
|
|
134
|
+
|
|
135
|
+
- Scalars (1,1) are broadcastable
|
|
136
|
+
- Any non-scalar fixes the target shape
|
|
137
|
+
- If multiple non-scalars exist, they must all match exactly
|
|
138
|
+
- If all scalars => target is (1,1)
|
|
139
|
+
"""
|
|
140
|
+
non_scalar_shapes = {a.shape for a in arrays if not self._is_scalar_2d(a)}
|
|
141
|
+
|
|
142
|
+
if len(non_scalar_shapes) == 0:
|
|
143
|
+
return (1, 1)
|
|
144
|
+
|
|
145
|
+
if len(non_scalar_shapes) == 1:
|
|
146
|
+
return next(iter(non_scalar_shapes))
|
|
147
|
+
|
|
148
|
+
raise ValueError(
|
|
149
|
+
f"[{self.name}] Inconsistent input shapes for Sum: "
|
|
150
|
+
f"{[a.shape for a in arrays]}. All non-scalar inputs must have the same shape."
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
# ------------------------------------------------------------------
|
|
154
|
+
def _broadcast_scalar_only(self, arr: np.ndarray, target_shape: tuple[int, int], input_name: str) -> np.ndarray:
|
|
155
|
+
"""
|
|
156
|
+
Broadcast only scalar (1,1) to target_shape.
|
|
157
|
+
Non-scalar must match target_shape exactly.
|
|
158
|
+
"""
|
|
159
|
+
if self._is_scalar_2d(arr):
|
|
160
|
+
if target_shape == (1, 1):
|
|
161
|
+
return arr.astype(float, copy=False)
|
|
162
|
+
return np.full(target_shape, float(arr[0, 0]), dtype=float)
|
|
163
|
+
|
|
164
|
+
if arr.shape != target_shape:
|
|
165
|
+
raise ValueError(
|
|
166
|
+
f"[{self.name}] Input '{input_name}' shape {arr.shape} is incompatible with target shape {target_shape}. "
|
|
167
|
+
f"Only scalar (1,1) inputs can be broadcast."
|
|
168
|
+
)
|
|
169
|
+
return arr.astype(float, copy=False)
|
|
170
|
+
|
|
171
|
+
# ------------------------------------------------------------------
|
|
172
|
+
def _compute_output(self, prevalidated_arrays: list[np.ndarray] | None = None) -> np.ndarray:
|
|
173
|
+
"""
|
|
174
|
+
Compute signed element-wise sum with strict scalar-only broadcast.
|
|
175
|
+
"""
|
|
176
|
+
if prevalidated_arrays is None:
|
|
177
|
+
arrays = [np.asarray(self.inputs[f"in{i+1}"], dtype=float) for i in range(self.num_inputs)]
|
|
178
|
+
else:
|
|
179
|
+
arrays = prevalidated_arrays
|
|
180
|
+
|
|
181
|
+
target_shape = self._resolve_common_shape(arrays)
|
|
182
|
+
|
|
183
|
+
total = np.zeros(target_shape, dtype=float)
|
|
184
|
+
for i, (s, a) in enumerate(zip(self.signs, arrays), start=1):
|
|
185
|
+
a2 = self._broadcast_scalar_only(a, target_shape, input_name=f"in{i}")
|
|
186
|
+
total += s * a2
|
|
187
|
+
|
|
188
|
+
return total
|