From 5372f9dc4b3553e465a071422e7f7ffcb755de99 Mon Sep 17 00:00:00 2001 From: Alessandrini Antoine Date: Tue, 28 Jul 2026 17:39:37 +0200 Subject: [PATCH] feat: add linear sys feedforward --- .../blocks/systems/linear_state_space.py | 45 ++++++++++++++++--- .../docs/blocks/systems/linear_state_space.md | 27 ++++++++--- .../gui/blocks/systems/linear_state_space.py | 10 ++++- 3 files changed, 71 insertions(+), 11 deletions(-) diff --git a/pySimBlocks/blocks/systems/linear_state_space.py b/pySimBlocks/blocks/systems/linear_state_space.py index 7a5cd08..28206a4 100644 --- a/pySimBlocks/blocks/systems/linear_state_space.py +++ b/pySimBlocks/blocks/systems/linear_state_space.py @@ -29,20 +29,25 @@ class LinearStateSpace(Block): """Discrete-time linear state-space system block. - Implements a strictly proper discrete-time linear system: + Implements a discrete-time linear system: x[k+1] = A x[k] + B u[k] - - y[k] = C x[k] - The D matrix is intentionally not supported to avoid algebraic loops. + y[k] = C x[k] (if D is None) + y[k] = C x[k] + D u[k] (if D is provided) + + When D is None the block is strictly proper (no direct feedthrough). + When D is provided the block has direct feedthrough and algebraic loops + involving this block will be detected and rejected at compile time. Attributes: A: State transition matrix of shape (n, n). B: Input matrix of shape (n, m). C: Output matrix of shape (p, n). + D: Feedthrough matrix of shape (p, m), or None. """ + # Default at class level; overridden per-instance when D is provided. direct_feedthrough = False def __init__( @@ -51,6 +56,7 @@ def __init__( A: ArrayLike, B: ArrayLike, C: ArrayLike, + D: ArrayLike | None = None, x0: ArrayLike | None = None, sample_time: float | None = None, ): @@ -61,6 +67,9 @@ def __init__( A: State transition matrix, array-like of shape (n, n). B: Input matrix, array-like of shape (n, m). C: Output matrix, array-like of shape (p, n). + D: Feedthrough matrix, array-like of shape (p, m), or None. + When provided, the block gains direct feedthrough and + y[k] = C x[k] + D u[k]. x0: Initial state vector, array-like of shape (n, 1) or (n,). Defaults to zeros. sample_time: Sampling period in seconds, or None to use the @@ -101,6 +110,23 @@ def __init__( self._m = self.B.shape[1] self._p = self.C.shape[0] + # --- D matrix (optional) --- + if D is not None: + self.D = np.asarray(D, dtype=float) + if self.D.ndim != 2: + raise ValueError(f"[{self.name}] D must be 2D. Got shape {self.D.shape}.") + if self.D.shape != (self._p, self._m): + raise ValueError( + f"[{self.name}] D must have shape ({self._p}, {self._m}). " + f"Got {self.D.shape}." + ) + # Override direct_feedthrough at the instance level so the + # scheduler sees this block as having direct feedthrough. + self.direct_feedthrough = True + else: + self.D = None + + # --- Initial state --- if x0 is None: x0_arr = np.zeros((n, 1), dtype=float) else: @@ -143,12 +169,21 @@ def initialize(self, t0: float) -> None: def output_update(self, t: float, dt: float) -> None: """Compute y and x outputs from the committed state. + When D is provided, u[k] is read at this step (direct feedthrough). + Args: t: Current simulation time in seconds. dt: Current time step in seconds. """ x = self.state["x"] - self.outputs["y"] = self.C @ x + if self.D is not None: + u = self.inputs["u"] + if u is None: + raise RuntimeError(f"[{self.name}] Input 'u' is not connected or not set.") + u_vec = self._to_col_vec("u", u, self._m) + self.outputs["y"] = self.C @ x + self.D @ u_vec + else: + self.outputs["y"] = self.C @ x self.outputs["x"] = x.copy() def state_update(self, t: float, dt: float) -> None: diff --git a/pySimBlocks/docs/blocks/systems/linear_state_space.md b/pySimBlocks/docs/blocks/systems/linear_state_space.md index e851d8b..c1c9314 100644 --- a/pySimBlocks/docs/blocks/systems/linear_state_space.md +++ b/pySimBlocks/docs/blocks/systems/linear_state_space.md @@ -2,13 +2,16 @@ ## Summary -The **LinearStateSpace** block implements a discrete-time linear state-space system without direct feedthrough. +The **LinearStateSpace** block implements a discrete-time linear state-space system. + +Without feedthrough matrix $D$ the system is strictly proper. When $D$ is provided +the block has direct feedthrough and $y[k]$ depends on $u[k]$ at the same step. --- ## Mathematical definition -The system is defined by the equations: +Without $D$ (strictly proper): $$ x[k+1] = A x[k] + B u[k] @@ -18,6 +21,16 @@ $$ y[k] = C x[k] $$ +With $D$ (direct feedthrough): + +$$ +x[k+1] = A x[k] + B u[k] +$$ + +$$ +y[k] = C x[k] + D u[k] +$$ + where: - $x[k]$ is the state vector, - $u[k]$ is the input vector, @@ -32,6 +45,7 @@ where: | `A` | 2D array | State transition matrix of size (n, n). | False | | `B` | 2D array | Input matrix of size (n, m). | False | | `C` | 2D array | Output matrix of size (p, n). | False | +| `D` | 2D array | Feedthrough matrix of size (p, m). If omitted, no direct feedthrough. | True | | `x0` | 1D array | Initial state vector of size (n,). If omitted, the state is initialized to zero. | True | | `sample_time` | float | Block sample time. If omitted, the global simulation time step is used. | True | @@ -57,9 +71,12 @@ where: ## Notes - The block has internal state. -- The system is strictly proper (no direct feedthrough). -- Matrix $D$ is intentionally not supported to avoid algebraic loops. -- The output is computed from the current state. +- When `D` is omitted or `None`, the system is strictly proper (no direct feedthrough). +- When `D` is provided, the block has direct feedthrough: `y[k]` depends on `u[k]` + at the same simulation step. +- A block with direct feedthrough cannot be part of a feedback loop without a `Delay` + block breaking the cycle — pySimBlocks will raise a `RuntimeError` at compile time + if an algebraic loop is detected. --- diff --git a/pySimBlocks/gui/blocks/systems/linear_state_space.py b/pySimBlocks/gui/blocks/systems/linear_state_space.py index 047c62a..c909a46 100644 --- a/pySimBlocks/gui/blocks/systems/linear_state_space.py +++ b/pySimBlocks/gui/blocks/systems/linear_state_space.py @@ -45,8 +45,10 @@ def __init__(self): "x[k+1] = A x[k] + B u[k]\n" "$$\n" "$$\n" - "y[k] = C x[k]\n" + "y[k] = C x[k] + D u[k]\n" "$$\n" + "When D is omitted the system is strictly proper (no direct feedthrough).\n" + "When D is provided the block has direct feedthrough.\n" ) self.parameters = [ @@ -74,6 +76,12 @@ def __init__(self): default=[[1.0]], description="Output matrix." ), + ParameterMeta( + name="D", + type="matrix", + required=False, + description="Feedthrough matrix. If provided, y[k] = C x[k] + D u[k] (direct feedthrough)." + ), ParameterMeta( name="x0", type="vector",