Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
723eef3
fix(docs): index duplication blocks core
AlessandriniAntoine Mar 16, 2026
4fbcd3a
fix(docs): readme docs links
AlessandriniAntoine Mar 16, 2026
8fb6838
fix(docs): specify data shape file source block
AlessandriniAntoine Mar 17, 2026
0c61536
fix(docs): download link on rtd
AlessandriniAntoine Mar 18, 2026
843c3f9
fix(docs): starting point tuto sofa
AlessandriniAntoine Mar 18, 2026
0aa419f
fix(docs): starting point tuto sofa
AlessandriniAntoine Mar 18, 2026
740a987
fix(docs): unify download linl
AlessandriniAntoine Mar 18, 2026
5d15b09
feat(docs): add concepts structure
AlessandriniAntoine Mar 18, 2026
b34322a
fix(core): sofa time out in worker
AlessandriniAntoine Mar 24, 2026
bf8d586
feat(sofa): add dt consistency in sofaplant
AlessandriniAntoine Mar 24, 2026
561c912
feat(sofa): refactor(sofa): auto-resolve SOFA context in controller
AlessandriniAntoine Mar 24, 2026
587c4be
chore: remove dead metadata fields from project YAML
AlessandriniAntoine Mar 24, 2026
c968cd2
feat(docs): simulation documentation
AlessandriniAntoine Mar 25, 2026
80c1915
feat(docs): execution order section
AlessandriniAntoine Mar 25, 2026
143e8e7
fix(docs): windows installation path fail
AlessandriniAntoine Mar 25, 2026
3dcf9c8
fix(core): hardware runner fix
AlessandriniAntoine Mar 25, 2026
a03a805
fix(gui): remove unused external ports
AlessandriniAntoine Mar 25, 2026
2e73de5
feat(docs): adds glossary and block
AlessandriniAntoine Mar 25, 2026
12f50a9
fix(tests): multitask new compatibility
AlessandriniAntoine Mar 25, 2026
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
46 changes: 11 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,55 +3,31 @@
[![PyPI version](https://img.shields.io/pypi/v/pySimBlocks.svg)](https://pypi.org/project/pySimBlocks/)
[![Python](https://img.shields.io/pypi/pyversions/pySimBlocks.svg)](https://pypi.org/project/pySimBlocks/)
[![License](https://img.shields.io/github/license/AlessandriniAntoine/pySimBlocks)](./LICENSE.md)
[![Documentation](https://readthedocs.org/projects/pysimblocks/badge/?version=latest)](https://pysimblocks.readthedocs.io)

---

A deterministic block-diagram simulation framework for discrete-time modeling,
co-simulation and research prototyping in Python.

pySimBlocks allows you to build, configure, and execute discrete-time systems
pySimBlocks allows you to build, configure, and execute discrete-time systems
using either:

- A pure Python API
- A graphical editor (PySide6)
- YAML project configuration
- YAML project configuration with exportable Python runner (`run.py`)
- Optional SOFA and hardware integration

![pySimBlocks graphical editor](https://raw.githubusercontent.com/AlessandriniAntoine/pySimBlocks/main/docs/source/images/user_guide/gui_example.png)

## Features

- Block-based modeling (Simulink-like)
- Deterministic discrete-time simulation engine
- PySide6 graphical editor
- YAML-based project serialization
- Exportable Python runner (`run.py`)
- Extensible block architecture

## Installation

### From PyPI

Install the latest stable version from PyPI using pip:
```
```bash
pip install pySimBlocks
```

### From GitHub

Install directly from GitHub using pip:
```
pip install git+https://github.com/AlessandriniAntoine/pySimBlocks
```

### Locally

Clone the repository and install locally:
```
git clone https://github.com/AlessandriniAntoine/pySimBlocks.git
cd pySimBlocks
pip install .
```
Full documentation — user guide, tutorials, and API reference — is available on
[**Read the Docs**](https://pysimblocks.readthedocs.io).

## Getting Started

Expand Down Expand Up @@ -126,14 +102,14 @@ The quick-start GUI project is stored in a single

#### Tutorials

Three step-by-step tutorials are available detailed in the
[guide](./docs/source/user_guide/tutorials.md):
Three step-by-step tutorials are available in the
[documentation](https://pysimblocks.readthedocs.io/en/latest/user_guide/tutorials/index.html):

| | Tutorial | Description |
|---|---|---|
| 1 | [Python API](./docs/source/user_guide/tutorials/tutorial_1_python.md) | Build a closed-loop PI control system in pure Python |
| 2 | [GUI](./docs/source/user_guide/tutorials/tutorial_2_gui.md) | Build the same system visually with the graphical editor |
| 3 | [SOFA](./docs/source/user_guide/tutorials/tutorial_3_sofa.md) | Replace the plant with a SOFA physics simulation |
| 1 | [Python API](https://pysimblocks.readthedocs.io/en/latest/user_guide/tutorials/tutorial_1_python.html) | Build a closed-loop PI control system in pure Python |
| 2 | [GUI](https://pysimblocks.readthedocs.io/en/latest/user_guide/tutorials/tutorial_2_gui.html) | Build the same system visually with the graphical editor |
| 3 | [SOFA](https://pysimblocks.readthedocs.io/en/latest/user_guide/tutorials/tutorial_3_sofa.html) | Replace the plant with a SOFA physics simulation |


#### Other Examples
Expand Down
1 change: 0 additions & 1 deletion docs/source/_ext/api_generator.py
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,6 @@ def _module_page(module: str) -> str:
content = _title(title)
content += f".. automodule:: {module}\n"
content += " :members:\n"
content += " :undoc-members:\n"
content += " :show-inheritance:\n"
content += " :member-order: bysource\n"
content += "\n"
Expand Down
146 changes: 146 additions & 0 deletions docs/source/concepts/adding_block.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Adding a New Block

## Overview

Adding a block to pySimBlocks requires creating or modifying three things:

- `pySimBlocks/blocks/<category>/my_block.py` — the core simulation logic
- `pySimBlocks/gui/blocks/<category>/my_block.py` — the GUI metadata
- `pySimBlocks/project/pySimBlocks_blocks_index.yaml` — the block registry entry

The core and GUI layers are fully independent. The core block runs without
the GUI and the GUI block contains no simulation logic.

For a detailed description of the block interface, see {doc}`block_model`.

## Core block

Place the file in the sub-package matching the block's category
(`sources`, `operators`, `controllers`, etc.). The filename must be the
class name in snake_case.

A core block must:

- inherit from {py:class}`~pySimBlocks.core.block.Block`
- declare all input and output ports in `__init__`
- set `direct_feedthrough` at class level
- implement `initialize()` and `output_update()`
- implement `state_update()` if the block has internal state
- use `sample_time=None` to inherit the global `dt`

The following example implements a `ScalarGain` block that multiplies its
input by a constant:
```python
import numpy as np
from pySimBlocks.core.block import Block

class ScalarGain(Block):

direct_feedthrough = True

def __init__(self, name: str, gain: float, sample_time: float | None = None):
super().__init__(name, sample_time)
self.gain = float(gain)
self.inputs["in"] = None
self.outputs["out"] = None

def initialize(self, t0: float) -> None:
self.outputs["out"] = np.zeros((1, 1))

def output_update(self, t: float, dt: float) -> None:
self.outputs["out"] = self.gain * self.inputs["in"]

def state_update(self, t: float, dt: float) -> None:
pass
```

Register it in `pySimBlocks/blocks/operators/__init__.py`:
```python
from .scalar_gain import ScalarGain
```

## GUI block

Place the file in `pySimBlocks/gui/blocks/<category>/my_block.py`.
The class name must be the `myBlockMeta`. It must inherit from
{py:class}`~pySimBlocks.gui.blocks.block_meta.BlockMeta` and declare the
following class attributes in `__init__`:

- `name` — user-facing block name
- `category` — must match the core block category
- `type` — stable identifier used in `project.yaml` (snake_case)
- `summary` — one-line description shown in the block list
- `description` — rich text shown in the block dialog (Markdown, supports LaTeX)
- `inputs` — list of {py:class}`~pySimBlocks.gui.blocks.port_meta.PortMeta`
- `outputs` — list of {py:class}`~pySimBlocks.gui.blocks.port_meta.PortMeta`
- `parameters` — list of {py:class}`~pySimBlocks.gui.blocks.parameter_meta.ParameterMeta`

### Minimal

The following example is the GUI counterpart of the `ScalarGain` block:
```python
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 ScalarGainMeta(BlockMeta):

def __init__(self):
self.name = "ScalarGain"
self.category = "operators"
self.type = "scalar_gain"
self.summary = "Multiplies input by a scalar constant."
self.description = (
"Computes:\n"
"$$\n"
"y = K \\cdot u\n"
"$$\n"
)
self.inputs = [
PortMeta(name="in", display_as="in", shape=["n", "m"])
]
self.outputs = [
PortMeta(name="out", display_as="out", shape=["n", "m"])
]
self.parameters = [
ParameterMeta(name="gain", type="float", required=True, default=1.0),
ParameterMeta(name="sample_time", type="float"),
]
```

### Conditional parameters

Override `is_parameter_active()` to show or hide parameters depending on
the current block configuration. It receives the parameter name and the
current instance parameters, and returns `True` if the parameter should
be visible.

The following example hides `Ki` unless the selected controller includes
an integral term:
```python
def is_parameter_active(self, param_name: str, instance_params: dict) -> bool:
if param_name == "Ki":
return instance_params.get("controller") in ["I", "PI", "PID"]
return super().is_parameter_active(param_name, instance_params)
```

### Dynamic ports and custom dialogs
```{tip}
For dynamic ports (ports whose number depends on a parameter), override
`resolve_port_group()`. For a complete example see
{py:class}`~pySimBlocks.gui.blocks.operators.algebraic_function.AlgebraicFunctionMeta`.

For fully custom dialog layouts (file pickers, extra buttons), override
`build_param()` and/or `build_post_param()`. See the same class for reference.
```

## Registering in the index

Once the core block is registered in its `__init__.py`, run:
```bash
pysimblocks update
```

This regenerates `pySimBlocks/project/pySimBlocks_blocks_index.yaml`
automatically. The new block is then available in the GUI block list and
the project loader.
112 changes: 112 additions & 0 deletions docs/source/concepts/block_model.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Block Model

## Overview

A block is the fundamental unit of a pySimBlocks model. It encapsulates a
discrete-time computation — sources, operators, controllers, physical plants
— and exposes a uniform interface that the {doc}`simulator <simulation_lifecycle>`
calls at each step.

Every block inherits from {py:class}`~pySimBlocks.core.block.Block` and
implements at minimum `initialize()` and `output_update()`.

To add a new block to pySimBlocks — including its GUI metadata and index
registration — see {doc}`adding_block`.

## Anatomy of a block

### Inputs and outputs

Inputs and outputs are plain Python dicts mapping port names to NumPy arrays
of shape `(n, m)`. They are declared in `__init__` and updated each step.
```python
self.inputs["in"] = None # declared, not yet connected
self.outputs["out"] = None # declared, not yet computed
```

An input is `None` until a connection is established. An output is `None`
until `initialize()` or `output_update()` sets it. Accessing a `None` input
in `output_update()` should raise a `RuntimeError`.

### State

State is also a dict, split into two separate dicts: `state` holds the
current value `x[k]`, and `next_state` holds the value computed by
`state_update()` before it is committed.
```python
self.state["x"] = np.zeros((2, 1))
self.next_state["x"] = np.zeros((2, 1))
```

A block with no state simply leaves both dicts empty. The simulator checks
`block.has_state` to decide whether to call `state_update()` and
`commit_state()`.

### Parameters

Parameters are regular Python attributes set in `__init__`. There is no
dedicated container — a gain value, a matrix, a file path are all just
attributes.
```python
self.K = np.array(gain)
self.sample_time = sample_time
```

They are fixed at construction time and should not change during simulation.

## Block lifecycle methods

### initialize()

Called once before the simulation loop starts, in topological order.
Must set a valid initial value for all outputs and state entries.
Receives `t0`, the initial simulation time.
```python
def initialize(self, t0: float) -> None:
self.state["x"] = np.zeros((2, 1))
self.outputs["out"] = self.state["x"].copy()
```

### output_update()

Called every step for all active blocks, in topological order.
Must compute `outputs` from `state` and `inputs`. Must not modify `state`.
```python
def output_update(self, t: float, dt: float) -> None:
self.outputs["out"] = self.state["x"].copy()
```

### state_update()

Called every step, after all `output_update()` calls. Must write the next
state into `next_state`. Must not modify `state` or `outputs`.
```python
def state_update(self, t: float, dt: float) -> None:
self.next_state["x"] = self.state["x"] + dt * self.inputs["in"]
```

Only called if `block.has_state` is `True`. Blocks with no state can omit
this method.

### finalize()

Called once after the simulation loop ends. Optional — the base class
provides a no-op default. Use it to close files, release resources, or
flush buffers.

## direct_feedthrough flag

`direct_feedthrough` is a class-level boolean attribute that tells the
simulator whether `u[k]` appears in `output_update()`. It defaults to
`True` in the base class and should be overridden when the block's output
does not depend on its inputs at the same step.
```python
class MyIntegrator(Block):
direct_feedthrough = False
```

Setting it correctly is critical — it determines which edges appear in the
dependency graph and therefore the execution order. An incorrect value either
causes unnecessary ordering constraints or, worse, silently produces stale
inputs. See {doc}`execution_order` for details.

Loading
Loading