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,233 @@
|
|
|
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 os
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Literal
|
|
26
|
+
|
|
27
|
+
from PySide6.QtWidgets import QFormLayout, QLabel, QLineEdit, QPushButton
|
|
28
|
+
|
|
29
|
+
from pySimBlocks.gui.blocks.block_meta import BlockMeta, ParameterMeta
|
|
30
|
+
from pySimBlocks.gui.blocks.port_meta import PortMeta
|
|
31
|
+
from pySimBlocks.gui.models import BlockInstance, PortInstance
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class NonLinearStateSpaceMeta(BlockMeta):
|
|
35
|
+
|
|
36
|
+
def __init__(self):
|
|
37
|
+
self.name = "Non Linear State Space"
|
|
38
|
+
self.category = "systems"
|
|
39
|
+
self.type = "non_linear_state_space"
|
|
40
|
+
self.summary = "User-defined non linear state space function implemented in Python."
|
|
41
|
+
self.description = (
|
|
42
|
+
"This block evaluates a user-provided Python function of the form:\n"
|
|
43
|
+
" x+ = f(t, dt, x, u1, u2, ...)\n"
|
|
44
|
+
" y = g(t, dt, x)\n"
|
|
45
|
+
"The function is loaded from an external Python file and executed at each\n"
|
|
46
|
+
"simulation step. Input and output ports are explicitly declared via\n"
|
|
47
|
+
"`input_keys` and `output_keys`.\n"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
self.parameters = [
|
|
51
|
+
ParameterMeta(
|
|
52
|
+
name="file_path",
|
|
53
|
+
type="string",
|
|
54
|
+
required=True,
|
|
55
|
+
description="Path to the Python file containing the functions relative to the project.yaml file."
|
|
56
|
+
),
|
|
57
|
+
ParameterMeta(
|
|
58
|
+
name="state_function_name",
|
|
59
|
+
type="string",
|
|
60
|
+
required=True,
|
|
61
|
+
description="Name of the state function to call inside the Python file."
|
|
62
|
+
),
|
|
63
|
+
ParameterMeta(
|
|
64
|
+
name="output_function_name",
|
|
65
|
+
type="string",
|
|
66
|
+
required=True,
|
|
67
|
+
description="Name of the output function to call inside the Python file."
|
|
68
|
+
),
|
|
69
|
+
ParameterMeta(
|
|
70
|
+
name="input_keys",
|
|
71
|
+
type="list[string]",
|
|
72
|
+
required=True,
|
|
73
|
+
description="List of input port names. These must match the function arguments after (t, dt)."
|
|
74
|
+
),
|
|
75
|
+
ParameterMeta(
|
|
76
|
+
name="output_keys",
|
|
77
|
+
type="list[string]",
|
|
78
|
+
required=True,
|
|
79
|
+
description="List of output port names. The function must return a dict with exactly these keys."
|
|
80
|
+
),
|
|
81
|
+
ParameterMeta(
|
|
82
|
+
name="x0",
|
|
83
|
+
type="vector",
|
|
84
|
+
required=True,
|
|
85
|
+
description="Initial state vector."
|
|
86
|
+
),
|
|
87
|
+
ParameterMeta(
|
|
88
|
+
name="sample_time",
|
|
89
|
+
type="float",
|
|
90
|
+
description="Optional execution period of the block."
|
|
91
|
+
)
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
self.inputs = [
|
|
95
|
+
PortMeta(
|
|
96
|
+
name="in",
|
|
97
|
+
display_as="",
|
|
98
|
+
shape=[] # shapes are dynamic
|
|
99
|
+
)
|
|
100
|
+
]
|
|
101
|
+
|
|
102
|
+
self.outputs = [
|
|
103
|
+
PortMeta(
|
|
104
|
+
name="out",
|
|
105
|
+
display_as="",
|
|
106
|
+
shape=[] # shapes are dynamic
|
|
107
|
+
)
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
# --------------------------------------------------------------------------
|
|
111
|
+
# Port resolution
|
|
112
|
+
# --------------------------------------------------------------------------
|
|
113
|
+
def resolve_port_group(
|
|
114
|
+
self,
|
|
115
|
+
port_meta: PortMeta,
|
|
116
|
+
direction: Literal['input', 'output'],
|
|
117
|
+
instance: "BlockInstance"
|
|
118
|
+
) -> list["PortInstance"]:
|
|
119
|
+
if direction == 'input' and port_meta.name == 'in':
|
|
120
|
+
input_keys = instance.parameters.get("input_keys", [])
|
|
121
|
+
if input_keys is None:
|
|
122
|
+
return []
|
|
123
|
+
return [
|
|
124
|
+
PortInstance(
|
|
125
|
+
name=key,
|
|
126
|
+
display_as=key,
|
|
127
|
+
direction="input",
|
|
128
|
+
block=instance
|
|
129
|
+
) for key in input_keys
|
|
130
|
+
]
|
|
131
|
+
elif direction == 'output' and port_meta.name == 'out':
|
|
132
|
+
output_keys = instance.parameters.get("output_keys", [])
|
|
133
|
+
if output_keys is None:
|
|
134
|
+
return []
|
|
135
|
+
return [
|
|
136
|
+
PortInstance(
|
|
137
|
+
name=key,
|
|
138
|
+
display_as=key,
|
|
139
|
+
direction="output",
|
|
140
|
+
block=instance
|
|
141
|
+
) for key in output_keys
|
|
142
|
+
]
|
|
143
|
+
return super().resolve_port_group(port_meta, direction, instance)
|
|
144
|
+
|
|
145
|
+
# --------------------------------------------------------------------------
|
|
146
|
+
# Dialog methods
|
|
147
|
+
# --------------------------------------------------------------------------
|
|
148
|
+
def build_param(
|
|
149
|
+
self,
|
|
150
|
+
session,
|
|
151
|
+
form: QFormLayout,
|
|
152
|
+
readonly: bool = False,
|
|
153
|
+
):
|
|
154
|
+
name_edit = QLineEdit(session.instance.name)
|
|
155
|
+
name_edit.textChanged.connect(
|
|
156
|
+
lambda val: self._on_param_changed(val, "name", session, readonly)
|
|
157
|
+
)
|
|
158
|
+
form.addRow(QLabel("Block name:"), name_edit)
|
|
159
|
+
if readonly:
|
|
160
|
+
name_edit.setReadOnly(True)
|
|
161
|
+
session.name_edit = name_edit
|
|
162
|
+
|
|
163
|
+
for pmeta in self.parameters:
|
|
164
|
+
if pmeta.name == "file_path":
|
|
165
|
+
self.build_file_param_row(
|
|
166
|
+
session,
|
|
167
|
+
form,
|
|
168
|
+
pmeta,
|
|
169
|
+
readonly=readonly,
|
|
170
|
+
file_filter="Python files (*.py);;All files (*)",
|
|
171
|
+
)
|
|
172
|
+
continue
|
|
173
|
+
|
|
174
|
+
label, widget = self._create_param_row(session, pmeta, readonly)
|
|
175
|
+
if widget is None:
|
|
176
|
+
continue
|
|
177
|
+
if readonly:
|
|
178
|
+
self._set_readonly_style(widget)
|
|
179
|
+
|
|
180
|
+
form.addRow(label, widget)
|
|
181
|
+
session.param_widgets[pmeta.name] = widget
|
|
182
|
+
session.param_labels[pmeta.name] = label
|
|
183
|
+
|
|
184
|
+
# ------------------------------------------------------
|
|
185
|
+
def build_post_param(self, session, form: QFormLayout, readonly: bool = False):
|
|
186
|
+
open_btn = QPushButton("Open file")
|
|
187
|
+
open_btn.clicked.connect(lambda: self._open_file_from_session(session))
|
|
188
|
+
form.addRow(QLabel(""), open_btn)
|
|
189
|
+
session.open_file_btn = open_btn
|
|
190
|
+
self._refresh_open_button_state(session)
|
|
191
|
+
|
|
192
|
+
# ------------------------------------------------------
|
|
193
|
+
def refresh_form(self, session):
|
|
194
|
+
super().refresh_form(session)
|
|
195
|
+
self._refresh_open_button_state(session)
|
|
196
|
+
|
|
197
|
+
# ------------------------------------------------------
|
|
198
|
+
def _resolve_file_path(self, session) -> Path | None:
|
|
199
|
+
raw = session.local_params.get("file_path")
|
|
200
|
+
if not raw:
|
|
201
|
+
return None
|
|
202
|
+
|
|
203
|
+
path = Path(str(raw)).expanduser()
|
|
204
|
+
if not path.is_absolute() and session.project_dir is not None:
|
|
205
|
+
path = (session.project_dir / path).resolve()
|
|
206
|
+
return path
|
|
207
|
+
|
|
208
|
+
# ------------------------------------------------------
|
|
209
|
+
def _refresh_open_button_state(self, session) -> None:
|
|
210
|
+
btn = getattr(session, "open_file_btn", None)
|
|
211
|
+
if btn is None:
|
|
212
|
+
return
|
|
213
|
+
|
|
214
|
+
target = self._resolve_file_path(session)
|
|
215
|
+
exists = target is not None and target.is_file()
|
|
216
|
+
btn.setEnabled(exists)
|
|
217
|
+
if exists:
|
|
218
|
+
btn.setToolTip(str(target))
|
|
219
|
+
else:
|
|
220
|
+
btn.setToolTip("Set a valid existing file_path to open the file.")
|
|
221
|
+
|
|
222
|
+
# ------------------------------------------------------
|
|
223
|
+
def _open_file_from_session(self, session) -> None:
|
|
224
|
+
target = self._resolve_file_path(session)
|
|
225
|
+
if target is None or not target.is_file():
|
|
226
|
+
return
|
|
227
|
+
|
|
228
|
+
if sys.platform.startswith("darwin"):
|
|
229
|
+
subprocess.Popen(["open", str(target)])
|
|
230
|
+
elif os.name == "nt":
|
|
231
|
+
os.startfile(str(target))
|
|
232
|
+
else:
|
|
233
|
+
subprocess.Popen(["xdg-open", str(target)])
|
|
@@ -0,0 +1,109 @@
|
|
|
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.gui.blocks.block_meta import BlockMeta
|
|
22
|
+
from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
|
|
23
|
+
from pySimBlocks.gui.blocks.port_meta import PortMeta
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class PolytopicStateSpaceMeta(BlockMeta):
|
|
27
|
+
|
|
28
|
+
def __init__(self):
|
|
29
|
+
self.name = "PolytopicStateSpace"
|
|
30
|
+
self.category = "systems"
|
|
31
|
+
self.type = "polytopic_state_space"
|
|
32
|
+
self.summary = "Discrete-time polytopic state-space system."
|
|
33
|
+
self.description = (
|
|
34
|
+
"Implements the discrete-time polytopic state-space equations:\n"
|
|
35
|
+
"$$\n"
|
|
36
|
+
"x[k+1] = A\\,\\mathrm{kron}(w[k], x[k]) + B\\,\\mathrm{kron}(w[k], u[k])\n"
|
|
37
|
+
"$$\n"
|
|
38
|
+
"$$\n"
|
|
39
|
+
"y[k] = C x[k]\n"
|
|
40
|
+
"$$\n"
|
|
41
|
+
"with $w[k]$ the vertex weight vector.\n"
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
self.parameters = [
|
|
45
|
+
ParameterMeta(
|
|
46
|
+
name="A",
|
|
47
|
+
type="matrix",
|
|
48
|
+
required=True,
|
|
49
|
+
autofill=True,
|
|
50
|
+
default=[[1.0, 0.0]],
|
|
51
|
+
description="Stacked polytopic state matrix of shape (nx, r*nx): [A1 ... Ar]."
|
|
52
|
+
),
|
|
53
|
+
ParameterMeta(
|
|
54
|
+
name="B",
|
|
55
|
+
type="matrix",
|
|
56
|
+
required=True,
|
|
57
|
+
autofill=True,
|
|
58
|
+
default=[[1.0, 1.0]],
|
|
59
|
+
description="Stacked polytopic input matrix of shape (nx, r*nu): [B1 ... Br]."
|
|
60
|
+
),
|
|
61
|
+
ParameterMeta(
|
|
62
|
+
name="C",
|
|
63
|
+
type="matrix",
|
|
64
|
+
required=True,
|
|
65
|
+
autofill=True,
|
|
66
|
+
default=[[1.0]],
|
|
67
|
+
description="Output matrix of shape (ny, nx)."
|
|
68
|
+
),
|
|
69
|
+
ParameterMeta(
|
|
70
|
+
name="x0",
|
|
71
|
+
type="vector",
|
|
72
|
+
description="Initial state vector."
|
|
73
|
+
),
|
|
74
|
+
ParameterMeta(
|
|
75
|
+
name="sample_time",
|
|
76
|
+
type="float",
|
|
77
|
+
description="Block execution period."
|
|
78
|
+
)
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
self.inputs = [
|
|
82
|
+
PortMeta(
|
|
83
|
+
name="w",
|
|
84
|
+
display_as="w",
|
|
85
|
+
shape=["r", 1],
|
|
86
|
+
description="Vertex weights vector."
|
|
87
|
+
),
|
|
88
|
+
PortMeta(
|
|
89
|
+
name="u",
|
|
90
|
+
display_as="u",
|
|
91
|
+
shape=["nu", 1],
|
|
92
|
+
description="Input vector."
|
|
93
|
+
)
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
self.outputs = [
|
|
97
|
+
PortMeta(
|
|
98
|
+
name="x",
|
|
99
|
+
display_as="x",
|
|
100
|
+
shape=["nx", 1],
|
|
101
|
+
description="State vector."
|
|
102
|
+
),
|
|
103
|
+
PortMeta(
|
|
104
|
+
name="y",
|
|
105
|
+
display_as="y",
|
|
106
|
+
shape=["ny", 1],
|
|
107
|
+
description="Output vector."
|
|
108
|
+
)
|
|
109
|
+
]
|
|
@@ -0,0 +1,221 @@
|
|
|
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 os
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from typing import Literal
|
|
26
|
+
|
|
27
|
+
from PySide6.QtWidgets import QFormLayout, QLabel, QLineEdit, QPushButton
|
|
28
|
+
|
|
29
|
+
from pySimBlocks.gui.blocks.block_meta import BlockMeta
|
|
30
|
+
from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
|
|
31
|
+
from pySimBlocks.gui.blocks.port_meta import PortMeta
|
|
32
|
+
from pySimBlocks.gui.models import BlockInstance, PortInstance
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class SofaExchangeIOMeta(BlockMeta):
|
|
36
|
+
|
|
37
|
+
def __init__(self):
|
|
38
|
+
self.name = "SofaExchangeIO"
|
|
39
|
+
self.category = "systems"
|
|
40
|
+
self.type = "sofa_exchange_i_o"
|
|
41
|
+
self.summary = "Interface block for exchanging signals between pySimBlocks and SOFA."
|
|
42
|
+
self.description = (
|
|
43
|
+
"Provides a stateless interface block that exposes dynamic input and output\n"
|
|
44
|
+
"ports to connect a pySimBlocks model with an external SOFA controller.\n"
|
|
45
|
+
"This block does not execute a SOFA simulation."
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self.parameters = [
|
|
49
|
+
ParameterMeta(
|
|
50
|
+
name="scene_file",
|
|
51
|
+
type="string",
|
|
52
|
+
description="Path to the SOFA scene file used for automatic generation relative to project.yaml file."
|
|
53
|
+
),
|
|
54
|
+
ParameterMeta(
|
|
55
|
+
name="input_keys",
|
|
56
|
+
type="list[string]",
|
|
57
|
+
required=True,
|
|
58
|
+
description="List of input keys corresponding to SOFA input ports."
|
|
59
|
+
),
|
|
60
|
+
ParameterMeta(
|
|
61
|
+
name="output_keys",
|
|
62
|
+
type="list[string]",
|
|
63
|
+
required=True,
|
|
64
|
+
description="List of output keys corresponding to SOFA output ports."
|
|
65
|
+
),
|
|
66
|
+
ParameterMeta(
|
|
67
|
+
name="slider_params",
|
|
68
|
+
type="dict",
|
|
69
|
+
description="Dictionary of slider parameters to be modified in the SOFA scene at runtime."
|
|
70
|
+
),
|
|
71
|
+
ParameterMeta(
|
|
72
|
+
name="sample_time",
|
|
73
|
+
type="float"
|
|
74
|
+
)
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
self.inputs = [
|
|
78
|
+
PortMeta(
|
|
79
|
+
name="sofa_inputs",
|
|
80
|
+
display_as="",
|
|
81
|
+
shape=[]
|
|
82
|
+
)
|
|
83
|
+
]
|
|
84
|
+
|
|
85
|
+
self.outputs = [
|
|
86
|
+
PortMeta(
|
|
87
|
+
name="sofa_outputs",
|
|
88
|
+
display_as="",
|
|
89
|
+
shape=[]
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
# --------------------------------------------------------------------------
|
|
94
|
+
# Port Resolution
|
|
95
|
+
# --------------------------------------------------------------------------
|
|
96
|
+
def resolve_port_group(
|
|
97
|
+
self,
|
|
98
|
+
port_meta: PortMeta,
|
|
99
|
+
direction: Literal["input", "output"],
|
|
100
|
+
instance: "BlockInstance"
|
|
101
|
+
) -> list["PortInstance"]:
|
|
102
|
+
|
|
103
|
+
if direction == "input" and port_meta.name == "sofa_inputs":
|
|
104
|
+
keys = instance.parameters.get("input_keys", [])
|
|
105
|
+
if keys is None:
|
|
106
|
+
return []
|
|
107
|
+
return [
|
|
108
|
+
PortInstance(
|
|
109
|
+
name=key,
|
|
110
|
+
display_as=key,
|
|
111
|
+
direction="input",
|
|
112
|
+
block=instance
|
|
113
|
+
)
|
|
114
|
+
for key in keys
|
|
115
|
+
]
|
|
116
|
+
|
|
117
|
+
if direction == "output" and port_meta.name == "sofa_outputs":
|
|
118
|
+
keys = instance.parameters.get("output_keys", [])
|
|
119
|
+
if keys is None:
|
|
120
|
+
return []
|
|
121
|
+
return [
|
|
122
|
+
PortInstance(
|
|
123
|
+
name=key,
|
|
124
|
+
display_as=key,
|
|
125
|
+
direction="output",
|
|
126
|
+
block=instance
|
|
127
|
+
)
|
|
128
|
+
for key in keys
|
|
129
|
+
]
|
|
130
|
+
|
|
131
|
+
return super().resolve_port_group(port_meta, direction, instance)
|
|
132
|
+
|
|
133
|
+
# --------------------------------------------------------------------------
|
|
134
|
+
# Dialog Methods
|
|
135
|
+
# --------------------------------------------------------------------------
|
|
136
|
+
def build_param(
|
|
137
|
+
self,
|
|
138
|
+
session,
|
|
139
|
+
form: QFormLayout,
|
|
140
|
+
readonly: bool = False,
|
|
141
|
+
):
|
|
142
|
+
name_edit = QLineEdit(session.instance.name)
|
|
143
|
+
name_edit.textChanged.connect(
|
|
144
|
+
lambda val: self._on_param_changed(val, "name", session, readonly)
|
|
145
|
+
)
|
|
146
|
+
form.addRow(QLabel("Block name:"), name_edit)
|
|
147
|
+
if readonly:
|
|
148
|
+
name_edit.setReadOnly(True)
|
|
149
|
+
session.name_edit = name_edit
|
|
150
|
+
|
|
151
|
+
for pmeta in self.parameters:
|
|
152
|
+
if pmeta.name == "scene_file":
|
|
153
|
+
self.build_file_param_row(
|
|
154
|
+
session,
|
|
155
|
+
form,
|
|
156
|
+
pmeta,
|
|
157
|
+
readonly=readonly,
|
|
158
|
+
file_filter="SOFA scene files (*.py);;All files (*)",
|
|
159
|
+
)
|
|
160
|
+
continue
|
|
161
|
+
|
|
162
|
+
label, widget = self._create_param_row(session, pmeta, readonly)
|
|
163
|
+
if widget is None:
|
|
164
|
+
continue
|
|
165
|
+
if readonly:
|
|
166
|
+
self._set_readonly_style(widget)
|
|
167
|
+
|
|
168
|
+
form.addRow(label, widget)
|
|
169
|
+
session.param_widgets[pmeta.name] = widget
|
|
170
|
+
session.param_labels[pmeta.name] = label
|
|
171
|
+
|
|
172
|
+
# ------------------------------------------------------
|
|
173
|
+
def build_post_param(self, session, form: QFormLayout, readonly: bool = False):
|
|
174
|
+
open_btn = QPushButton("Open file")
|
|
175
|
+
open_btn.clicked.connect(lambda: self._open_file_from_session(session))
|
|
176
|
+
form.addRow(QLabel(""), open_btn)
|
|
177
|
+
session.open_file_btn = open_btn
|
|
178
|
+
self._refresh_open_button_state(session)
|
|
179
|
+
|
|
180
|
+
# ------------------------------------------------------
|
|
181
|
+
def refresh_form(self, session):
|
|
182
|
+
super().refresh_form(session)
|
|
183
|
+
self._refresh_open_button_state(session)
|
|
184
|
+
|
|
185
|
+
# ------------------------------------------------------
|
|
186
|
+
def _resolve_file_path(self, session) -> Path | None:
|
|
187
|
+
raw = session.local_params.get("scene_file")
|
|
188
|
+
if not raw:
|
|
189
|
+
return None
|
|
190
|
+
|
|
191
|
+
path = Path(str(raw)).expanduser()
|
|
192
|
+
if not path.is_absolute() and session.project_dir is not None:
|
|
193
|
+
path = (session.project_dir / path).resolve()
|
|
194
|
+
return path
|
|
195
|
+
|
|
196
|
+
# ------------------------------------------------------
|
|
197
|
+
def _refresh_open_button_state(self, session) -> None:
|
|
198
|
+
btn = getattr(session, "open_file_btn", None)
|
|
199
|
+
if btn is None:
|
|
200
|
+
return
|
|
201
|
+
|
|
202
|
+
target = self._resolve_file_path(session)
|
|
203
|
+
exists = target is not None and target.is_file()
|
|
204
|
+
btn.setEnabled(exists)
|
|
205
|
+
if exists:
|
|
206
|
+
btn.setToolTip(str(target))
|
|
207
|
+
else:
|
|
208
|
+
btn.setToolTip("Set a valid existing scene_file to open the file.")
|
|
209
|
+
|
|
210
|
+
# ------------------------------------------------------
|
|
211
|
+
def _open_file_from_session(self, session) -> None:
|
|
212
|
+
target = self._resolve_file_path(session)
|
|
213
|
+
if target is None or not target.is_file():
|
|
214
|
+
return
|
|
215
|
+
|
|
216
|
+
if sys.platform.startswith("darwin"):
|
|
217
|
+
subprocess.Popen(["open", str(target)])
|
|
218
|
+
elif os.name == "nt":
|
|
219
|
+
os.startfile(str(target))
|
|
220
|
+
else:
|
|
221
|
+
subprocess.Popen(["xdg-open", str(target)])
|