From a38128c509098aff39711a28dd911829c7dcf497 Mon Sep 17 00:00:00 2001 From: Alessandrini Antoine Date: Sat, 14 Feb 2026 15:54:55 +0100 Subject: [PATCH 1/3] feat(systems): add polytope block & gui & docs --- pySimBlocks/blocks/__init__.py | 3 +- pySimBlocks/blocks/systems/__init__.py | 2 + .../blocks/systems/polytopic_state_space.py | 191 ++++++++++++++++++ .../blocks/systems/polytopic_state_space.md | 72 +++++++ .../blocks/systems/polytopic_state_space.py | 109 ++++++++++ .../project/pySimBlocks_blocks_index.yaml | 3 + .../systems/test_polytopic_state_space.py | 133 ++++++++++++ 7 files changed, 512 insertions(+), 1 deletion(-) create mode 100644 pySimBlocks/blocks/systems/polytopic_state_space.py create mode 100644 pySimBlocks/docs/blocks/systems/polytopic_state_space.md create mode 100644 pySimBlocks/gui/blocks/systems/polytopic_state_space.py create mode 100644 test/blocks/systems/test_polytopic_state_space.py diff --git a/pySimBlocks/blocks/__init__.py b/pySimBlocks/blocks/__init__.py index a2f9bb5..997e188 100644 --- a/pySimBlocks/blocks/__init__.py +++ b/pySimBlocks/blocks/__init__.py @@ -26,7 +26,7 @@ Gain, Mux, Product, RateLimiter, Saturation, Sum, ZeroOrderHold ) from pySimBlocks.blocks.sources import Constant, Ramp, Step, Sinusoidal, WhiteNoise -from pySimBlocks.blocks.systems import LinearStateSpace +from pySimBlocks.blocks.systems import LinearStateSpace, PolytopicStateSpace __all__ = [ "Pid", @@ -56,4 +56,5 @@ "WhiteNoise", "LinearStateSpace", + "PolytopicStateSpace", ] diff --git a/pySimBlocks/blocks/systems/__init__.py b/pySimBlocks/blocks/systems/__init__.py index 5f269cb..e26a04a 100644 --- a/pySimBlocks/blocks/systems/__init__.py +++ b/pySimBlocks/blocks/systems/__init__.py @@ -19,7 +19,9 @@ # ****************************************************************************** from pySimBlocks.blocks.systems.linear_state_space import LinearStateSpace +from pySimBlocks.blocks.systems.polytopic_state_space import PolytopicStateSpace __all__ = [ "LinearStateSpace", + "PolytopicStateSpace", ] diff --git a/pySimBlocks/blocks/systems/polytopic_state_space.py b/pySimBlocks/blocks/systems/polytopic_state_space.py new file mode 100644 index 0000000..da7be8d --- /dev/null +++ b/pySimBlocks/blocks/systems/polytopic_state_space.py @@ -0,0 +1,191 @@ +# ****************************************************************************** +# pySimBlocks +# Copyright (c) 2026 Université de Lille & INRIA +# ****************************************************************************** +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# ****************************************************************************** +# Authors: see Authors.txt +# ****************************************************************************** + +import numpy as np +from numpy.typing import ArrayLike +from pySimBlocks.core.block import Block + + +class PolytopicStateSpace(Block): + """ + Discrete-time polytopic state-space block. + + Summary: + Implements: + x[k+1] = sum_{i=1}^r w_i[k] (A_i x[k] + B_i u[k]) + y[k] = C x[k] + + Parameters (overview): + A : array-like + concatenation of A_i matrices [A_1, A_2, ..., A_r], shape (nx, r*nx). + B : array-like + concatenation of B_i matrices [B_1, B_2, ..., B_r], shape (nx, r*nu). + C : array-like + Output matrix. + x0 : array-like, optional + Initial state vector. + sample_time : float, optional + Block execution period. + + I/O: + Inputs: + u : input vector. + w : vertex weight vector (must sum to 1). + Outputs: + y : output vector. + x : state vector. + + Notes: + - The system is strictly proper (no direct feedthrough). + - The block has internal state. + """ + + direct_feedthrough = False + + def __init__( + self, + name: str, + A: ArrayLike, + B: ArrayLike, + C: ArrayLike, + x0: ArrayLike | None = None, + sample_time: float | None = None, + ): + super().__init__(name, sample_time) + + self.A = np.asarray(A, dtype=float) + self.B = np.asarray(B, dtype=float) + self.C = np.asarray(C, dtype=float) + + if self.A.ndim != 2: + raise ValueError(f"[{self.name}] A must be 2D. Got shape {self.A.shape}.") + if self.B.ndim != 2: + raise ValueError(f"[{self.name}] B must be 2D. Got shape {self.B.shape}.") + if self.C.ndim != 2: + raise ValueError(f"[{self.name}] C must be 2D. Got shape {self.C.shape}.") + + nx = self.A.shape[0] + if nx <= 0: + raise ValueError(f"[{self.name}] A must have at least one row.") + if self.A.shape[1] % nx != 0: + raise ValueError( + f"[{self.name}] A must have shape (nx, r*nx). Got {self.A.shape}." + ) + + r = self.A.shape[1] // nx + if r <= 0: + raise ValueError(f"[{self.name}] Number of vertices r must be >= 1.") + + if self.B.shape[0] != nx: + raise ValueError( + f"[{self.name}] B must have nx rows. A is {self.A.shape}, B is {self.B.shape}." + ) + if self.B.shape[1] % r != 0: + raise ValueError( + f"[{self.name}] B must have shape (nx, r*nu). A gives r={r}, B is {self.B.shape}." + ) + + nu = self.B.shape[1] // r + if nu <= 0: + raise ValueError(f"[{self.name}] Input size nu must be >= 1.") + + if self.C.shape[1] != nx: + raise ValueError( + f"[{self.name}] C must have nx columns. A is {self.A.shape}, C is {self.C.shape}." + ) + + ny = self.C.shape[0] + + self._nx = nx + self._nu = nu + self._ny = ny + self._r = r + + if x0 is None: + x0_arr = np.zeros((nx, 1), dtype=float) + else: + x0_arr = self._to_col_vec("x0", x0, nx) + + self.state["x"] = x0_arr.copy() + self.next_state["x"] = x0_arr.copy() + + self.inputs["w"] = None + self.inputs["u"] = None + self.outputs["x"] = None + self.outputs["y"] = None + + # -------------------------------------------------------------------------- + # Public methods + # -------------------------------------------------------------------------- + def initialize(self, t0: float) -> None: + x = self.state["x"] + self.outputs["x"] = x.copy() + self.outputs["y"] = self.C @ x + self.next_state["x"] = x.copy() + + # ------------------------------------------------------------------ + def output_update(self, t: float, dt: float) -> None: + x = self.state["x"] + self.outputs["x"] = x.copy() + self.outputs["y"] = self.C @ x + + # ------------------------------------------------------------------ + def state_update(self, t: float, dt: float) -> None: + w = self.inputs["w"] + u = self.inputs["u"] + if w is None: + raise RuntimeError(f"[{self.name}] Input 'w' is not connected or not set.") + if u is None: + raise RuntimeError(f"[{self.name}] Input 'u' is not connected or not set.") + + w_vec = self._to_col_vec("w", w, self._r) + if not np.isclose(np.sum(w_vec), 1.0): + raise ValueError(f"[{self.name}] Vertex weights w must sum to 1. Got sum {np.sum(w_vec)}.") + + u_vec = self._to_col_vec("u", u, self._nu) + x = self.state["x"] + + x_next = self.A @ np.kron(w_vec, x) + self.B @ np.kron(w_vec, u_vec) + self.next_state["x"] = x_next + + # -------------------------------------------------------------------------- + # Private methods + # -------------------------------------------------------------------------- + def _to_col_vec(self, name: str, value: ArrayLike, expected_rows: int) -> np.ndarray: + arr = np.asarray(value, dtype=float) + + if arr.ndim == 0: + arr = arr.reshape(1, 1) + elif arr.ndim == 1: + arr = arr.reshape(-1, 1) + elif arr.ndim == 2: + pass + else: + raise ValueError(f"[{self.name}] {name} must be 1D or 2D. Got shape {arr.shape}.") + + if arr.shape[1] != 1: + raise ValueError(f"[{self.name}] {name} must be a column vector (k,1). Got {arr.shape}.") + + if arr.shape[0] != expected_rows: + raise ValueError( + f"[{self.name}] {name} must have shape ({expected_rows},1). Got {arr.shape}." + ) + + return arr diff --git a/pySimBlocks/docs/blocks/systems/polytopic_state_space.md b/pySimBlocks/docs/blocks/systems/polytopic_state_space.md new file mode 100644 index 0000000..55f8a22 --- /dev/null +++ b/pySimBlocks/docs/blocks/systems/polytopic_state_space.md @@ -0,0 +1,72 @@ +# PolytopicStateSpace + +## Summary + +The **PolytopicStateSpace** block implements a discrete-time polytopic state-space model without direct feedthrough. + +--- + +## Mathematical definition + +The system is defined by: + +$$ +x[k+1] = \sum_{i=1}^{r} w_i[k] (A_i x[k] + B_i u[k]) +$$ + +$$ +y[k] = C x[k] +$$ + +where: +- $x[k] \in \mathbb{R}^{nx}$ is the state vector, +- $u[k] \in \mathbb{R}^{nu}$ is the input vector, +- $w[k] \in \mathbb{R}^{r}$ is the vertex weight vector, +- $y[k] \in \mathbb{R}^{ny}$ is the output vector. + +The block expects: +- $A \in \mathbb{R}^{nx \times (r\,nx)}$ as $[A_1 \ \cdots \ A_r]$, +- $B \in \mathbb{R}^{nx \times (r\,nu)}$ as $[B_1 \ \cdots \ B_r]$, +- $C \in \mathbb{R}^{ny \times nx}$. + +--- + +## Parameters + +| Name | Type | Description | Optional | +|------------|-------------|-------------|-------------| +| `A` | 2D array | Stacked state matrix of size (nx, r*nx): `[A1, ..., Ar]`. | False | +| `B` | 2D array | Stacked input matrix of size (nx, r*nu): `[B1, ..., Br]`. | False | +| `C` | 2D array | Output matrix of size (ny, nx). | False | +| `x0` | 1D array | Initial state vector of size (nx,). If omitted, initialized to zero. | True | +| `sample_time` | float | Block sample time. If omitted, the global simulation time step is used. | True | + +--- + +## Inputs + +| Port | Description | +|------|------------| +| `w` | Weight vector of shape (r,1) (or `(r,)` as parameterized array). | +| `u` | Input vector of shape (nu,1). | + +--- + +## Outputs + +| Port | Description | +|------|------------| +| `x` | State vector of shape (nx,1). | +| `y` | Output vector of shape (ny,1). | + +--- + +## Notes + +- The block has internal state. +- The system is strictly proper (no direct feedthrough). +- Input dimensions are validated at runtime. + + +--- +© 2026 Université de Lille & INRIA – Licensed under LGPL-3.0-or-later diff --git a/pySimBlocks/gui/blocks/systems/polytopic_state_space.py b/pySimBlocks/gui/blocks/systems/polytopic_state_space.py new file mode 100644 index 0000000..ee142d0 --- /dev/null +++ b/pySimBlocks/gui/blocks/systems/polytopic_state_space.py @@ -0,0 +1,109 @@ +# ****************************************************************************** +# pySimBlocks +# Copyright (c) 2026 Université de Lille & INRIA +# ****************************************************************************** +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# ****************************************************************************** +# Authors: see Authors.txt +# ****************************************************************************** + +from pySimBlocks.gui.blocks.block_meta import BlockMeta +from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta +from pySimBlocks.gui.blocks.port_meta import PortMeta + + +class PolytopicStateSpaceMeta(BlockMeta): + + def __init__(self): + self.name = "PolytopicStateSpace" + self.category = "systems" + self.type = "polytopic_state_space" + self.summary = "Discrete-time polytopic state-space system." + self.description = ( + "Implements the discrete-time polytopic state-space equations:\n" + "$$\n" + "x[k+1] = A\\,\\mathrm{kron}(w[k], x[k]) + B\\,\\mathrm{kron}(w[k], u[k])\n" + "$$\n" + "$$\n" + "y[k] = C x[k]\n" + "$$\n" + "with $w[k]$ the vertex weight vector.\n" + ) + + self.parameters = [ + ParameterMeta( + name="A", + type="matrix", + required=True, + autofill=True, + default=[[1.0, 0.0]], + description="Stacked polytopic state matrix of shape (nx, r*nx): [A1 ... Ar]." + ), + ParameterMeta( + name="B", + type="matrix", + required=True, + autofill=True, + default=[[1.0, 1.0]], + description="Stacked polytopic input matrix of shape (nx, r*nu): [B1 ... Br]." + ), + ParameterMeta( + name="C", + type="matrix", + required=True, + autofill=True, + default=[[1.0]], + description="Output matrix of shape (ny, nx)." + ), + ParameterMeta( + name="x0", + type="vector", + description="Initial state vector." + ), + ParameterMeta( + name="sample_time", + type="float", + description="Block execution period." + ) + ] + + self.inputs = [ + PortMeta( + name="w", + display_as="w", + shape=["r", 1], + description="Vertex weights vector." + ), + PortMeta( + name="u", + display_as="u", + shape=["nu", 1], + description="Input vector." + ) + ] + + self.outputs = [ + PortMeta( + name="x", + display_as="x", + shape=["nx", 1], + description="State vector." + ), + PortMeta( + name="y", + display_as="y", + shape=["ny", 1], + description="Output vector." + ) + ] diff --git a/pySimBlocks/project/pySimBlocks_blocks_index.yaml b/pySimBlocks/project/pySimBlocks_blocks_index.yaml index 8fa60e5..225a559 100644 --- a/pySimBlocks/project/pySimBlocks_blocks_index.yaml +++ b/pySimBlocks/project/pySimBlocks_blocks_index.yaml @@ -83,6 +83,9 @@ systems: linear_state_space: class: LinearStateSpace module: pySimBlocks.blocks.systems.linear_state_space + polytopic_state_space: + class: PolytopicStateSpace + module: pySimBlocks.blocks.systems.polytopic_state_space non_linear_state_space: class: NonLinearStateSpace module: pySimBlocks.blocks.systems.non_linear_state_space diff --git a/test/blocks/systems/test_polytopic_state_space.py b/test/blocks/systems/test_polytopic_state_space.py new file mode 100644 index 0000000..46c32a0 --- /dev/null +++ b/test/blocks/systems/test_polytopic_state_space.py @@ -0,0 +1,133 @@ +import numpy as np +import pytest + +from pySimBlocks.core import Model, Simulator, SimulationConfig +from pySimBlocks.blocks.sources.constant import Constant +from pySimBlocks.blocks.systems.polytopic_state_space import PolytopicStateSpace + + +def run_sim(src_w, src_u, sys, dt=0.1, T=0.3): + m = Model() + m.add_block(src_w) + m.add_block(src_u) + m.add_block(sys) + m.connect(src_w.name, "out", sys.name, "w") + m.connect(src_u.name, "out", sys.name, "u") + + sim_cfg = SimulationConfig(dt, T, logging=[f"{sys.name}.outputs.y", f"{sys.name}.outputs.x"]) + sim = Simulator(m, sim_cfg) + logs = sim.run() + return logs + + +def test_poly_scalar_basic(): + # r=2, nx=1, nu=1, ny=1 + A = [[0.8, 0.5]] # [A1 A2] + B = [[1.0, 2.0]] # [B1 B2] + C = [[1.0]] + + src_w = Constant("w", [[0.25], [0.75]]) + src_u = Constant("u", [[2.0]]) + sys = PolytopicStateSpace("sys", A=A, B=B, C=C, x0=[[0.0]]) + + logs = run_sim(src_w, src_u, sys) + y = logs["sys.outputs.y"] + x = logs["sys.outputs.x"] + + # x[k+1] = 0.575*x[k] + 3.5 + assert np.allclose(y[0], [[0.0]]) + assert np.allclose(x[1], [[3.5]]) + assert np.allclose(y[1], [[3.5]]) + assert np.allclose(x[2], [[5.5125]]) + assert np.allclose(y[2], [[5.5125]]) + + +def test_poly_vector_dimensions(): + # r=2, nx=2, nu=1, ny=1 + A1 = np.array([[1.0, 0.1], [0.0, 1.0]]) + A2 = np.array([[0.9, 0.0], [0.0, 0.95]]) + A = np.hstack([A1, A2]) + + B1 = np.array([[0.0], [1.0]]) + B2 = np.array([[1.0], [0.0]]) + B = np.hstack([B1, B2]) + C = np.array([[1.0, 0.0]]) + + src_w = Constant("w", [[0.2], [0.8]]) + src_u = Constant("u", [[2.0]]) + sys = PolytopicStateSpace("sys", A=A, B=B, C=C, x0=[[0.0], [0.0]]) + + logs = run_sim(src_w, src_u, sys) + x = logs["sys.outputs.x"] + y = logs["sys.outputs.y"] + + assert np.allclose(x[1], [[1.6], [0.4]]) + assert np.allclose(y[1], [[1.6]]) + assert np.allclose(x[2], [[3.08], [0.784]]) + assert np.allclose(y[2], [[3.08]]) + + +def test_poly_missing_w_input_raises(): + A = [[1.0, 0.0]] + B = [[1.0, 1.0]] + C = [[1.0]] + + src_u = Constant("u", [[1.0]]) + sys = PolytopicStateSpace("sys", A=A, B=B, C=C) + + m = Model() + m.add_block(src_u) + m.add_block(sys) + m.connect("u", "out", "sys", "u") + + sim_cfg = SimulationConfig(0.1, 0.1, logging=["sys.outputs.y"]) + sim = Simulator(m, sim_cfg) + sim.initialize() + + with pytest.raises(RuntimeError): + sim.run() + + +def test_poly_rejects_non_column_w(): + A = [[1.0, 0.0]] + B = [[1.0, 1.0]] + C = [[1.0]] + + src_w = Constant("w", [[0.2, 0.8], [0.1, 0.9]]) # 2x2, not column vector + src_u = Constant("u", [[1.0]]) + sys = PolytopicStateSpace("sys", A=A, B=B, C=C) + + m = Model() + m.add_block(src_w) + m.add_block(src_u) + m.add_block(sys) + m.connect("w", "out", "sys", "w") + m.connect("u", "out", "sys", "u") + + sim_cfg = SimulationConfig(0.1, 0.1, logging=["sys.outputs.y"]) + sim = Simulator(m, sim_cfg) + + with pytest.raises(ValueError) as err: + sim.run() + + assert "must be a column vector" in str(err.value) + + +def test_poly_invalid_matrix_dimensions(): + C = [[1.0]] + + # A has nx=1 but 3 columns -> not (nx, r*nx) + with pytest.raises(ValueError): + PolytopicStateSpace("sys1", A=[[1.0, 2.0, 3.0]], B=[[1.0, 1.0]], C=C) + + # A => nx=1, r=2, so B must have 2*nu columns + with pytest.raises(ValueError): + PolytopicStateSpace("sys2", A=[[1.0, 2.0]], B=[[1.0, 2.0, 3.0]], C=C) + + # C has wrong number of columns + with pytest.raises(ValueError): + PolytopicStateSpace("sys3", A=[[1.0, 2.0]], B=[[1.0, 1.0]], C=[[1.0, 2.0]]) + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) From 26b99435cb82cfccce3b5b465269e90bf47ac63b Mon Sep 17 00:00:00 2001 From: Alessandrini Antoine Date: Sat, 14 Feb 2026 16:05:51 +0100 Subject: [PATCH 2/3] example(systems): polytope gui use --- .../generate/state_space/polytope/layout.yaml | 18 +++++++++ .../generate/state_space/polytope/model.yaml | 17 ++++++++ .../state_space/polytope/parameters.yaml | 28 +++++++++++++ .../generate/state_space/polytope/params.py | 39 +++++++++++++++++++ .../generate/state_space/polytope/weights.py | 12 ++++++ 5 files changed, 114 insertions(+) create mode 100644 examples/generate/state_space/polytope/layout.yaml create mode 100644 examples/generate/state_space/polytope/model.yaml create mode 100644 examples/generate/state_space/polytope/parameters.yaml create mode 100644 examples/generate/state_space/polytope/params.py create mode 100644 examples/generate/state_space/polytope/weights.py diff --git a/examples/generate/state_space/polytope/layout.yaml b/examples/generate/state_space/polytope/layout.yaml new file mode 100644 index 0000000..2a4be81 --- /dev/null +++ b/examples/generate/state_space/polytope/layout.yaml @@ -0,0 +1,18 @@ +version: 1 +blocks: + Polytope: + x: -164.0 + y: -107.0 + orientation: normal + weights: + x: -400.0 + y: -150.0 + orientation: normal + Step: + x: -400.0 + y: -60.0 + orientation: normal + Constant: + x: -575.0 + y: -150.0 + orientation: normal diff --git a/examples/generate/state_space/polytope/model.yaml b/examples/generate/state_space/polytope/model.yaml new file mode 100644 index 0000000..8763594 --- /dev/null +++ b/examples/generate/state_space/polytope/model.yaml @@ -0,0 +1,17 @@ +blocks: +- name: Polytope + category: systems + type: polytopic_state_space +- name: weights + category: operators + type: algebraic_function +- name: Step + category: sources + type: step +- name: Constant + category: sources + type: constant +connections: +- [Step.out, Polytope.u] +- [weights.w, Polytope.w] +- [Constant.out, weights.c] diff --git a/examples/generate/state_space/polytope/parameters.yaml b/examples/generate/state_space/polytope/parameters.yaml new file mode 100644 index 0000000..d933810 --- /dev/null +++ b/examples/generate/state_space/polytope/parameters.yaml @@ -0,0 +1,28 @@ +simulation: + dt: 0.1 + T: 10.0 + solver: fixed +blocks: + Polytope: + A: '#A' + B: '#B' + C: '#C' + weights: + file_path: weights.py + function_name: get_weights + input_keys: + - c + output_keys: + - w + Step: + value_before: [[0.0]] + value_after: [[1.0]] + start_time: 1.0 + Constant: + value: [[0.3]] +logging: +- Polytope.outputs.x +- Polytope.outputs.y +- weights.outputs.w +plots: [] +external: params.py diff --git a/examples/generate/state_space/polytope/params.py b/examples/generate/state_space/polytope/params.py new file mode 100644 index 0000000..be960e8 --- /dev/null +++ b/examples/generate/state_space/polytope/params.py @@ -0,0 +1,39 @@ +import numpy as np +import matplotlib.pyplot as plt + +# Dimensions +r = 3 # number of vertices +nx = 4 # state dimension +nu = 1 # input dimension +ny = 2 # output dimension + + +# Create A_i matrices +A1 = np.array([ + [0.8, 0.1, 0.0, 0.0], + [0.0, 0.7, 0.1, 0.0], + [0.0, 0.0, 0.6, 0.1], + [0.0, 0.0, 0.0, 0.5], +]) + +A2 = A1 + 0.05 * np.eye(nx) +A3 = A1 - 0.05 * np.eye(nx) + +A = np.hstack([A1, A2, A3]) + + +# Create B_i matrices +B1 = np.array([[1.0], [0.0], [0.0], [0.0]]) +B2 = np.array([[0.5], [0.5], [0.0], [0.0]]) +B3 = np.array([[0.2], [0.3], [0.3], [0.2]]) + +B = np.hstack([B1, B2, B3]) + + +# Output matrix +C = np.array([ + [1.0, 0.0, 0.0, 0.0], + [0.0, 0.0, 1.0, 0.0], +]) + + diff --git a/examples/generate/state_space/polytope/weights.py b/examples/generate/state_space/polytope/weights.py new file mode 100644 index 0000000..b99730f --- /dev/null +++ b/examples/generate/state_space/polytope/weights.py @@ -0,0 +1,12 @@ +import numpy as np + + +def get_weights(t, dt, c): + w1 = np.clip(0.4 + 0.2 * np.sin(0.5 * t) + c, 0.0, 1.0) + w2 = np.clip(0.3 + 0.1 * np.cos(0.3 * t), 0.0, 1-w1) + w3 = 1.0 - w1 - w2 + w3 = np.clip(w3, 0.0, 1.0) + + val = np.array([w1, w2, w3]).reshape(-1, 1) + return {"w": val} + From 41ab79d541c71bda3044b8bec0eae41b199253b3 Mon Sep 17 00:00:00 2001 From: Alessandrini Antoine Date: Sat, 14 Feb 2026 16:06:26 +0100 Subject: [PATCH 3/3] fix(systems): polytope check positive weights --- pySimBlocks/blocks/systems/polytopic_state_space.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pySimBlocks/blocks/systems/polytopic_state_space.py b/pySimBlocks/blocks/systems/polytopic_state_space.py index da7be8d..8439e84 100644 --- a/pySimBlocks/blocks/systems/polytopic_state_space.py +++ b/pySimBlocks/blocks/systems/polytopic_state_space.py @@ -158,6 +158,8 @@ def state_update(self, t: float, dt: float) -> None: w_vec = self._to_col_vec("w", w, self._r) if not np.isclose(np.sum(w_vec), 1.0): raise ValueError(f"[{self.name}] Vertex weights w must sum to 1. Got sum {np.sum(w_vec)}.") + if np.any(w_vec < 0): + raise ValueError(f"[{self.name}] Vertex weights w must be non-negative. Got {w_vec.flatten()}.") u_vec = self._to_col_vec("u", u, self._nu) x = self.state["x"]