Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 40 additions & 5 deletions pySimBlocks/blocks/systems/linear_state_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__(
Expand All @@ -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,
):
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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:
Expand Down
27 changes: 22 additions & 5 deletions pySimBlocks/docs/blocks/systems/linear_state_space.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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,
Expand All @@ -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 |

Expand All @@ -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.


---
Expand Down
10 changes: 9 additions & 1 deletion pySimBlocks/gui/blocks/systems/linear_state_space.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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",
Expand Down
Loading