diff --git a/pySimBlocks/blocks/operators/__init__.py b/pySimBlocks/blocks/operators/__init__.py
index 8fb9741..1a5f8c4 100644
--- a/pySimBlocks/blocks/operators/__init__.py
+++ b/pySimBlocks/blocks/operators/__init__.py
@@ -19,6 +19,7 @@
# ******************************************************************************
from pySimBlocks.blocks.operators.dead_zone import DeadZone
+from pySimBlocks.blocks.operators.demux import Demux
from pySimBlocks.blocks.operators.delay import Delay
from pySimBlocks.blocks.operators.discrete_derivator import DiscreteDerivator
from pySimBlocks.blocks.operators.discrete_integrator import DiscreteIntegrator
@@ -32,6 +33,7 @@
__all__ = [
"DeadZone",
+ "Demux",
"Delay",
"DiscreteDerivator",
"DiscreteIntegrator",
diff --git a/pySimBlocks/blocks/operators/demux.py b/pySimBlocks/blocks/operators/demux.py
new file mode 100644
index 0000000..a2dc3fc
--- /dev/null
+++ b/pySimBlocks/blocks/operators/demux.py
@@ -0,0 +1,123 @@
+# ******************************************************************************
+# 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 Demux(Block):
+ """
+ Vector split block (inverse of Mux).
+
+ Summary:
+ Splits one input column vector into multiple output segments.
+
+ Parameters:
+ num_outputs : int
+ Number of scalar outputs to produce.
+ sample_time : float, optional
+ Block execution period.
+
+ Inputs:
+ in : vector (n,1)
+ Input must be a column vector.
+
+ Outputs:
+ out1, out2, ..., outP : array (k,1)
+ Output segment sizes follow:
+ - q = n // p
+ - m = n % p
+ - first m outputs have size (q+1,1)
+ - remaining (p-m) outputs have size (q,1)
+ """
+
+ direct_feedthrough = True
+
+ def __init__(self, name: str, num_outputs: int = 2, sample_time: float | None = None):
+ super().__init__(name, sample_time)
+
+ if not isinstance(num_outputs, int) or num_outputs < 1:
+ raise ValueError(f"[{self.name}] num_outputs must be a positive integer.")
+ self.num_outputs = num_outputs
+
+ self.inputs["in"] = None
+ for i in range(num_outputs):
+ self.outputs[f"out{i+1}"] = None
+
+
+ # --------------------------------------------------------------------------
+ # Public methods
+ # --------------------------------------------------------------------------
+ def initialize(self, t0: float) -> None:
+ if self.inputs["in"] is None:
+ for i in range(self.num_outputs):
+ self.outputs[f"out{i+1}"] = np.zeros((1, 1), dtype=float)
+ return
+
+ self._compute_outputs()
+
+ # ---------------------------------------------------------
+ def output_update(self, t: float, dt: float) -> None:
+ self._compute_outputs()
+
+ # ---------------------------------------------------------
+ def state_update(self, t: float, dt: float) -> None:
+ return # stateless
+
+
+ # --------------------------------------------------------------------------
+ # Private methods
+ # --------------------------------------------------------------------------
+ def _to_vector(self, value: ArrayLike) -> np.ndarray:
+ arr = np.asarray(value, dtype=float)
+
+ if arr.ndim != 2 or arr.shape[1] != 1:
+ raise ValueError(
+ f"[{self.name}] Input 'in' must be a column vector (n,1). "
+ f"Got shape {arr.shape}."
+ )
+ return arr
+
+ # ---------------------------------------------------------
+ def _compute_outputs(self) -> None:
+ u = self.inputs["in"]
+ if u is None:
+ raise RuntimeError(f"[{self.name}] Input 'in' is not connected or not set.")
+
+ vec = self._to_vector(u)
+ n = vec.shape[0]
+ p = self.num_outputs
+
+ if p > n:
+ raise ValueError(
+ f"[{self.name}] num_outputs ({p}) must be <= input vector length ({n})."
+ )
+
+ q = n // p
+ m = n % p
+
+ start = 0
+ for i in range(p):
+ seg_len = q + 1 if i < m else q
+ end = start + seg_len
+ self.outputs[f"out{i+1}"] = vec[start:end].copy()
+ start = end
diff --git a/pySimBlocks/docs/blocks/operators/demux.md b/pySimBlocks/docs/blocks/operators/demux.md
new file mode 100644
index 0000000..5599f7d
--- /dev/null
+++ b/pySimBlocks/docs/blocks/operators/demux.md
@@ -0,0 +1,64 @@
+# Demux
+
+## Summary
+
+The **Demux** block splits one input column vector into multiple output
+column vectors.
+
+---
+
+## Mathematical definition
+
+Given an input vector $\mathrm{in} \in \mathbb{R}^{n \times 1}$ and
+$p$ outputs:
+
+$$
+q = \left\lfloor \frac{n}{p} \right\rfloor, \quad
+m = n \bmod p
+$$
+
+The output segments are:
+- the first $m$ outputs have size $(q+1, 1)$,
+- the remaining $(p-m)$ outputs have size $(q, 1)$.
+
+This corresponds to an even split with the remainder distributed on the
+first outputs.
+
+---
+
+## Parameters
+
+| Name | Type | Description | Optional |
+|------------|-------------|-------------|-------------|
+| `num_outputs` | integer | Number of output ports to create. | True |
+| `sample_time` | float | Block sample time. If omitted, the global simulation time step is used. | True |
+
+---
+
+## Inputs
+
+| Port | Description |
+|------|------------|
+| `in` | Input column vector of shape $(n,1)$. |
+
+---
+
+## Outputs
+
+| Port | Description |
+|------|------------|
+| `out1 … outP` | Output vector segments according to the split rule above. |
+
+---
+
+## Notes
+
+- The block has no internal state.
+- The block has direct feedthrough.
+- Input must be a column vector of shape $(n,1)$.
+- The parameter `num_outputs` must satisfy $1 \le p \le n$.
+- This block is equivalent to the Simulink **Demux** concept for vector splitting.
+
+
+---
+© 2026 Université de Lille & INRIA – Licensed under LGPL-3.0-or-later
diff --git a/pySimBlocks/docs/blocks/sources/chirp.md b/pySimBlocks/docs/blocks/sources/chirp.md
new file mode 100644
index 0000000..5b4da47
--- /dev/null
+++ b/pySimBlocks/docs/blocks/sources/chirp.md
@@ -0,0 +1,102 @@
+# Chirp
+
+## Summary
+
+The **Chirp** block generates a frequency-swept sinusoidal signal
+(linear or logarithmic) with configurable amplitude, phase, offset,
+start time, and duration.
+
+---
+
+## Mathematical definition
+
+For each output component $i$:
+
+$$
+y_i(t) = A_i \sin(\phi_i(t)) + o_i
+$$
+
+with parameters:
+- $A_i$: amplitude,
+- $o_i$: offset,
+- $\phi_i(t)$: chirp phase,
+- $f_{0,i}$: initial frequency,
+- $f_{1,i}$: final frequency,
+- $T_i$: chirp duration,
+- $t_{0,i}$: chirp start time,
+- $\varphi_i$: initial phase.
+
+Define:
+$$
+\tau_i = \max(0, t - t_{0,i}), \quad
+\tau_{c,i} = \min(\tau_i, T_i)
+$$
+
+Linear mode:
+$$
+k_i = \frac{f_{1,i} - f_{0,i}}{T_i}
+$$
+$$
+\phi_i(t) =
+2\pi\left(f_{0,i}\tau_{c,i} + \frac{1}{2}k_i\tau_{c,i}^2\right)
++ 2\pi f_{1,i}\max(0, \tau_i - T_i)
++ \varphi_i
+$$
+
+Log mode:
+$$
+r_i = \frac{f_{1,i}}{f_{0,i}}
+$$
+$$
+\phi_i(t) =
+\frac{2\pi f_{0,i}T_i}{\ln(r_i)}
+\left(r_i^{\tau_{c,i}/T_i} - 1\right)
++ 2\pi f_{1,i}\max(0, \tau_i - T_i)
++ \varphi_i
+$$
+
+After duration, phase continuity is preserved and oscillation continues at $f_1$.
+
+---
+
+## Parameters
+
+| Name | Type | Description | Optional |
+|------------|-------------|-------------|-------------|
+| `amplitude` | scalar or vector or matrix | Signal amplitude. Scalars are broadcast to all dimensions. | False |
+| `f0` | scalar or vector or matrix | Initial frequency in Hertz. Scalars are broadcast. | False |
+| `f1` | scalar or vector or matrix | Final frequency in Hertz. Scalars are broadcast. | False |
+| `duration` | scalar or vector or matrix | Sweep duration in seconds. Must be strictly positive. Scalars are broadcast. | False |
+| `start_time` | scalar or vector or matrix | Start time in seconds. Before this time, sweep is not active. Default is `0.0`. | True |
+| `offset` | scalar or vector or matrix | Constant offset added to output. Default is `0.0`. | True |
+| `phase` | scalar or vector or matrix | Initial phase in radians. Default is `0.0`. | True |
+| `mode` | string | Sweep mode: `linear` or `log`. Default is `linear`. | True |
+| `sample_time` | float | Block sample time. If omitted, the global simulation time step is used. | True |
+
+---
+
+## Inputs
+
+This block has **no inputs**.
+
+---
+
+## Outputs
+
+| Port | Description |
+|------|------------|
+| `out` | Chirp output signal. |
+
+---
+
+## Notes
+
+- The block has no internal state.
+- All array-like parameters must have compatible shapes.
+- Scalar parameters are broadcast to the common signal shape.
+- In `log` mode, `f0 > 0`, `f1 > 0`, and `f0 != f1` are required.
+- The output keeps oscillating after `duration` at frequency `f1`.
+
+
+---
+© 2026 Université de Lille & INRIA – Licensed under LGPL-3.0-or-later
diff --git a/pySimBlocks/gui/blocks/operators/demux.py b/pySimBlocks/gui/blocks/operators/demux.py
new file mode 100644
index 0000000..42c652b
--- /dev/null
+++ b/pySimBlocks/gui/blocks/operators/demux.py
@@ -0,0 +1,98 @@
+# ******************************************************************************
+# 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 typing import Literal
+
+from pySimBlocks.gui.blocks.block_meta import BlockMeta
+from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta
+from pySimBlocks.gui.blocks.port_meta import PortMeta
+from pySimBlocks.gui.model import BlockInstance, PortInstance
+
+
+class DemuxMeta(BlockMeta):
+
+ def __init__(self):
+ self.name = "Demux"
+ self.category = "operators"
+ self.type = "demux"
+ self.summary = "Vector split block (inverse of Mux)."
+ self.description = (
+ "Splits one input column vector into multiple output segments.\n"
+ "For input length $n$ and $p$ outputs:\n"
+ "$$\n"
+ "q = n // p, \\quad m = n \\% p\n"
+ "$$\n"
+ "The first $m$ outputs have $(q+1)$ elements and the remaining\n"
+ "$(p-m)$ outputs have $q$ elements.\n"
+ )
+
+ self.parameters = [
+ ParameterMeta(
+ name="num_outputs",
+ type="int",
+ autofill=True,
+ default=2
+ ),
+ ParameterMeta(
+ name="sample_time",
+ type="float"
+ )
+ ]
+
+ self.inputs = [
+ PortMeta(
+ name="in",
+ display_as="in",
+ shape=["n", 1],
+ description="Input column vector to split."
+ )
+ ]
+
+ self.outputs = [
+ PortMeta(
+ name="out",
+ display_as="",
+ shape=["k", 1],
+ description="Output vector segments."
+ )
+ ]
+
+ def resolve_port_group(
+ self,
+ port_meta: PortMeta,
+ direction: Literal["input", "output"],
+ instance: "BlockInstance"
+ ) -> list["PortInstance"]:
+
+ if direction == "output" and port_meta.name == "out":
+ num_outputs = instance.parameters.get("num_outputs", 0)
+ ports = []
+ for i in range(1, num_outputs + 1):
+ ports.append(
+ PortInstance(
+ name=f"{port_meta.name}{i}",
+ display_as=f"{i}",
+ direction="output",
+ block=instance
+ )
+ )
+ return ports
+
+ return super().resolve_port_group(port_meta, direction, instance)
diff --git a/pySimBlocks/project/pySimBlocks_blocks_index.yaml b/pySimBlocks/project/pySimBlocks_blocks_index.yaml
index 88e804c..8fa60e5 100644
--- a/pySimBlocks/project/pySimBlocks_blocks_index.yaml
+++ b/pySimBlocks/project/pySimBlocks_blocks_index.yaml
@@ -23,6 +23,9 @@ operators:
dead_zone:
class: DeadZone
module: pySimBlocks.blocks.operators.dead_zone
+ demux:
+ class: Demux
+ module: pySimBlocks.blocks.operators.demux
delay:
class: Delay
module: pySimBlocks.blocks.operators.delay
diff --git a/pyproject.toml b/pyproject.toml
index d6a968f..b940645 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -32,7 +32,6 @@ where = ["."]
[tool.setuptools.package-data]
pySimBlocks = [
- "blocks_metadata/**/*.yaml",
"docs/blocks/**/*.md",
"project/pySimBlocks_blocks_index.yaml",
]
@@ -43,5 +42,6 @@ examples = [
]
tests = [
- "control",
+ "pytest",
+ "pytest-qt",
]
diff --git a/test/blocks/operators/test_demux.py b/test/blocks/operators/test_demux.py
new file mode 100644
index 0000000..ba72e4d
--- /dev/null
+++ b/test/blocks/operators/test_demux.py
@@ -0,0 +1,111 @@
+import numpy as np
+import pytest
+
+from pySimBlocks.blocks.operators.demux import Demux
+from pySimBlocks.blocks.sources.constant import Constant
+from pySimBlocks.core import Model, SimulationConfig, Simulator
+
+
+def run_sim(value, num_outputs):
+ m = Model()
+
+ c = Constant("c", value)
+ dmx = Demux("D", num_outputs=num_outputs)
+
+ m.add_block(c)
+ m.add_block(dmx)
+ m.connect("c", "out", "D", "in")
+
+ logs = [f"D.outputs.out{i+1}" for i in range(num_outputs)]
+ sim_cfg = SimulationConfig(0.1, 0.1, logging=logs)
+ sim = Simulator(m, sim_cfg)
+ data = sim.run()
+ return [data[key][-1] for key in logs]
+
+
+def test_demux_basic_column_vector():
+ outputs = run_sim([[1.0], [2.0], [3.0]], num_outputs=3)
+ assert np.allclose(outputs[0], [[1.0]])
+ assert np.allclose(outputs[1], [[2.0]])
+ assert np.allclose(outputs[2], [[3.0]])
+
+
+def test_demux_equal_split_when_divisible():
+ outputs = run_sim([[1.0], [2.0], [3.0], [4.0]], num_outputs=2)
+ assert np.allclose(outputs[0], [[1.0], [2.0]])
+ assert np.allclose(outputs[1], [[3.0], [4.0]])
+
+
+def test_demux_remainder_split_rule():
+ # n=5, p=3 => q=1, m=2
+ # out1/out2 have 2 elems, out3 has 1 elem
+ outputs = run_sim([[1.0], [2.0], [3.0], [4.0], [5.0]], num_outputs=3)
+ assert np.allclose(outputs[0], [[1.0], [2.0]])
+ assert np.allclose(outputs[1], [[3.0], [4.0]])
+ assert np.allclose(outputs[2], [[5.0]])
+
+
+def test_demux_invalid_parameter():
+ with pytest.raises(ValueError) as err:
+ Demux("D", num_outputs=0)
+ assert "num_outputs must be a positive integer" in str(err.value)
+
+
+def test_demux_missing_input_raises():
+ m = Model()
+ dmx = Demux("D", num_outputs=2)
+ m.add_block(dmx)
+
+ sim_cfg = SimulationConfig(0.1, 0.1)
+ sim = Simulator(m, sim_cfg)
+
+ with pytest.raises(RuntimeError) as err:
+ sim.run(T=0.1)
+
+ assert "not connected or not set" in str(err.value)
+
+
+def test_demux_rejects_matrix_input():
+ m = Model()
+ c = Constant("c", [[1.0, 2.0], [3.0, 4.0]])
+ dmx = Demux("D", num_outputs=4)
+
+ m.add_block(c)
+ m.add_block(dmx)
+ m.connect("c", "out", "D", "in")
+
+ sim_cfg = SimulationConfig(0.1, 0.1)
+ sim = Simulator(m, sim_cfg)
+
+ with pytest.raises(RuntimeError) as err:
+ sim.run(T=0.1)
+
+ assert "must be a column vector (n,1)" in str(err.value)
+
+
+def test_demux_rejects_1d_input():
+ dmx = Demux("D", num_outputs=1)
+ dmx.inputs["in"] = np.array([5.0])
+
+ with pytest.raises(ValueError) as err:
+ dmx.output_update(t=0.0, dt=0.1)
+
+ assert "must be a column vector (n,1)" in str(err.value)
+
+
+def test_demux_rejects_more_outputs_than_input_length():
+ m = Model()
+ c = Constant("c", [[1.0], [2.0]])
+ dmx = Demux("D", num_outputs=3)
+
+ m.add_block(c)
+ m.add_block(dmx)
+ m.connect("c", "out", "D", "in")
+
+ sim_cfg = SimulationConfig(0.1, 0.1)
+ sim = Simulator(m, sim_cfg)
+
+ with pytest.raises(RuntimeError) as err:
+ sim.run(T=0.1)
+
+ assert "must be <= input vector length" in str(err.value)