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,141 @@
|
|
|
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 ZeroOrderHold(Block):
|
|
27
|
+
"""
|
|
28
|
+
Zero-Order Hold (ZOH) block.
|
|
29
|
+
|
|
30
|
+
Summary:
|
|
31
|
+
Samples the input signal at discrete instants and holds the sampled
|
|
32
|
+
value constant between sampling instants.
|
|
33
|
+
|
|
34
|
+
Parameters:
|
|
35
|
+
sample_time : float
|
|
36
|
+
Sampling period Ts (> 0).
|
|
37
|
+
|
|
38
|
+
Inputs:
|
|
39
|
+
in : array (m,n)
|
|
40
|
+
Input signal (must be 2D).
|
|
41
|
+
|
|
42
|
+
Outputs:
|
|
43
|
+
out : array (m,n)
|
|
44
|
+
Held output signal.
|
|
45
|
+
|
|
46
|
+
Notes:
|
|
47
|
+
- Stateful block.
|
|
48
|
+
- Direct feedthrough (output depends on current u only at sampling instants).
|
|
49
|
+
- Shape is frozen after first resolution.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
direct_feedthrough = True
|
|
53
|
+
|
|
54
|
+
def __init__(self, name: str, sample_time: float):
|
|
55
|
+
super().__init__(name, sample_time)
|
|
56
|
+
|
|
57
|
+
if not isinstance(sample_time, (float, int)) or float(sample_time) <= 0.0:
|
|
58
|
+
raise ValueError(f"[{self.name}] sample_time must be > 0.")
|
|
59
|
+
|
|
60
|
+
self.sample_time = float(sample_time)
|
|
61
|
+
self.EPS = 1e-12
|
|
62
|
+
|
|
63
|
+
self.inputs["in"] = None
|
|
64
|
+
self.outputs["out"] = None
|
|
65
|
+
|
|
66
|
+
self.state["y"] = None
|
|
67
|
+
self.next_state["y"] = None
|
|
68
|
+
self.state["t_last"] = None
|
|
69
|
+
self.next_state["t_last"] = None
|
|
70
|
+
|
|
71
|
+
self._resolved_shape: tuple[int, int] | None = None
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
# --------------------------------------------------------------------------
|
|
75
|
+
# Public methods
|
|
76
|
+
# --------------------------------------------------------------------------
|
|
77
|
+
def initialize(self, t0: float):
|
|
78
|
+
u = self.inputs["in"]
|
|
79
|
+
if u is None:
|
|
80
|
+
raise RuntimeError(f"[{self.name}] Input 'in' is None at initialization.")
|
|
81
|
+
|
|
82
|
+
u = self._to_2d_array("input", u)
|
|
83
|
+
self._ensure_shape(u)
|
|
84
|
+
|
|
85
|
+
y0 = u.copy()
|
|
86
|
+
self.state["y"] = y0
|
|
87
|
+
self.state["t_last"] = float(t0)
|
|
88
|
+
|
|
89
|
+
self.next_state["y"] = y0.copy()
|
|
90
|
+
self.next_state["t_last"] = float(t0)
|
|
91
|
+
|
|
92
|
+
self.outputs["out"] = y0.copy()
|
|
93
|
+
|
|
94
|
+
# ------------------------------------------------------------------
|
|
95
|
+
def output_update(self, t: float, dt: float):
|
|
96
|
+
u = self.inputs["in"]
|
|
97
|
+
if u is None:
|
|
98
|
+
raise RuntimeError(f"[{self.name}] Input 'in' is None.")
|
|
99
|
+
|
|
100
|
+
u = self._to_2d_array("input", u)
|
|
101
|
+
self._ensure_shape(u)
|
|
102
|
+
|
|
103
|
+
t_last = self.state["t_last"]
|
|
104
|
+
if t_last is None:
|
|
105
|
+
raise RuntimeError(f"[{self.name}] ZOH not initialized (t_last is None).")
|
|
106
|
+
|
|
107
|
+
# Sample if enough time elapsed
|
|
108
|
+
if (t - t_last) >= self.sample_time - self.EPS:
|
|
109
|
+
self.outputs["out"] = u.copy()
|
|
110
|
+
else:
|
|
111
|
+
self.outputs["out"] = self.state["y"].copy()
|
|
112
|
+
|
|
113
|
+
# ------------------------------------------------------------------
|
|
114
|
+
def state_update(self, t: float, dt: float):
|
|
115
|
+
t_last = self.state["t_last"]
|
|
116
|
+
if t_last is None:
|
|
117
|
+
raise RuntimeError(f"[{self.name}] ZOH not initialized (t_last is None).")
|
|
118
|
+
|
|
119
|
+
if (t - t_last) >= self.sample_time - self.EPS:
|
|
120
|
+
self.next_state["y"] = self.outputs["out"].copy()
|
|
121
|
+
self.next_state["t_last"] = float(t)
|
|
122
|
+
else:
|
|
123
|
+
self.next_state["y"] = self.state["y"].copy()
|
|
124
|
+
self.next_state["t_last"] = float(t_last)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# --------------------------------------------------------------------------
|
|
128
|
+
# Private methods
|
|
129
|
+
# --------------------------------------------------------------------------
|
|
130
|
+
def _ensure_shape(self, u: np.ndarray) -> None:
|
|
131
|
+
if u.ndim != 2:
|
|
132
|
+
raise ValueError(
|
|
133
|
+
f"[{self.name}] Input 'in' must be a 2D array. Got ndim={u.ndim} with shape {u.shape}."
|
|
134
|
+
)
|
|
135
|
+
if self._resolved_shape is None:
|
|
136
|
+
self._resolved_shape = u.shape
|
|
137
|
+
return
|
|
138
|
+
if u.shape != self._resolved_shape:
|
|
139
|
+
raise ValueError(
|
|
140
|
+
f"[{self.name}] Input 'in' shape changed: expected {self._resolved_shape}, got {u.shape}."
|
|
141
|
+
)
|
|
@@ -0,0 +1,23 @@
|
|
|
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
|
+
from pySimBlocks.blocks.optimizers.quadratic_program import QuadraticProgram
|
|
22
|
+
|
|
23
|
+
__all__ = ["QuadraticProgram"]
|
|
@@ -0,0 +1,326 @@
|
|
|
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 qpsolvers import Problem, solve_problem, available_solvers
|
|
23
|
+
|
|
24
|
+
from pySimBlocks.core.block import Block
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class QuadraticProgram(Block):
|
|
28
|
+
"""
|
|
29
|
+
General time-varying quadratic program solver.
|
|
30
|
+
|
|
31
|
+
Summary:
|
|
32
|
+
Solves at each time step the quadratic program:
|
|
33
|
+
|
|
34
|
+
minimize 1/2 x^T P x + q^T x
|
|
35
|
+
subject to G x <= h
|
|
36
|
+
A x = b
|
|
37
|
+
lb <= x <= ub
|
|
38
|
+
|
|
39
|
+
All problem data are provided as inputs and may vary with time.
|
|
40
|
+
Constraints may be omitted by providing None.
|
|
41
|
+
|
|
42
|
+
Parameters:
|
|
43
|
+
name: str
|
|
44
|
+
Block name.
|
|
45
|
+
solver: str
|
|
46
|
+
QP solver name (default: "clarabel").
|
|
47
|
+
|
|
48
|
+
I/O:
|
|
49
|
+
Inputs:
|
|
50
|
+
P: array (n,n)
|
|
51
|
+
Quadratic cost matrix.
|
|
52
|
+
q: array (n,) or (n,1)
|
|
53
|
+
Linear cost vector.
|
|
54
|
+
G: array (m,n) or None
|
|
55
|
+
Inequality constraint matrix.
|
|
56
|
+
h: array (m,) or (m,1) or None
|
|
57
|
+
Inequality constraint vector.
|
|
58
|
+
A: array (p,n) or None
|
|
59
|
+
Equality constraint matrix.
|
|
60
|
+
b: array (p,) or (p,1) or None
|
|
61
|
+
Equality constraint vector.
|
|
62
|
+
lb: array (n,) or (n,1) or None
|
|
63
|
+
Lower bound on x. (No scalar broadcast.)
|
|
64
|
+
ub: array (n,) or (n,1) or None
|
|
65
|
+
Upper bound on x. (No scalar broadcast.)
|
|
66
|
+
|
|
67
|
+
Outputs:
|
|
68
|
+
x: array (n,1)
|
|
69
|
+
Optimal solution (or zeros on failure).
|
|
70
|
+
status: array (1,1)
|
|
71
|
+
Solver status:
|
|
72
|
+
0 = optimal
|
|
73
|
+
1 = infeasible / no solution
|
|
74
|
+
2 = solver error
|
|
75
|
+
3 = input error
|
|
76
|
+
cost: array (1,1)
|
|
77
|
+
Optimal cost value (NaN on failure).
|
|
78
|
+
"""
|
|
79
|
+
|
|
80
|
+
def __init__(self, name: str, solver: str = "clarabel"):
|
|
81
|
+
super().__init__(name)
|
|
82
|
+
|
|
83
|
+
self._size: int | None = None
|
|
84
|
+
|
|
85
|
+
if solver not in available_solvers:
|
|
86
|
+
raise ValueError(
|
|
87
|
+
f"Solver '{solver}' is not available. Available solvers: {available_solvers}"
|
|
88
|
+
)
|
|
89
|
+
self.solver = solver
|
|
90
|
+
|
|
91
|
+
self.inputs = {
|
|
92
|
+
"P": None,
|
|
93
|
+
"q": None,
|
|
94
|
+
"G": None,
|
|
95
|
+
"h": None,
|
|
96
|
+
"A": None,
|
|
97
|
+
"b": None,
|
|
98
|
+
"lb": None,
|
|
99
|
+
"ub": None,
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
self.outputs = {
|
|
103
|
+
"x": None,
|
|
104
|
+
"status": None,
|
|
105
|
+
"cost": None,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
self.state = {}
|
|
109
|
+
self.next_state = {}
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
# --------------------------------------------------------------------------
|
|
113
|
+
# Public methods
|
|
114
|
+
# --------------------------------------------------------------------------
|
|
115
|
+
def initialize(self, t0: float):
|
|
116
|
+
self.outputs["x"] = np.zeros((1, 1))
|
|
117
|
+
self.outputs["status"] = np.array([[2]])
|
|
118
|
+
self.outputs["cost"] = np.array([[np.nan]])
|
|
119
|
+
|
|
120
|
+
# ------------------------------------------------------------------
|
|
121
|
+
def output_update(self, t: float, dt: float):
|
|
122
|
+
# --- fetch raw
|
|
123
|
+
P_raw = self.inputs.get("P", None)
|
|
124
|
+
q_raw = self.inputs.get("q", None)
|
|
125
|
+
G_raw = self.inputs.get("G", None)
|
|
126
|
+
h_raw = self.inputs.get("h", None)
|
|
127
|
+
A_raw = self.inputs.get("A", None)
|
|
128
|
+
b_raw = self.inputs.get("b", None)
|
|
129
|
+
lb_raw = self.inputs.get("lb", None)
|
|
130
|
+
ub_raw = self.inputs.get("ub", None)
|
|
131
|
+
|
|
132
|
+
# --- normalize types/shapes (numpy)
|
|
133
|
+
try:
|
|
134
|
+
P = self._as_matrix(P_raw) if P_raw is not None else None
|
|
135
|
+
q = self._as_vector(q_raw) if q_raw is not None else None
|
|
136
|
+
|
|
137
|
+
G = self._as_matrix(G_raw) if G_raw is not None else None
|
|
138
|
+
h = self._as_vector(h_raw) if h_raw is not None else None
|
|
139
|
+
|
|
140
|
+
A = self._as_matrix(A_raw) if A_raw is not None else None
|
|
141
|
+
b = self._as_vector(b_raw) if b_raw is not None else None
|
|
142
|
+
|
|
143
|
+
lb = self._as_vector(lb_raw) if lb_raw is not None else None
|
|
144
|
+
ub = self._as_vector(ub_raw) if ub_raw is not None else None
|
|
145
|
+
|
|
146
|
+
self._check_needed_input(P, q, G, h, A, b)
|
|
147
|
+
self._check_size_compatibility(P, q, G, h, A, b, lb, ub)
|
|
148
|
+
|
|
149
|
+
except (ValueError, TypeError):
|
|
150
|
+
self._set_failure(status=3)
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
# Ensure output placeholder x matches resolved size (even before solving)
|
|
154
|
+
self._ensure_output_x_size()
|
|
155
|
+
|
|
156
|
+
# --- build & solve
|
|
157
|
+
try:
|
|
158
|
+
problem = Problem(P, q, G, h, A, b, lb, ub)
|
|
159
|
+
sol = solve_problem(problem, solver=self.solver)
|
|
160
|
+
|
|
161
|
+
if sol is None or getattr(sol, "x", None) is None:
|
|
162
|
+
self._set_failure(status=1)
|
|
163
|
+
return
|
|
164
|
+
|
|
165
|
+
x = np.asarray(sol.x, dtype=float).reshape(-1, 1)
|
|
166
|
+
if x.shape != (self._size, 1):
|
|
167
|
+
raise RuntimeError(
|
|
168
|
+
f"[{self.name}] Solver returned x with shape {x.shape}, expected ({self._size},1)."
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
# cost = 1/2 x^T P x + q^T x
|
|
172
|
+
cost = 0.5 * float(x.T @ P @ x) + float(q.reshape(1, -1) @ x)
|
|
173
|
+
|
|
174
|
+
self.outputs["x"] = x
|
|
175
|
+
self.outputs["status"] = np.array([[0]])
|
|
176
|
+
self.outputs["cost"] = np.array([[cost]])
|
|
177
|
+
|
|
178
|
+
except Exception:
|
|
179
|
+
self._set_failure(status=2)
|
|
180
|
+
|
|
181
|
+
# ------------------------------------------------------------------
|
|
182
|
+
def state_update(self, t: float, dt: float):
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
# --------------------------------------------------------------------------
|
|
187
|
+
# Private methods
|
|
188
|
+
# --------------------------------------------------------------------------
|
|
189
|
+
@staticmethod
|
|
190
|
+
def _check_needed_input(P, q, G, h, A, b):
|
|
191
|
+
if P is None:
|
|
192
|
+
raise ValueError("Missing required QP input 'P'.")
|
|
193
|
+
if q is None:
|
|
194
|
+
raise ValueError("Missing required QP input 'q'.")
|
|
195
|
+
|
|
196
|
+
# paired constraints
|
|
197
|
+
if (G is None) != (h is None):
|
|
198
|
+
raise ValueError("Inequality constraints G and h must both be provided or both be None.")
|
|
199
|
+
if (A is None) != (b is None):
|
|
200
|
+
raise ValueError("Equality constraints A and b must both be provided or both be None.")
|
|
201
|
+
|
|
202
|
+
# ------------------------------------------------------------------
|
|
203
|
+
@staticmethod
|
|
204
|
+
def _as_matrix(value) -> np.ndarray:
|
|
205
|
+
arr = np.asarray(value, dtype=float)
|
|
206
|
+
if arr.ndim != 2:
|
|
207
|
+
raise ValueError(f"Matrix input must be 2D. Got shape {arr.shape}.")
|
|
208
|
+
return arr
|
|
209
|
+
|
|
210
|
+
# ------------------------------------------------------------------
|
|
211
|
+
@staticmethod
|
|
212
|
+
def _as_vector(value) -> np.ndarray:
|
|
213
|
+
"""
|
|
214
|
+
Convert input to a strict 1D vector (n,).
|
|
215
|
+
Accepts:
|
|
216
|
+
- (n,) -> ok
|
|
217
|
+
- (n,1) -> flatten
|
|
218
|
+
Rejects:
|
|
219
|
+
- scalar
|
|
220
|
+
- (1,1)
|
|
221
|
+
- any other shape
|
|
222
|
+
"""
|
|
223
|
+
arr = np.asarray(value, dtype=float)
|
|
224
|
+
|
|
225
|
+
if arr.ndim == 1:
|
|
226
|
+
return arr
|
|
227
|
+
|
|
228
|
+
if arr.ndim == 2 and arr.shape[1] == 1:
|
|
229
|
+
return arr[:, 0]
|
|
230
|
+
|
|
231
|
+
raise ValueError(
|
|
232
|
+
f"Vector input must be shape (n,) or (n,1). Got shape {arr.shape}."
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
# ------------------------------------------------------------------
|
|
236
|
+
def _ensure_output_x_size(self) -> None:
|
|
237
|
+
"""
|
|
238
|
+
Keep outputs['x'] consistent with resolved size if known.
|
|
239
|
+
This prevents dimension-propagation issues when the solver fails.
|
|
240
|
+
"""
|
|
241
|
+
size = self._size if self._size is not None else 1
|
|
242
|
+
|
|
243
|
+
x = self.outputs.get("x", None)
|
|
244
|
+
if x is None:
|
|
245
|
+
self.outputs["x"] = np.zeros((size, 1))
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
x_arr = np.asarray(x)
|
|
249
|
+
if x_arr.shape != (size, 1):
|
|
250
|
+
self.outputs["x"] = np.zeros((size, 1))
|
|
251
|
+
|
|
252
|
+
# ------------------------------------------------------------------
|
|
253
|
+
def _set_failure(self, status: int):
|
|
254
|
+
self.outputs["status"] = np.array([[status]])
|
|
255
|
+
self.outputs["cost"] = np.array([[np.nan]])
|
|
256
|
+
self._ensure_output_x_size()
|
|
257
|
+
|
|
258
|
+
# ------------------------------------------------------------------
|
|
259
|
+
def _check_size_compatibility(self, P, q, G, h, A, b, lb, ub):
|
|
260
|
+
# P defines the size n
|
|
261
|
+
n = P.shape[0]
|
|
262
|
+
|
|
263
|
+
# Freeze size once known
|
|
264
|
+
if self._size is None:
|
|
265
|
+
self._size = n
|
|
266
|
+
elif self._size != n:
|
|
267
|
+
raise ValueError(
|
|
268
|
+
f"[{self.name}] Inconsistent QP size across time steps. "
|
|
269
|
+
f"Previous size: {self._size}, current size: {n}."
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
# P must be square
|
|
273
|
+
if P.ndim != 2 or P.shape[1] != n:
|
|
274
|
+
raise ValueError(f"[{self.name}] Input 'P' must be square, got shape {P.shape}.")
|
|
275
|
+
|
|
276
|
+
# q must be (n,)
|
|
277
|
+
if q.ndim != 1 or q.shape[0] != n:
|
|
278
|
+
raise ValueError(
|
|
279
|
+
f"[{self.name}] Input 'q' has shape {q.shape}. Must be (n,) with n={n}."
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
# Inequality constraints
|
|
283
|
+
if G is not None:
|
|
284
|
+
if h is None:
|
|
285
|
+
raise ValueError(f"[{self.name}] Inequality constraints require both G and h.")
|
|
286
|
+
if G.ndim != 2 or G.shape[1] != n:
|
|
287
|
+
raise ValueError(
|
|
288
|
+
f"[{self.name}] Input 'G' has shape {G.shape}. Must be (m,n) with n={n}."
|
|
289
|
+
)
|
|
290
|
+
m = G.shape[0]
|
|
291
|
+
if h.ndim != 1 or h.shape[0] != m:
|
|
292
|
+
raise ValueError(
|
|
293
|
+
f"[{self.name}] Input 'h' has shape {h.shape}. Must be (m,) with m={m}."
|
|
294
|
+
)
|
|
295
|
+
else:
|
|
296
|
+
if h is not None:
|
|
297
|
+
raise ValueError(f"[{self.name}] Inequality constraints G and h must both be provided or both be None.")
|
|
298
|
+
|
|
299
|
+
# Equality constraints
|
|
300
|
+
if A is not None:
|
|
301
|
+
if b is None:
|
|
302
|
+
raise ValueError(f"[{self.name}] Equality constraints require both A and b.")
|
|
303
|
+
if A.ndim != 2 or A.shape[1] != n:
|
|
304
|
+
raise ValueError(
|
|
305
|
+
f"[{self.name}] Input 'A' has shape {A.shape}. Must be (p,n) with n={n}."
|
|
306
|
+
)
|
|
307
|
+
p = A.shape[0]
|
|
308
|
+
if b.ndim != 1 or b.shape[0] != p:
|
|
309
|
+
raise ValueError(
|
|
310
|
+
f"[{self.name}] Input 'b' has shape {b.shape}. Must be (p,) with p={p}."
|
|
311
|
+
)
|
|
312
|
+
else:
|
|
313
|
+
if b is not None:
|
|
314
|
+
raise ValueError(f"[{self.name}] Equality constraints A and b must both be provided or both be None.")
|
|
315
|
+
|
|
316
|
+
# Bounds (no scalar broadcast)
|
|
317
|
+
if lb is not None:
|
|
318
|
+
if lb.ndim != 1 or lb.shape[0] != n:
|
|
319
|
+
raise ValueError(
|
|
320
|
+
f"[{self.name}] Input 'lb' has shape {lb.shape}. Must be (n,) with n={n}."
|
|
321
|
+
)
|
|
322
|
+
if ub is not None:
|
|
323
|
+
if ub.ndim != 1 or ub.shape[0] != n:
|
|
324
|
+
raise ValueError(
|
|
325
|
+
f"[{self.name}] Input 'ub' has shape {ub.shape}. Must be (n,) with n={n}."
|
|
326
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
from pySimBlocks.blocks.sources.constant import Constant
|
|
22
|
+
from pySimBlocks.blocks.sources.file_source import FileSource
|
|
23
|
+
from pySimBlocks.blocks.sources.function_source import FunctionSource
|
|
24
|
+
from pySimBlocks.blocks.sources.ramp import Ramp
|
|
25
|
+
from pySimBlocks.blocks.sources.step import Step
|
|
26
|
+
from pySimBlocks.blocks.sources.sinusoidal import Sinusoidal
|
|
27
|
+
from pySimBlocks.blocks.sources.white_noise import WhiteNoise
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"Constant",
|
|
31
|
+
"FileSource",
|
|
32
|
+
"FunctionSource",
|
|
33
|
+
"Ramp",
|
|
34
|
+
"Step",
|
|
35
|
+
"Sinusoidal",
|
|
36
|
+
"WhiteNoise",
|
|
37
|
+
]
|
|
@@ -0,0 +1,168 @@
|
|
|
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
|
+
from pySimBlocks.core.block_source import BlockSource
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Chirp(BlockSource):
|
|
27
|
+
"""
|
|
28
|
+
Multi-dimensional chirp signal source (linear or logarithmic).
|
|
29
|
+
|
|
30
|
+
mode:
|
|
31
|
+
"linear" -> linear frequency sweep
|
|
32
|
+
"log" -> logarithmic (exponential) sweep
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
VALID_MODES = {"linear", "log"}
|
|
36
|
+
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
name: str,
|
|
40
|
+
amplitude: ArrayLike,
|
|
41
|
+
f0: ArrayLike,
|
|
42
|
+
f1: ArrayLike,
|
|
43
|
+
duration: ArrayLike,
|
|
44
|
+
start_time: ArrayLike = 0.0,
|
|
45
|
+
offset: ArrayLike = 0.0,
|
|
46
|
+
phase: ArrayLike = 0.0,
|
|
47
|
+
mode: str = "linear",
|
|
48
|
+
sample_time: float | None = None,
|
|
49
|
+
):
|
|
50
|
+
super().__init__(name, sample_time)
|
|
51
|
+
|
|
52
|
+
if mode not in self.VALID_MODES:
|
|
53
|
+
raise ValueError(
|
|
54
|
+
f"[{name}] mode must be one of {self.VALID_MODES}"
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
self.mode = mode
|
|
58
|
+
|
|
59
|
+
A = self._to_2d_array("amplitude", amplitude, dtype=float)
|
|
60
|
+
F0 = self._to_2d_array("f0", f0, dtype=float)
|
|
61
|
+
F1 = self._to_2d_array("f1", f1, dtype=float)
|
|
62
|
+
D = self._to_2d_array("duration", duration, dtype=float)
|
|
63
|
+
T0 = self._to_2d_array("start_time", start_time, dtype=float)
|
|
64
|
+
O = self._to_2d_array("offset", offset, dtype=float)
|
|
65
|
+
P = self._to_2d_array("phase", phase, dtype=float)
|
|
66
|
+
|
|
67
|
+
target_shape = self._resolve_common_shape({
|
|
68
|
+
"amplitude": A,
|
|
69
|
+
"f0": F0,
|
|
70
|
+
"f1": F1,
|
|
71
|
+
"duration": D,
|
|
72
|
+
"start_time": T0,
|
|
73
|
+
"offset": O,
|
|
74
|
+
"phase": P,
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
self.amplitude = self._broadcast_scalar_only("amplitude", A, target_shape)
|
|
78
|
+
self.f0 = self._broadcast_scalar_only("f0", F0, target_shape)
|
|
79
|
+
self.f1 = self._broadcast_scalar_only("f1", F1, target_shape)
|
|
80
|
+
self.duration = self._broadcast_scalar_only("duration", D, target_shape)
|
|
81
|
+
self.start_time = self._broadcast_scalar_only("start_time", T0, target_shape)
|
|
82
|
+
self.offset = self._broadcast_scalar_only("offset", O, target_shape)
|
|
83
|
+
self.phase = self._broadcast_scalar_only("phase", P, target_shape)
|
|
84
|
+
|
|
85
|
+
if np.any(self.duration <= 0.0):
|
|
86
|
+
raise ValueError(f"[{self.name}] duration must be > 0.")
|
|
87
|
+
|
|
88
|
+
if self.mode == "log":
|
|
89
|
+
if np.any(self.f0 <= 0.0) or np.any(self.f1 <= 0.0):
|
|
90
|
+
raise ValueError(
|
|
91
|
+
f"[{self.name}] f0 and f1 must be > 0 for log mode."
|
|
92
|
+
)
|
|
93
|
+
if np.any(self.f0 == self.f1):
|
|
94
|
+
raise ValueError(
|
|
95
|
+
f"[{self.name}] f0 must differ from f1 in log mode."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
self.outputs["out"] = np.zeros(target_shape, dtype=float)
|
|
99
|
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
def initialize(self, t0: float) -> None:
|
|
103
|
+
self._compute_output(t0)
|
|
104
|
+
|
|
105
|
+
# ------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def output_update(self, t: float, dt: float) -> None:
|
|
108
|
+
self._compute_output(t)
|
|
109
|
+
|
|
110
|
+
# ------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def _compute_output(self, t: float) -> None:
|
|
113
|
+
|
|
114
|
+
tau = np.maximum(0.0, t - self.start_time)
|
|
115
|
+
tau_clip = np.minimum(tau, self.duration)
|
|
116
|
+
|
|
117
|
+
if self.mode == "linear":
|
|
118
|
+
phi = self._linear_phase(tau, tau_clip)
|
|
119
|
+
|
|
120
|
+
else: # log
|
|
121
|
+
phi = self._log_phase(tau, tau_clip)
|
|
122
|
+
|
|
123
|
+
self.outputs["out"] = (
|
|
124
|
+
self.amplitude * np.sin(phi) + self.offset
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# ------------------------------------------------------------------
|
|
128
|
+
|
|
129
|
+
def _linear_phase(self, tau, tau_clip):
|
|
130
|
+
|
|
131
|
+
k = (self.f1 - self.f0) / self.duration
|
|
132
|
+
|
|
133
|
+
phi_sweep = (
|
|
134
|
+
2.0 * np.pi *
|
|
135
|
+
(self.f0 * tau_clip + 0.5 * k * tau_clip * tau_clip)
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
extra = (
|
|
139
|
+
2.0 * np.pi *
|
|
140
|
+
self.f1 *
|
|
141
|
+
np.maximum(0.0, tau - self.duration)
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
return phi_sweep + extra + self.phase
|
|
145
|
+
|
|
146
|
+
# ------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
def _log_phase(self, tau, tau_clip):
|
|
149
|
+
|
|
150
|
+
ratio = self.f1 / self.f0
|
|
151
|
+
log_ratio = np.log(ratio)
|
|
152
|
+
|
|
153
|
+
coeff = 2.0 * np.pi * self.f0 * self.duration / log_ratio
|
|
154
|
+
|
|
155
|
+
phi_sweep = coeff * (
|
|
156
|
+
np.power(ratio, tau_clip / self.duration) - 1.0
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
# phase continuity after duration
|
|
160
|
+
phi_end = coeff * (ratio - 1.0)
|
|
161
|
+
|
|
162
|
+
extra = (
|
|
163
|
+
2.0 * np.pi *
|
|
164
|
+
self.f1 *
|
|
165
|
+
np.maximum(0.0, tau - self.duration)
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
return phi_sweep + extra + self.phase
|