diff --git a/README.md b/README.md
index 0b8498d..d5788bd 100644
--- a/README.md
+++ b/README.md
@@ -110,6 +110,9 @@ To open the graphical editor, run:
pysimblocks examples/quick_start/gui
```
+The quick-start GUI project is stored in a single
+`examples/quick_start/gui/project.yaml` file.
+
### Tutorials
See the [Getting Started Guide](./docs/User_Guide/getting_started.md) for
diff --git a/docs/User_Guide/tutorial_2_gui.md b/docs/User_Guide/tutorial_2_gui.md
index 94cbc66..3facc3a 100644
--- a/docs/User_Guide/tutorial_2_gui.md
+++ b/docs/User_Guide/tutorial_2_gui.md
@@ -128,18 +128,21 @@ Under the hood, the GUI generates the same `Model` structure used in [Tutorial 1
### Saving
-Saving the project using the `Save` button in the `Toolbar` creates three YAML files in the current folder:
-- `model.yaml` — defines the blocks and their connections
-- `parameters.yaml` — contains block parameters, simulation settings, and plot definitions
-- `layout.yaml` — stores the graphical layout of blocks and connections in the Diagram View
+Saving the project using the `Save` button in the `Toolbar` creates a unified `project.yaml` file in the current folder.
-Together, these files fully describe the model structure and its configuration. They can be used to reload the project in the GUI or to run the simulation programmatically using Python.
+This file contains:
+- project metadata
+- simulation settings (`dt`, `T`, logging, plots)
+- block diagram (`diagram.blocks`, `diagram.connections`)
+- GUI layout (`gui.layout`)
+
+This single file fully describes the model and can be reloaded in the GUI or executed programmatically in Python.
### Exporting a Python Runner
The `Export` button in the `Toolbar` generates a `run.py` file.
-This script loads the YAML configuration (model and parameters) and builds the corresponding `Model` and `Simulator` objects programmatically.
+This script loads `project.yaml` and builds the corresponding `Model` and `Simulator` objects programmatically.
It allows the project to be executed directly from the command line:
```bash
@@ -182,4 +185,3 @@ Run it from the command line to verify that the exported script reproduces the s
This tutorial demonstrates how to build and execute a model visually.
The next tutorials extend this approach to SOFA integration and real-time execution.
-
diff --git a/examples/advanced/hardware/block_diagram.py b/examples/advanced/hardware/block_diagram.py
index 374ed3d..fd0d529 100644
--- a/examples/advanced/hardware/block_diagram.py
+++ b/examples/advanced/hardware/block_diagram.py
@@ -1,13 +1,18 @@
import time
+from pathlib import Path
import numpy as np
import parameters as prm
from emioapi import EmioMotors
-from pySimBlocks.core import Model, Simulator
-from pySimBlocks.project.load_project_config import load_simulation_config
+from pySimBlocks.project import load_simulator_from_project
from pySimBlocks.real_time import RealTimeRunner
+try:
+ BASE_DIR = Path(__file__).parent.resolve()
+except Exception:
+ BASE_DIR = Path("")
+
# ------------------------------------------------------------------------------
# Process
@@ -85,9 +90,7 @@ def process_block_diagram(shared_markers_pos, shared_ref_ol, shared_ref_cl,
# Helpers
# ------------------------------------------------------------------------------
def setup_block_diagram():
- sim_cfg, model_cfg = load_simulation_config("parameters.yaml")
- model = Model( name="model", model_yaml="model.yaml", model_cfg=model_cfg)
- sim = Simulator(model, sim_cfg)
+ sim, _plot_cfg = load_simulator_from_project(BASE_DIR / "project.yaml")
runner = RealTimeRunner(
sim,
input_blocks=["Camera", "Ref_cl", "Ref_ol", "Mode"],
diff --git a/examples/advanced/hardware/layout.yaml b/examples/advanced/hardware/layout.yaml
deleted file mode 100644
index b39fef8..0000000
--- a/examples/advanced/hardware/layout.yaml
+++ /dev/null
@@ -1,63 +0,0 @@
-version: 1
-blocks:
- Cmd:
- x: 1000.0
- y: 75.0
- orientation: normal
- SF:
- x: 225.0
- y: -20.0
- orientation: normal
- Ref_cl:
- x: 25.0
- y: -30.0
- orientation: normal
- Ref_ol:
- x: 230.0
- y: 65.0
- orientation: normal
- Select:
- x: 420.0
- y: 65.0
- orientation: normal
- Mode:
- x: 230.0
- y: 140.0
- orientation: normal
- Filter:
- x: 625.0
- y: 75.0
- orientation: normal
- Delay:
- x: 635.0
- y: 155.0
- orientation: flipped
- Rate:
- x: 800.0
- y: 75.0
- orientation: normal
- Luenberger:
- x: 465.0
- y: -135.0
- orientation: flipped
- Mux:
- x: 235.0
- y: -135.0
- orientation: flipped
- Delay_1:
- x: 675.0
- y: -80.0
- orientation: flipped
- Camera:
- x: 650.0
- y: -190.0
- orientation: flipped
-connections:
- Rate.out -> Delay_1.in:
- ports: [Rate.out, Delay_1.in]
- route: [[935.0, 105.0], [950.0, 105.0], [950.0, 27.5], [950.0, 27.5], [950.0,
- -60.0], [801.0, -60.0]]
- Delay_1.out -> Mux.in2:
- ports: [Delay_1.out, Mux.in2]
- route: [[660.0, -50.0], [652.0, -50.0], [380.0, -50.0], [380.0, -95.0], [369.0,
- -95.0], [361.0, -95.0]]
diff --git a/examples/advanced/hardware/model.yaml b/examples/advanced/hardware/model.yaml
deleted file mode 100644
index 1506d16..0000000
--- a/examples/advanced/hardware/model.yaml
+++ /dev/null
@@ -1,56 +0,0 @@
-blocks:
-- name: Cmd
- category: interfaces
- type: external_output
-- name: SF
- category: controllers
- type: state_feedback
-- name: Ref_cl
- category: interfaces
- type: external_input
-- name: Ref_ol
- category: interfaces
- type: external_input
-- name: Select
- category: operators
- type: algebraic_function
-- name: Mode
- category: interfaces
- type: external_input
-- name: Filter
- category: operators
- type: algebraic_function
-- name: Delay
- category: operators
- type: delay
-- name: Rate
- category: operators
- type: rate_limiter
-- name: Luenberger
- category: observers
- type: luenberger
-- name: Mux
- category: operators
- type: mux
-- name: Delay_1
- category: operators
- type: delay
-- name: Camera
- category: interfaces
- type: external_input
-connections:
-- [Ref_cl.out, SF.r]
-- [Ref_ol.out, Select.u_ol]
-- [Mode.out, Select.mode]
-- [SF.u, Select.u_cl]
-- [Select.u, Filter.u]
-- [Delay.out, Filter.u_prev]
-- [Filter.u_filter, Delay.in]
-- [Filter.u_filter, Rate.in]
-- [Rate.out, Cmd.in]
-- [Luenberger.x_hat, Mux.in1]
-- [Mux.out, SF.x]
-- [Rate.out, Delay_1.in]
-- [Delay_1.out, Luenberger.u]
-- [Delay_1.out, Mux.in2]
-- [Camera.out, Luenberger.y]
diff --git a/examples/advanced/hardware/parameters.yaml b/examples/advanced/hardware/parameters.yaml
deleted file mode 100644
index 240804a..0000000
--- a/examples/advanced/hardware/parameters.yaml
+++ /dev/null
@@ -1,57 +0,0 @@
-simulation:
- dt: 1/60
- T: 10.0
- solver: fixed
- clock: external
-blocks:
- Cmd: {}
- SF:
- K: '#K'
- G: '#G'
- Ref_cl: {}
- Ref_ol: {}
- Select:
- file_path: block_diagram.py
- function_name: select_cmd
- input_keys:
- - u_cl
- - u_ol
- - mode
- output_keys:
- - u
- Mode: {}
- Filter:
- file_path: block_diagram.py
- function_name: filter_first_order
- input_keys:
- - u
- - u_prev
- output_keys:
- - u_filter
- Delay:
- num_delays: 1
- initial_output: np.zeros((2,1))
- Rate:
- rising_slope: 7.906
- falling_slope: -7.906
- Luenberger:
- A: '#A'
- B: '#B'
- C: '#C'
- L: '#L'
- x0: '#x0'
- Mux:
- num_inputs: 2
- Delay_1:
- num_delays: 1
- initial_output: np.zeros((2,1))
- Camera: {}
-logging:
-- Cmd.outputs.out
-- SF.outputs.u
-- Ref_cl.outputs.out
-- Ref_ol.outputs.out
-- Select.outputs.u
-- Mode.outputs.out
-plots: []
-external: parameters.py
diff --git a/examples/advanced/hardware/project.yaml b/examples/advanced/hardware/project.yaml
new file mode 100644
index 0000000..e2e9a41
--- /dev/null
+++ b/examples/advanced/hardware/project.yaml
@@ -0,0 +1,249 @@
+schema_version: 1
+project:
+ name: hardware
+ metadata:
+ created_by: pySimBlocks
+ created_at: '2026-02-18T00:00:00Z'
+simulation:
+ dt: 1/60
+ T: 10.0
+ solver: fixed
+ clock: external
+ external_module: parameters.py
+ logging:
+ - Cmd.outputs.out
+ - SF.outputs.u
+ - Ref_cl.outputs.out
+ - Ref_ol.outputs.out
+ - Select.outputs.u
+ - Mode.outputs.out
+ plots: []
+diagram:
+ blocks:
+ - name: Cmd
+ category: interfaces
+ type: external_output
+ parameters: {}
+ - name: SF
+ category: controllers
+ type: state_feedback
+ parameters:
+ K: '#K'
+ G: '#G'
+ - name: Ref_cl
+ category: interfaces
+ type: external_input
+ parameters: {}
+ - name: Ref_ol
+ category: interfaces
+ type: external_input
+ parameters: {}
+ - name: Select
+ category: operators
+ type: algebraic_function
+ parameters:
+ file_path: block_diagram.py
+ function_name: select_cmd
+ input_keys:
+ - u_cl
+ - u_ol
+ - mode
+ output_keys:
+ - u
+ - name: Mode
+ category: interfaces
+ type: external_input
+ parameters: {}
+ - name: Filter
+ category: operators
+ type: algebraic_function
+ parameters:
+ file_path: block_diagram.py
+ function_name: filter_first_order
+ input_keys:
+ - u
+ - u_prev
+ output_keys:
+ - u_filter
+ - name: Delay
+ category: operators
+ type: delay
+ parameters:
+ num_delays: 1
+ initial_output: np.zeros((2,1))
+ - name: Rate
+ category: operators
+ type: rate_limiter
+ parameters:
+ rising_slope: 7.906
+ falling_slope: -7.906
+ - name: Luenberger
+ category: observers
+ type: luenberger
+ parameters:
+ A: '#A'
+ B: '#B'
+ C: '#C'
+ L: '#L'
+ x0: '#x0'
+ - name: Mux
+ category: operators
+ type: mux
+ parameters:
+ num_inputs: 2
+ - name: Delay_1
+ category: operators
+ type: delay
+ parameters:
+ num_delays: 1
+ initial_output: np.zeros((2,1))
+ - name: Camera
+ category: interfaces
+ type: external_input
+ parameters: {}
+ connections:
+ - name: c1
+ ports:
+ - Ref_cl.out
+ - SF.r
+ - name: c2
+ ports:
+ - Ref_ol.out
+ - Select.u_ol
+ - name: c3
+ ports:
+ - Mode.out
+ - Select.mode
+ - name: c4
+ ports:
+ - SF.u
+ - Select.u_cl
+ - name: c5
+ ports:
+ - Select.u
+ - Filter.u
+ - name: c6
+ ports:
+ - Delay.out
+ - Filter.u_prev
+ - name: c7
+ ports:
+ - Filter.u_filter
+ - Delay.in
+ - name: c8
+ ports:
+ - Filter.u_filter
+ - Rate.in
+ - name: c9
+ ports:
+ - Rate.out
+ - Cmd.in
+ - name: c10
+ ports:
+ - Luenberger.x_hat
+ - Mux.in1
+ - name: c11
+ ports:
+ - Mux.out
+ - SF.x
+ - name: c12
+ ports:
+ - Rate.out
+ - Delay_1.in
+ - name: c13
+ ports:
+ - Delay_1.out
+ - Luenberger.u
+ - name: c14
+ ports:
+ - Delay_1.out
+ - Mux.in2
+ - name: c15
+ ports:
+ - Camera.out
+ - Luenberger.y
+gui:
+ layout:
+ blocks:
+ Cmd:
+ x: 1000.0
+ y: 75.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ SF:
+ x: 225.0
+ y: -20.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Ref_cl:
+ x: 25.0
+ y: -30.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Ref_ol:
+ x: 230.0
+ y: 65.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Select:
+ x: 420.0
+ y: 65.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Mode:
+ x: 230.0
+ y: 140.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Filter:
+ x: 625.0
+ y: 75.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Delay:
+ x: 635.0
+ y: 155.0
+ orientation: flipped
+ width: 120.0
+ height: 60.0
+ Rate:
+ x: 800.0
+ y: 75.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Luenberger:
+ x: 465.0
+ y: -135.0
+ orientation: flipped
+ width: 120.0
+ height: 60.0
+ Mux:
+ x: 235.0
+ y: -135.0
+ orientation: flipped
+ width: 120.0
+ height: 60.0
+ Delay_1:
+ x: 675.0
+ y: -80.0
+ orientation: flipped
+ width: 120.0
+ height: 60.0
+ Camera:
+ x: 650.0
+ y: -190.0
+ orientation: flipped
+ width: 120.0
+ height: 60.0
+ connections:
+ c14:
+ route: [[660.0, -50.0], [652.0, -50.0], [410.0, -50.0], [410.0, -95.0], [
+ 369.0, -95.0], [361.0, -95.0]]
diff --git a/examples/advanced/qp_problem/layout.yaml b/examples/advanced/qp_problem/layout.yaml
deleted file mode 100644
index d9d3c20..0000000
--- a/examples/advanced/qp_problem/layout.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-version: 1
-blocks:
- QuadraticProgram:
- x: 169.0
- y: -643.0
- orientation: normal
- P:
- x: -80.0
- y: -720.0
- orientation: normal
- q:
- x: -86.0
- y: -608.0
- orientation: normal
diff --git a/examples/advanced/qp_problem/model.yaml b/examples/advanced/qp_problem/model.yaml
deleted file mode 100644
index fa929d7..0000000
--- a/examples/advanced/qp_problem/model.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-blocks:
-- name: QuadraticProgram
- category: optimizers
- type: quadratic_program
-- name: P
- category: sources
- type: constant
-- name: q
- category: sources
- type: constant
-connections:
-- [P.out, QuadraticProgram.P]
-- [q.out, QuadraticProgram.q]
diff --git a/examples/advanced/qp_problem/parameters.yaml b/examples/advanced/qp_problem/parameters.yaml
deleted file mode 100644
index feac295..0000000
--- a/examples/advanced/qp_problem/parameters.yaml
+++ /dev/null
@@ -1,19 +0,0 @@
-simulation:
- dt: 0.1
- T: 10.0
- solver: fixed
-blocks:
- QuadraticProgram:
- solver: clarabel
- P:
- value: '#P'
- q:
- value: '#q'
-logging:
-- QuadraticProgram.outputs.x
-- QuadraticProgram.outputs.status
-- QuadraticProgram.outputs.cost
-- P.outputs.out
-- q.outputs.out
-plots: []
-external: params.py
diff --git a/examples/advanced/qp_problem/project.yaml b/examples/advanced/qp_problem/project.yaml
new file mode 100644
index 0000000..a98c28d
--- /dev/null
+++ b/examples/advanced/qp_problem/project.yaml
@@ -0,0 +1,65 @@
+schema_version: 1
+project:
+ name: qp_problem
+ metadata:
+ created_by: pySimBlocks
+ created_at: '2026-02-18T00:00:00Z'
+simulation:
+ dt: 0.1
+ T: 10.0
+ solver: fixed
+ external_module: params.py
+ logging:
+ - QuadraticProgram.outputs.x
+ - QuadraticProgram.outputs.status
+ - QuadraticProgram.outputs.cost
+ - P.outputs.out
+ - q.outputs.out
+ plots: []
+diagram:
+ blocks:
+ - name: QuadraticProgram
+ category: optimizers
+ type: quadratic_program
+ parameters:
+ solver: clarabel
+ - name: P
+ category: sources
+ type: constant
+ parameters:
+ value: '#P'
+ - name: q
+ category: sources
+ type: constant
+ parameters:
+ value: '#q'
+ connections:
+ - name: c1
+ ports:
+ - P.out
+ - QuadraticProgram.P
+ - name: c2
+ ports:
+ - q.out
+ - QuadraticProgram.q
+gui:
+ layout:
+ blocks:
+ QuadraticProgram:
+ x: 155.0
+ y: -710.0
+ orientation: normal
+ width: 245.0
+ height: 175.0
+ P:
+ x: -80.0
+ y: -720.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ q:
+ x: -75.0
+ y: -610.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
diff --git a/examples/advanced/qp_problem/run.py b/examples/advanced/qp_problem/run.py
index 339d9e1..5d68f4f 100644
--- a/examples/advanced/qp_problem/run.py
+++ b/examples/advanced/qp_problem/run.py
@@ -1,6 +1,5 @@
from pathlib import Path
-from pySimBlocks.core import Model, Simulator
-from pySimBlocks.project.load_project_config import load_project_config
+from pySimBlocks.project import load_simulator_from_project
from pySimBlocks.project.plot_from_config import plot_from_config
try:
@@ -8,16 +7,8 @@
except Exception:
BASE_DIR = Path("")
-sim_cfg, model_cfg, plot_cfg = load_project_config(BASE_DIR / "parameters.yaml")
-
-model = Model(
- name="model",
- model_yaml=BASE_DIR / "model.yaml",
- model_cfg=model_cfg
-)
-
-sim = Simulator(model, sim_cfg)
+sim, plot_cfg = load_simulator_from_project(BASE_DIR / "project.yaml")
logs = sim.run()
-if True:
+if True and plot_cfg is not None:
plot_from_config(logs, plot_cfg)
diff --git a/examples/advanced/sofa/gui/finger/FingerController.py b/examples/advanced/sofa/gui/finger/FingerController.py
index 6d0dc37..e04ab55 100644
--- a/examples/advanced/sofa/gui/finger/FingerController.py
+++ b/examples/advanced/sofa/gui/finger/FingerController.py
@@ -14,8 +14,7 @@ class FingerController(SofaPysimBlocksController):
def __init__(self, root, actuator, mo, tip_index=121, name="FingerController"):
super().__init__(root, name=name)
- self.parameters_yaml = str((BASE_DIR / "../sofa_exchange/parameters.yaml").resolve())
- self.model_yaml = str((BASE_DIR / "../sofa_exchange/model.yaml").resolve())
+ self.project_yaml = str((BASE_DIR / "../sofa_plant/project.yaml").resolve())
self.mo = mo
self.actuator = actuator
diff --git a/examples/advanced/sofa/gui/sofa_exchange/layout.yaml b/examples/advanced/sofa/gui/sofa_exchange/layout.yaml
deleted file mode 100644
index 16085bc..0000000
--- a/examples/advanced/sofa/gui/sofa_exchange/layout.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-version: 1
-blocks:
- error:
- x: 95.0
- y: -5.0
- orientation: normal
- width: 120.0
- height: 60.0
- sofa:
- x: 460.0
- y: -5.0
- orientation: normal
- width: 120.0
- height: 60.0
- ref:
- x: -100.0
- y: -15.0
- orientation: normal
- width: 120.0
- height: 60.0
- PID:
- x: 280.0
- y: -5.0
- orientation: normal
- width: 120.0
- height: 60.0
diff --git a/examples/advanced/sofa/gui/sofa_exchange/model.yaml b/examples/advanced/sofa/gui/sofa_exchange/model.yaml
deleted file mode 100644
index 0aaa54b..0000000
--- a/examples/advanced/sofa/gui/sofa_exchange/model.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-blocks:
-- name: error
- category: operators
- type: sum
-- name: sofa
- category: systems
- type: sofa_exchange_i_o
-- name: ref
- category: sources
- type: constant
-- name: PID
- category: controllers
- type: pid
-connections:
-- [sofa.measure, error.in2]
-- [error.out, PID.e]
-- [ref.out, error.in1]
-- [PID.u, sofa.cable]
diff --git a/examples/advanced/sofa/gui/sofa_exchange/parameters.yaml b/examples/advanced/sofa/gui/sofa_exchange/parameters.yaml
deleted file mode 100644
index ac7ddf4..0000000
--- a/examples/advanced/sofa/gui/sofa_exchange/parameters.yaml
+++ /dev/null
@@ -1,47 +0,0 @@
-simulation:
- dt: 0.01
- T: 5.0
- solver: fixed
-blocks:
- error:
- signs: +-
- sofa:
- scene_file: ../finger/Finger.py
- input_keys:
- - cable
- output_keys:
- - tip
- - measure
- slider_params:
- ref.value:
- - -10.0
- - 50.0
- PID.Kp:
- - 0.01
- - 3.0
- PID.Ki:
- - 0.01
- - 3.0
- PID.Kd:
- - 0.001
- - 0.01
- ref:
- value: [[1.0]]
- PID:
- controller: PID
- Kp: [[0.5]]
- Ki: [[0.8]]
- Kd: [[0.005]]
- integration_method: euler forward
-logging:
-- sofa.outputs.measure
-- ref.outputs.out
-- PID.outputs.u
-plots:
-- title: Outputs
- signals:
- - sofa.outputs.measure
- - ref.outputs.out
-- title: Command
- signals:
- - PID.outputs.u
diff --git a/examples/advanced/sofa/gui/sofa_exchange/project.yaml b/examples/advanced/sofa/gui/sofa_exchange/project.yaml
new file mode 100644
index 0000000..0c8c077
--- /dev/null
+++ b/examples/advanced/sofa/gui/sofa_exchange/project.yaml
@@ -0,0 +1,98 @@
+schema_version: 1
+
+project:
+ name: sofa_exchange
+ metadata:
+ created_by: pySimBlocks
+ created_at: "2026-02-18T00:00:00Z"
+
+simulation:
+ dt: 0.01
+ T: 5.0
+ solver: fixed
+ logging:
+ - sofa.outputs.measure
+ - ref.outputs.out
+ - PID.outputs.u
+ plots:
+ - title: Outputs
+ signals:
+ - sofa.outputs.measure
+ - ref.outputs.out
+ - title: Command
+ signals:
+ - PID.outputs.u
+
+diagram:
+ blocks:
+ - name: error
+ category: operators
+ type: sum
+ parameters:
+ signs: +-
+ - name: sofa
+ category: systems
+ type: sofa_exchange_i_o
+ parameters:
+ scene_file: ../finger/Finger.py
+ input_keys:
+ - cable
+ output_keys:
+ - tip
+ - measure
+ slider_params:
+ ref.value: [-10.0, 50.0]
+ PID.Kp: [0.01, 3.0]
+ PID.Ki: [0.01, 3.0]
+ PID.Kd: [0.001, 0.01]
+ - name: ref
+ category: sources
+ type: constant
+ parameters:
+ value: [[1.0]]
+ - name: PID
+ category: controllers
+ type: pid
+ parameters:
+ controller: PID
+ Kp: [[0.5]]
+ Ki: [[0.8]]
+ Kd: [[0.005]]
+ integration_method: euler forward
+ connections:
+ - name: c1
+ ports: [sofa.measure, error.in2]
+ - name: c2
+ ports: [error.out, PID.e]
+ - name: c3
+ ports: [ref.out, error.in1]
+ - name: c4
+ ports: [PID.u, sofa.cable]
+
+gui:
+ layout:
+ blocks:
+ error:
+ x: 95.0
+ y: -5.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ sofa:
+ x: 460.0
+ y: -5.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ ref:
+ x: -100.0
+ y: -15.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ PID:
+ x: 280.0
+ y: -5.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
diff --git a/examples/advanced/sofa/gui/sofa_plant/layout.yaml b/examples/advanced/sofa/gui/sofa_plant/layout.yaml
deleted file mode 100644
index ee7ed72..0000000
--- a/examples/advanced/sofa/gui/sofa_plant/layout.yaml
+++ /dev/null
@@ -1,53 +0,0 @@
-version: 1
-blocks:
- error:
- x: -245.0
- y: -35.0
- orientation: normal
- width: 120.0
- height: 60.0
- Kp:
- x: -30.0
- y: -85.0
- orientation: normal
- width: 120.0
- height: 60.0
- Ki:
- x: 140.0
- y: 5.0
- orientation: normal
- width: 120.0
- height: 60.0
- integrator:
- x: -35.0
- y: 5.0
- orientation: normal
- width: 120.0
- height: 60.0
- sum:
- x: 300.0
- y: -75.0
- orientation: normal
- width: 120.0
- height: 60.0
- sofa:
- x: 490.0
- y: -75.0
- orientation: normal
- width: 120.0
- height: 60.0
- ref:
- x: -420.0
- y: -45.0
- orientation: normal
- width: 120.0
- height: 60.0
-connections:
- error.out -> Kp.in:
- ports: [error.out, Kp.in]
- route: [[-110.0, -5.0], [-102.0, -5.0], [-75.0, -5.0], [-75.0, -55.0], [-44.0,
- -55.0], [-36.0, -55.0]]
- sofa.measure -> error.in2:
- ports: [sofa.measure, error.in2]
- route: [[625.0, -35.0], [633.0, -35.0], [633.0, 85.0], [-259.0, 85.0], [-259.0,
- 5.0], [-251.0, 5.0]]
diff --git a/examples/advanced/sofa/gui/sofa_plant/model.yaml b/examples/advanced/sofa/gui/sofa_plant/model.yaml
deleted file mode 100644
index f810fa6..0000000
--- a/examples/advanced/sofa/gui/sofa_plant/model.yaml
+++ /dev/null
@@ -1,31 +0,0 @@
-blocks:
-- name: error
- category: operators
- type: sum
-- name: Kp
- category: operators
- type: gain
-- name: Ki
- category: operators
- type: gain
-- name: integrator
- category: operators
- type: discrete_integrator
-- name: sum
- category: operators
- type: sum
-- name: sofa
- category: systems
- type: sofa_plant
-- name: ref
- category: sources
- type: constant
-connections:
-- [error.out, Kp.in]
-- [error.out, integrator.in]
-- [Kp.out, sum.in1]
-- [Ki.out, sum.in2]
-- [sum.out, sofa.cable]
-- [sofa.measure, error.in2]
-- [ref.out, error.in1]
-- [integrator.out, Ki.in]
diff --git a/examples/advanced/sofa/gui/sofa_plant/parameters.yaml b/examples/advanced/sofa/gui/sofa_plant/parameters.yaml
deleted file mode 100644
index 26b589b..0000000
--- a/examples/advanced/sofa/gui/sofa_plant/parameters.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
-simulation:
- dt: 0.01
- T: 5.0
- solver: fixed
-blocks:
- error:
- signs: +-
- Kp:
- gain: [[0.5]]
- multiplication: Element wise (K * u)
- Ki:
- gain: [[0.8]]
- multiplication: Element wise (K * u)
- integrator:
- initial_state: [[0.0]]
- method: euler forward
- sum:
- signs: ++
- sofa:
- scene_file: ../finger/Finger.py
- input_keys:
- - cable
- output_keys:
- - tip
- - measure
- slider_params:
- ref.value:
- - -10.0
- - 50.0
- Kp.gain:
- - 0.01
- - 3.0
- Ki.gain:
- - 0.01
- - 3.0
- ref:
- value: [[1.0]]
-logging:
-- sum.outputs.out
-- sofa.outputs.measure
-- ref.outputs.out
-plots:
-- title: Outputs
- signals:
- - sofa.outputs.measure
- - ref.outputs.out
-- title: Command
- signals:
- - sum.outputs.out
diff --git a/examples/advanced/sofa/gui/sofa_plant/project.yaml b/examples/advanced/sofa/gui/sofa_plant/project.yaml
new file mode 100644
index 0000000..e713a0f
--- /dev/null
+++ b/examples/advanced/sofa/gui/sofa_plant/project.yaml
@@ -0,0 +1,162 @@
+schema_version: 1
+project:
+ name: sofa_plant
+ metadata:
+ created_by: pySimBlocks
+ created_at: '2026-02-18T00:00:00Z'
+simulation:
+ dt: 0.01
+ T: 5.0
+ solver: fixed
+ logging:
+ - sum.outputs.out
+ - sofa.outputs.measure
+ - ref.outputs.out
+ plots:
+ - title: Outputs
+ signals:
+ - sofa.outputs.measure
+ - ref.outputs.out
+ - title: Command
+ signals:
+ - sum.outputs.out
+diagram:
+ blocks:
+ - name: error
+ category: operators
+ type: sum
+ parameters:
+ signs: +-
+ - name: Kp
+ category: operators
+ type: gain
+ parameters:
+ gain: [[0.5]]
+ multiplication: Element wise (K * u)
+ - name: Ki
+ category: operators
+ type: gain
+ parameters:
+ gain: [[0.8]]
+ multiplication: Element wise (K * u)
+ - name: integrator
+ category: operators
+ type: discrete_integrator
+ parameters:
+ initial_state: [[0.0]]
+ method: euler forward
+ - name: sum
+ category: operators
+ type: sum
+ parameters:
+ signs: ++
+ - name: sofa
+ category: systems
+ type: sofa_plant
+ parameters:
+ scene_file: ../finger/Finger.py
+ input_keys:
+ - cable
+ output_keys:
+ - tip
+ - measure
+ slider_params:
+ ref.value:
+ - -10.0
+ - 50.0
+ Kp.gain:
+ - 0.01
+ - 3.0
+ Ki.gain:
+ - 0.01
+ - 3.0
+ - name: ref
+ category: sources
+ type: constant
+ parameters:
+ value: [[1.0]]
+ connections:
+ - name: c1
+ ports:
+ - error.out
+ - Kp.in
+ - name: c2
+ ports:
+ - error.out
+ - integrator.in
+ - name: c3
+ ports:
+ - Kp.out
+ - sum.in1
+ - name: c4
+ ports:
+ - Ki.out
+ - sum.in2
+ - name: c5
+ ports:
+ - sum.out
+ - sofa.cable
+ - name: c6
+ ports:
+ - sofa.measure
+ - error.in2
+ - name: c7
+ ports:
+ - ref.out
+ - error.in1
+ - name: c8
+ ports:
+ - integrator.out
+ - Ki.in
+gui:
+ layout:
+ blocks:
+ error:
+ x: -245.0
+ y: -35.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Kp:
+ x: -30.0
+ y: -85.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Ki:
+ x: 140.0
+ y: 5.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ integrator:
+ x: -35.0
+ y: 5.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ sum:
+ x: 300.0
+ y: -75.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ sofa:
+ x: 490.0
+ y: -75.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ ref:
+ x: -420.0
+ y: -45.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ connections:
+ c1:
+ route: [[-110.0, -5.0], [-102.0, -5.0], [-75.0, -5.0], [-75.0, -55.0], [-44.0,
+ -55.0], [-36.0, -55.0]]
+ c6:
+ route: [[625.0, -35.0], [633.0, -35.0], [633.0, 100.0], [-259.0, 100.0], [
+ -259.0, 5.0], [-251.0, 5.0]]
diff --git a/examples/advanced/sofa/gui/sofa_plant/run.py b/examples/advanced/sofa/gui/sofa_plant/run.py
index 339d9e1..5d68f4f 100644
--- a/examples/advanced/sofa/gui/sofa_plant/run.py
+++ b/examples/advanced/sofa/gui/sofa_plant/run.py
@@ -1,6 +1,5 @@
from pathlib import Path
-from pySimBlocks.core import Model, Simulator
-from pySimBlocks.project.load_project_config import load_project_config
+from pySimBlocks.project import load_simulator_from_project
from pySimBlocks.project.plot_from_config import plot_from_config
try:
@@ -8,16 +7,8 @@
except Exception:
BASE_DIR = Path("")
-sim_cfg, model_cfg, plot_cfg = load_project_config(BASE_DIR / "parameters.yaml")
-
-model = Model(
- name="model",
- model_yaml=BASE_DIR / "model.yaml",
- model_cfg=model_cfg
-)
-
-sim = Simulator(model, sim_cfg)
+sim, plot_cfg = load_simulator_from_project(BASE_DIR / "project.yaml")
logs = sim.run()
-if True:
+if True and plot_cfg is not None:
plot_from_config(logs, plot_cfg)
diff --git a/examples/basics/algebraic_function/layout.yaml b/examples/basics/algebraic_function/layout.yaml
deleted file mode 100644
index 3bcaea6..0000000
--- a/examples/basics/algebraic_function/layout.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-version: 1
-blocks:
- Algebraic Function:
- x: 0.0
- y: 0.0
- Constant:
- x: -197.0
- y: -35.0
- Step:
- x: -196.0
- y: 43.0
diff --git a/examples/basics/algebraic_function/model.yaml b/examples/basics/algebraic_function/model.yaml
deleted file mode 100644
index 320874e..0000000
--- a/examples/basics/algebraic_function/model.yaml
+++ /dev/null
@@ -1,13 +0,0 @@
-blocks:
-- name: Algebraic Function
- category: operators
- type: algebraic_function
-- name: Constant
- category: sources
- type: constant
-- name: Step
- category: sources
- type: step
-connections:
-- [Constant.out, Algebraic Function.u1]
-- [Step.out, Algebraic Function.u2]
diff --git a/examples/basics/algebraic_function/parameters.yaml b/examples/basics/algebraic_function/parameters.yaml
deleted file mode 100644
index e516254..0000000
--- a/examples/basics/algebraic_function/parameters.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-simulation:
- dt: 0.1
- solver: fixed
- T: 10.0
-blocks:
- Algebraic Function:
- file_path: g.py
- function_name: g
- input_keys:
- - u1
- - u2
- output_keys:
- - y1
- - y2
- Constant:
- value: [[1.0]]
- Step:
- value_before: [[0.0]]
- value_after: [[1.0]]
- start_time: 1.0
-logging:
-- Algebraic Function.outputs.y1
-- Algebraic Function.outputs.y2
-- Constant.outputs.out
-- Step.outputs.out
-plots: []
diff --git a/examples/basics/algebraic_function/project.yaml b/examples/basics/algebraic_function/project.yaml
new file mode 100644
index 0000000..9869612
--- /dev/null
+++ b/examples/basics/algebraic_function/project.yaml
@@ -0,0 +1,72 @@
+schema_version: 1
+project:
+ name: algebraic_function
+ metadata:
+ created_by: pySimBlocks
+ created_at: '2026-02-18T00:00:00Z'
+simulation:
+ dt: 0.1
+ T: 10.0
+ solver: fixed
+ logging:
+ - Algebraic Function.outputs.y1
+ - Algebraic Function.outputs.y2
+ - Constant.outputs.out
+ - Step.outputs.out
+ plots: []
+diagram:
+ blocks:
+ - name: Algebraic Function
+ category: operators
+ type: algebraic_function
+ parameters:
+ file_path: g.py
+ function_name: g
+ input_keys:
+ - u1
+ - u2
+ output_keys:
+ - y1
+ - y2
+ - name: Constant
+ category: sources
+ type: constant
+ parameters:
+ value: [[1.0]]
+ - name: Step
+ category: sources
+ type: step
+ parameters:
+ value_before: [[0.0]]
+ value_after: [[1.0]]
+ start_time: 1.0
+ connections:
+ - name: c1
+ ports:
+ - Constant.out
+ - Algebraic Function.u1
+ - name: c2
+ ports:
+ - Step.out
+ - Algebraic Function.u2
+gui:
+ layout:
+ blocks:
+ Algebraic Function:
+ x: 0.0
+ y: 0.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Constant:
+ x: -197.0
+ y: -35.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Step:
+ x: -196.0
+ y: 43.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
diff --git a/examples/basics/algebraic_function/run.py b/examples/basics/algebraic_function/run.py
index 50637ef..8d74233 100644
--- a/examples/basics/algebraic_function/run.py
+++ b/examples/basics/algebraic_function/run.py
@@ -1,23 +1,14 @@
from pathlib import Path
-from pySimBlocks.core import Model, Simulator
-from pySimBlocks.project.load_project_config import load_project_config
+from pySimBlocks.project import load_simulator_from_project
from pySimBlocks.project.plot_from_config import plot_from_config
try:
BASE_DIR = Path(__file__).parent.resolve()
-except:
+except Exception:
BASE_DIR = Path("")
-sim_cfg, model_cfg, plot_cfg = load_project_config(BASE_DIR / "parameters.yaml")
-
-model = Model(
- name="model",
- model_yaml=BASE_DIR / "model.yaml",
- model_cfg=model_cfg
-)
-
-sim = Simulator(model, sim_cfg)
+sim, plot_cfg = load_simulator_from_project(BASE_DIR / 'project.yaml')
logs = sim.run()
-if True:
+if True and plot_cfg is not None:
plot_from_config(logs, plot_cfg)
diff --git a/examples/basics/dc_motor/gui/layout.yaml b/examples/basics/dc_motor/gui/layout.yaml
deleted file mode 100644
index d695568..0000000
--- a/examples/basics/dc_motor/gui/layout.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-version: 1
-blocks:
- ref:
- x: 0.0
- y: -80.0
- orientation: normal
- width: 120.0
- height: 60.0
- error:
- x: 195.0
- y: -71.0
- orientation: normal
- width: 120.0
- height: 60.0
- pid:
- x: 365.0
- y: -70.0
- orientation: normal
- width: 115.0
- height: 60.0
- plant:
- x: 535.0
- y: -70.0
- orientation: normal
- width: 120.0
- height: 60.0
diff --git a/examples/basics/dc_motor/gui/model.yaml b/examples/basics/dc_motor/gui/model.yaml
deleted file mode 100644
index 12fe936..0000000
--- a/examples/basics/dc_motor/gui/model.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-blocks:
-- name: ref
- category: sources
- type: step
-- name: error
- category: operators
- type: sum
-- name: pid
- category: controllers
- type: pid
-- name: plant
- category: systems
- type: linear_state_space
-connections:
-- [ref.out, error.in1]
-- [plant.y, error.in2]
-- [error.out, pid.e]
-- [pid.u, plant.u]
diff --git a/examples/basics/dc_motor/gui/parameters.yaml b/examples/basics/dc_motor/gui/parameters.yaml
deleted file mode 100644
index 29dc6c9..0000000
--- a/examples/basics/dc_motor/gui/parameters.yaml
+++ /dev/null
@@ -1,33 +0,0 @@
-simulation:
- dt: 0.01
- T: 3.0
- solver: fixed
-blocks:
- ref:
- value_before: [[0.0]]
- value_after: [[1.0]]
- start_time: 0.5
- error:
- signs: +-
- pid:
- controller: PI
- Kp: [[2.0]]
- Ki: [[1.0]]
- integration_method: euler forward
- plant:
- A: [[0.95]]
- B: [[0.5]]
- C: [[1.0]]
- x0: [[0.0]]
-logging:
-- ref.outputs.out
-- plant.outputs.y
-- pid.outputs.u
-plots:
-- title: Ref vs Output
- signals:
- - ref.outputs.out
- - plant.outputs.y
-- title: Command
- signals:
- - pid.outputs.u
diff --git a/examples/basics/dc_motor/gui/project.yaml b/examples/basics/dc_motor/gui/project.yaml
new file mode 100644
index 0000000..7e573aa
--- /dev/null
+++ b/examples/basics/dc_motor/gui/project.yaml
@@ -0,0 +1,90 @@
+schema_version: 1
+
+project:
+ name: dc_motor_gui
+ metadata:
+ created_by: pySimBlocks
+ created_at: "2026-02-18T00:00:00Z"
+
+simulation:
+ dt: 0.01
+ T: 3.0
+ solver: fixed
+ clock: internal
+ logging:
+ - ref.outputs.out
+ - plant.outputs.y
+ - pid.outputs.u
+ plots:
+ - title: Ref vs Output
+ signals: [ref.outputs.out, plant.outputs.y]
+ - title: Command
+ signals: [pid.outputs.u]
+
+diagram:
+ blocks:
+ - name: ref
+ category: sources
+ type: step
+ parameters:
+ value_before: [[0.0]]
+ value_after: [[1.0]]
+ start_time: 0.5
+ - name: error
+ category: operators
+ type: sum
+ parameters:
+ signs: +-
+ - name: pid
+ category: controllers
+ type: pid
+ parameters:
+ controller: PI
+ Kp: [[2.0]]
+ Ki: [[1.0]]
+ integration_method: euler forward
+ - name: plant
+ category: systems
+ type: linear_state_space
+ parameters:
+ A: [[0.95]]
+ B: [[0.5]]
+ C: [[1.0]]
+ x0: [[0.0]]
+ connections:
+ - name: c1
+ ports: [ref.out, error.in1]
+ - name: c2
+ ports: [plant.y, error.in2]
+ - name: c3
+ ports: [error.out, pid.e]
+ - name: c4
+ ports: [pid.u, plant.u]
+
+gui:
+ layout:
+ blocks:
+ ref:
+ x: 0.0
+ y: -80.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ error:
+ x: 195.0
+ y: -71.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ pid:
+ x: 365.0
+ y: -70.0
+ orientation: normal
+ width: 115.0
+ height: 60.0
+ plant:
+ x: 535.0
+ y: -70.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
diff --git a/examples/basics/dc_motor/gui/run.py b/examples/basics/dc_motor/gui/run.py
index a19b1f6..5d68f4f 100644
--- a/examples/basics/dc_motor/gui/run.py
+++ b/examples/basics/dc_motor/gui/run.py
@@ -1,6 +1,5 @@
from pathlib import Path
-from pySimBlocks.core import Model, Simulator
-from pySimBlocks.project.load_project_config import load_project_config
+from pySimBlocks.project import load_simulator_from_project
from pySimBlocks.project.plot_from_config import plot_from_config
try:
@@ -8,16 +7,8 @@
except Exception:
BASE_DIR = Path("")
-sim_cfg, model_cfg, plot_cfg = load_project_config(BASE_DIR / 'parameters.yaml')
-
-model = Model(
- name="model",
- model_yaml=BASE_DIR / 'model.yaml',
- model_cfg=model_cfg
-)
-
-sim = Simulator(model, sim_cfg)
+sim, plot_cfg = load_simulator_from_project(BASE_DIR / "project.yaml")
logs = sim.run()
-if True:
+if True and plot_cfg is not None:
plot_from_config(logs, plot_cfg)
diff --git a/examples/basics/external/layout.yaml b/examples/basics/external/layout.yaml
deleted file mode 100644
index e7e23cf..0000000
--- a/examples/basics/external/layout.yaml
+++ /dev/null
@@ -1,26 +0,0 @@
-version: 1
-blocks:
- ref:
- x: 17.0
- y: -157.0
- orientation: normal
- width: 120.0
- height: 60.0
- error:
- x: 196.0
- y: -149.0
- orientation: normal
- width: 120.0
- height: 60.0
- pid:
- x: 369.0
- y: -149.0
- orientation: normal
- width: 120.0
- height: 60.0
- plant:
- x: 530.0
- y: -150.0
- orientation: normal
- width: 120.0
- height: 60.0
diff --git a/examples/basics/external/model.yaml b/examples/basics/external/model.yaml
deleted file mode 100644
index 12fe936..0000000
--- a/examples/basics/external/model.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-blocks:
-- name: ref
- category: sources
- type: step
-- name: error
- category: operators
- type: sum
-- name: pid
- category: controllers
- type: pid
-- name: plant
- category: systems
- type: linear_state_space
-connections:
-- [ref.out, error.in1]
-- [plant.y, error.in2]
-- [error.out, pid.e]
-- [pid.u, plant.u]
diff --git a/examples/basics/external/parameters.yaml b/examples/basics/external/parameters.yaml
deleted file mode 100644
index 9f7ec33..0000000
--- a/examples/basics/external/parameters.yaml
+++ /dev/null
@@ -1,34 +0,0 @@
-simulation:
- dt: 0.01
- T: 3.0
- solver: fixed
-blocks:
- ref:
- value_before: [[0.0]]
- value_after: [[1.0]]
- start_time: 0.5
- error:
- signs: +-
- pid:
- controller: PI
- Kp: '#Kp'
- Ki: '#Ki'
- integration_method: euler forward
- plant:
- A: [[0.95]]
- B: [[0.5]]
- C: [[1.0]]
- x0: [[0.0]]
-logging:
-- ref.outputs.out
-- plant.outputs.y
-- pid.outputs.u
-plots:
-- title: Ref vs Output
- signals:
- - ref.outputs.out
- - plant.outputs.y
-- title: Command
- signals:
- - pid.outputs.u
-external: parameters.py
diff --git a/examples/basics/external/project.yaml b/examples/basics/external/project.yaml
new file mode 100644
index 0000000..172c655
--- /dev/null
+++ b/examples/basics/external/project.yaml
@@ -0,0 +1,91 @@
+schema_version: 1
+
+project:
+ name: external
+ metadata:
+ created_by: pySimBlocks
+ created_at: "2026-02-18T00:00:00Z"
+
+simulation:
+ dt: 0.01
+ T: 3.0
+ solver: fixed
+ clock: internal
+ external_module: parameters.py
+ logging:
+ - ref.outputs.out
+ - plant.outputs.y
+ - pid.outputs.u
+ plots:
+ - title: Ref vs Output
+ signals: [ref.outputs.out, plant.outputs.y]
+ - title: Command
+ signals: [pid.outputs.u]
+
+diagram:
+ blocks:
+ - name: ref
+ category: sources
+ type: step
+ parameters:
+ value_before: [[0.0]]
+ value_after: [[1.0]]
+ start_time: 0.5
+ - name: error
+ category: operators
+ type: sum
+ parameters:
+ signs: +-
+ - name: pid
+ category: controllers
+ type: pid
+ parameters:
+ controller: PI
+ Kp: '#Kp'
+ Ki: '#Ki'
+ integration_method: euler forward
+ - name: plant
+ category: systems
+ type: linear_state_space
+ parameters:
+ A: [[0.95]]
+ B: [[0.5]]
+ C: [[1.0]]
+ x0: [[0.0]]
+ connections:
+ - name: c1
+ ports: [ref.out, error.in1]
+ - name: c2
+ ports: [plant.y, error.in2]
+ - name: c3
+ ports: [error.out, pid.e]
+ - name: c4
+ ports: [pid.u, plant.u]
+
+gui:
+ layout:
+ blocks:
+ ref:
+ x: 17.0
+ y: -157.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ error:
+ x: 196.0
+ y: -149.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ pid:
+ x: 369.0
+ y: -149.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ plant:
+ x: 530.0
+ y: -150.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
diff --git a/examples/basics/external/run.py b/examples/basics/external/run.py
index 8fa4df8..5d68f4f 100644
--- a/examples/basics/external/run.py
+++ b/examples/basics/external/run.py
@@ -1,17 +1,14 @@
-from pySimBlocks.core import Model, Simulator
-from pySimBlocks.project.load_project_config import load_project_config
+from pathlib import Path
+from pySimBlocks.project import load_simulator_from_project
from pySimBlocks.project.plot_from_config import plot_from_config
-sim_cfg, model_cfg, plot_cfg = load_project_config("parameters.yaml")
+try:
+ BASE_DIR = Path(__file__).parent.resolve()
+except Exception:
+ BASE_DIR = Path("")
-model = Model(
- name="model",
- model_yaml="model.yaml",
- model_cfg=model_cfg
-)
-
-sim = Simulator(model, sim_cfg, verbose=True)
+sim, plot_cfg = load_simulator_from_project(BASE_DIR / "project.yaml")
logs = sim.run()
-if True:
+if True and plot_cfg is not None:
plot_from_config(logs, plot_cfg)
diff --git a/examples/basics/file_sources/layout.yaml b/examples/basics/file_sources/layout.yaml
deleted file mode 100644
index 02fed48..0000000
--- a/examples/basics/file_sources/layout.yaml
+++ /dev/null
@@ -1,20 +0,0 @@
-version: 1
-blocks:
- npz:
- x: -305.0
- y: -59.0
- orientation: normal
- width: 120.0
- height: 60.0
- npy:
- x: -300.0
- y: 15.0
- orientation: normal
- width: 120.0
- height: 60.0
- csv:
- x: -295.0
- y: 95.0
- orientation: normal
- width: 120.0
- height: 60.0
diff --git a/examples/basics/file_sources/model.yaml b/examples/basics/file_sources/model.yaml
deleted file mode 100644
index f3a1509..0000000
--- a/examples/basics/file_sources/model.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-blocks:
-- name: npz
- category: sources
- type: file_source
-- name: npy
- category: sources
- type: file_source
-- name: csv
- category: sources
- type: file_source
-connections: []
diff --git a/examples/basics/file_sources/parameters.yaml b/examples/basics/file_sources/parameters.yaml
deleted file mode 100644
index 1c1bba0..0000000
--- a/examples/basics/file_sources/parameters.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-simulation:
- dt: 0.05
- T: 10.0
- solver: fixed
-blocks:
- npz:
- file_path: data.npz
- key: y
- repeat: 'False'
- use_time: 'True'
- npy:
- file_path: data.npy
- repeat: 'True'
- csv:
- file_path: data.csv
- key: y
- repeat: 'False'
- use_time: false
-logging:
-- npz.outputs.out
-- npy.outputs.out
-- csv.outputs.out
-plots: []
diff --git a/examples/basics/file_sources/project.yaml b/examples/basics/file_sources/project.yaml
new file mode 100644
index 0000000..c3a59ee
--- /dev/null
+++ b/examples/basics/file_sources/project.yaml
@@ -0,0 +1,66 @@
+schema_version: 1
+
+project:
+ name: file_sources
+ metadata:
+ created_by: pySimBlocks
+ created_at: "2026-02-18T00:00:00Z"
+
+simulation:
+ dt: 0.05
+ T: 10.0
+ solver: fixed
+ clock: internal
+ logging:
+ - npz.outputs.out
+ - npy.outputs.out
+ - csv.outputs.out
+ plots: []
+
+diagram:
+ blocks:
+ - name: npz
+ category: sources
+ type: file_source
+ parameters:
+ file_path: data.npz
+ key: y
+ repeat: 'False'
+ use_time: 'True'
+ - name: npy
+ category: sources
+ type: file_source
+ parameters:
+ file_path: data.npy
+ repeat: 'True'
+ - name: csv
+ category: sources
+ type: file_source
+ parameters:
+ file_path: data.csv
+ key: y
+ repeat: 'False'
+ use_time: false
+ connections: []
+
+gui:
+ layout:
+ blocks:
+ npz:
+ x: -305.0
+ y: -59.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ npy:
+ x: -300.0
+ y: 15.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ csv:
+ x: -295.0
+ y: 95.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
diff --git a/examples/basics/state_feedback/gui/layout.yaml b/examples/basics/state_feedback/gui/layout.yaml
deleted file mode 100644
index 58fa544..0000000
--- a/examples/basics/state_feedback/gui/layout.yaml
+++ /dev/null
@@ -1,25 +0,0 @@
-version: 1
-blocks:
- controller:
- x: 25.0
- y: -18.0
- orientation: normal
- width: 120.0
- height: 60.0
- step:
- x: -170.0
- y: -30.0
- orientation: normal
- width: 120.0
- height: 60.0
- system:
- x: 195.0
- y: -15.0
- orientation: normal
- width: 120.0
- height: 60.0
-connections:
- system.x -> controller.x:
- ports: [system.x, controller.x]
- route: [[330.0, 5.0], [338.0, 5.0], [338.0, 70.0], [11.0, 70.0], [11.0, 22.0],
- [19.0, 22.0]]
diff --git a/examples/basics/state_feedback/gui/model.yaml b/examples/basics/state_feedback/gui/model.yaml
deleted file mode 100644
index 6ef84c4..0000000
--- a/examples/basics/state_feedback/gui/model.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-blocks:
-- name: controller
- category: controllers
- type: state_feedback
-- name: step
- category: sources
- type: step
-- name: system
- category: systems
- type: linear_state_space
-connections:
-- [controller.u, system.u]
-- [system.x, controller.x]
-- [step.out, controller.r]
diff --git a/examples/basics/state_feedback/gui/parameters.yaml b/examples/basics/state_feedback/gui/parameters.yaml
deleted file mode 100644
index 6fbf7c3..0000000
--- a/examples/basics/state_feedback/gui/parameters.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-simulation:
- dt: 0.02
- T: 2.0
- solver: fixed
-blocks:
- controller:
- K: '#K'
- G: '#G'
- step:
- value_before: [[0.0]]
- value_after: [[1.0]]
- start_time: 0.5
- system:
- A: '#A'
- B: '#B'
- C: '#C'
-logging:
-- controller.outputs.u
-- step.outputs.out
-- system.outputs.x
-- system.outputs.y
-plots:
-- title: Ref vs Output
- signals:
- - step.outputs.out
- - system.outputs.y
-- title: Command
- signals:
- - controller.outputs.u
-external: parameters.py
diff --git a/examples/basics/state_feedback/gui/project.yaml b/examples/basics/state_feedback/gui/project.yaml
new file mode 100644
index 0000000..36a9359
--- /dev/null
+++ b/examples/basics/state_feedback/gui/project.yaml
@@ -0,0 +1,80 @@
+schema_version: 1
+
+project:
+ name: state_feedback_gui
+ metadata:
+ created_by: pySimBlocks
+ created_at: "2026-02-18T00:00:00Z"
+
+simulation:
+ dt: 0.02
+ T: 2.0
+ solver: fixed
+ clock: internal
+ external_module: parameters.py
+ logging:
+ - controller.outputs.u
+ - step.outputs.out
+ - system.outputs.x
+ - system.outputs.y
+ plots:
+ - title: Ref vs Output
+ signals: [step.outputs.out, system.outputs.y]
+ - title: Command
+ signals: [controller.outputs.u]
+
+diagram:
+ blocks:
+ - name: controller
+ category: controllers
+ type: state_feedback
+ parameters:
+ K: '#K'
+ G: '#G'
+ - name: step
+ category: sources
+ type: step
+ parameters:
+ value_before: [[0.0]]
+ value_after: [[1.0]]
+ start_time: 0.5
+ - name: system
+ category: systems
+ type: linear_state_space
+ parameters:
+ A: '#A'
+ B: '#B'
+ C: '#C'
+
+ connections:
+ - name: c1
+ ports: [controller.u, system.u]
+ - name: c2
+ ports: [system.x, controller.x]
+ - name: c3
+ ports: [step.out, controller.r]
+
+gui:
+ layout:
+ blocks:
+ controller:
+ x: 25.0
+ y: -18.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ step:
+ x: -170.0
+ y: -30.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ system:
+ x: 195.0
+ y: -15.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ connections:
+ c2:
+ route: [[330.0, 5.0], [338.0, 5.0], [338.0, 70.0], [11.0, 70.0], [11.0, 22.0], [19.0, 22.0]]
diff --git a/examples/basics/state_feedback/gui/run.py b/examples/basics/state_feedback/gui/run.py
index 2c33fc1..8d74233 100644
--- a/examples/basics/state_feedback/gui/run.py
+++ b/examples/basics/state_feedback/gui/run.py
@@ -1,17 +1,14 @@
-from pySimBlocks.core import Model, Simulator
-from pySimBlocks.project.load_project_config import load_project_config
+from pathlib import Path
+from pySimBlocks.project import load_simulator_from_project
from pySimBlocks.project.plot_from_config import plot_from_config
-sim_cfg, model_cfg, plot_cfg = load_project_config("parameters.yaml")
+try:
+ BASE_DIR = Path(__file__).parent.resolve()
+except Exception:
+ BASE_DIR = Path("")
-model = Model(
- name="model",
- model_yaml="model.yaml",
- model_cfg=model_cfg
-)
-
-sim = Simulator(model, sim_cfg)
+sim, plot_cfg = load_simulator_from_project(BASE_DIR / 'project.yaml')
logs = sim.run()
-if True:
+if True and plot_cfg is not None:
plot_from_config(logs, plot_cfg)
diff --git a/examples/basics/state_space/linear/gui/layout.yaml b/examples/basics/state_space/linear/gui/layout.yaml
deleted file mode 100644
index 9db8084..0000000
--- a/examples/basics/state_space/linear/gui/layout.yaml
+++ /dev/null
@@ -1,49 +0,0 @@
-version: 1
-blocks:
- ref:
- x: 0.0
- y: -5.0
- orientation: normal
- width: 120.0
- height: 60.0
- sum:
- x: 348.0
- y: 5.0
- orientation: normal
- width: 120.0
- height: 60.0
- A:
- x: 430.0
- y: 140.0
- orientation: flipped
- width: 120.0
- height: 60.0
- B:
- x: 175.0
- y: -5.0
- orientation: normal
- width: 120.0
- height: 60.0
- delay:
- x: 545.0
- y: 15.0
- orientation: normal
- width: 120.0
- height: 60.0
- plant:
- x: 176.0
- y: -99.0
- orientation: normal
- width: 120.0
- height: 60.0
- C:
- x: 745.0
- y: 15.0
- orientation: normal
- width: 120.0
- height: 60.0
-connections:
- A.out -> sum.in2:
- ports: [A.out, sum.in2]
- route: [[415.0, 170.0], [407.0, 170.0], [407.0, 170.0], [334.0, 170.0], [334.0,
- 45.0], [342.0, 45.0]]
diff --git a/examples/basics/state_space/linear/gui/model.yaml b/examples/basics/state_space/linear/gui/model.yaml
deleted file mode 100644
index 392ea04..0000000
--- a/examples/basics/state_space/linear/gui/model.yaml
+++ /dev/null
@@ -1,30 +0,0 @@
-blocks:
-- name: ref
- category: sources
- type: step
-- name: sum
- category: operators
- type: sum
-- name: A
- category: operators
- type: gain
-- name: B
- category: operators
- type: gain
-- name: delay
- category: operators
- type: delay
-- name: plant
- category: systems
- type: linear_state_space
-- name: C
- category: operators
- type: gain
-connections:
-- [ref.out, B.in]
-- [B.out, sum.in1]
-- [A.out, sum.in2]
-- [sum.out, delay.in]
-- [delay.out, A.in]
-- [ref.out, plant.u]
-- [delay.out, C.in]
diff --git a/examples/basics/state_space/linear/gui/parameters.yaml b/examples/basics/state_space/linear/gui/parameters.yaml
deleted file mode 100644
index b38041e..0000000
--- a/examples/basics/state_space/linear/gui/parameters.yaml
+++ /dev/null
@@ -1,40 +0,0 @@
-simulation:
- dt: 0.01
- T: 5.0
- solver: fixed
-blocks:
- ref:
- value_before: [[0.0]]
- value_after: [[1.0]]
- start_time: 0.5
- sum:
- signs: ++
- A:
- gain: [[0.0, 0.25], [0.3, 0.91]]
- multiplication: Matrix (K @ u)
- B:
- gain: [[0.5], [0.3]]
- multiplication: Matrix (K @ u)
- delay:
- num_delays: 1
- initial_output: [[0.0], [0.0]]
- plant:
- A: [[0.0, 0.25], [0.3, 0.91]]
- B: [[0.5], [0.3]]
- C: [[0.0, 1.0]]
- x0: [[0.0], [0.0]]
- C:
- gain: [[0.0, 1.0]]
- multiplication: Matrix (K @ u)
-logging:
-- ref.outputs.out
-- delay.outputs.out
-- plant.outputs.x
-- plant.outputs.y
-- C.outputs.out
-plots:
-- title: System response
- signals:
- - delay.outputs.out
- - plant.outputs.x
- - ref.outputs.out
diff --git a/examples/basics/state_space/linear/gui/project.yaml b/examples/basics/state_space/linear/gui/project.yaml
new file mode 100644
index 0000000..d261425
--- /dev/null
+++ b/examples/basics/state_space/linear/gui/project.yaml
@@ -0,0 +1,133 @@
+schema_version: 1
+
+project:
+ name: linear_state_space_gui
+ metadata:
+ created_by: pySimBlocks
+ created_at: "2026-02-18T00:00:00Z"
+
+simulation:
+ dt: 0.01
+ T: 5.0
+ solver: fixed
+ clock: internal
+ logging:
+ - ref.outputs.out
+ - delay.outputs.out
+ - plant.outputs.x
+ - plant.outputs.y
+ - C.outputs.out
+ plots:
+ - title: System response
+ signals: [delay.outputs.out, plant.outputs.x, ref.outputs.out]
+
+diagram:
+ blocks:
+ - name: ref
+ category: sources
+ type: step
+ parameters:
+ value_before: [[0.0]]
+ value_after: [[1.0]]
+ start_time: 0.5
+ - name: sum
+ category: operators
+ type: sum
+ parameters:
+ signs: ++
+ - name: A
+ category: operators
+ type: gain
+ parameters:
+ gain: [[0.0, 0.25], [0.3, 0.91]]
+ multiplication: Matrix (K @ u)
+ - name: B
+ category: operators
+ type: gain
+ parameters:
+ gain: [[0.5], [0.3]]
+ multiplication: Matrix (K @ u)
+ - name: delay
+ category: operators
+ type: delay
+ parameters:
+ num_delays: 1
+ initial_output: [[0.0], [0.0]]
+ - name: plant
+ category: systems
+ type: linear_state_space
+ parameters:
+ A: [[0.0, 0.25], [0.3, 0.91]]
+ B: [[0.5], [0.3]]
+ C: [[0.0, 1.0]]
+ x0: [[0.0], [0.0]]
+ - name: C
+ category: operators
+ type: gain
+ parameters:
+ gain: [[0.0, 1.0]]
+ multiplication: Matrix (K @ u)
+ connections:
+ - name: c1
+ ports: [ref.out, B.in]
+ - name: c2
+ ports: [B.out, sum.in1]
+ - name: c3
+ ports: [A.out, sum.in2]
+ - name: c4
+ ports: [sum.out, delay.in]
+ - name: c5
+ ports: [delay.out, A.in]
+ - name: c6
+ ports: [ref.out, plant.u]
+ - name: c7
+ ports: [delay.out, C.in]
+
+gui:
+ layout:
+ blocks:
+ ref:
+ x: 0.0
+ y: -5.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ sum:
+ x: 348.0
+ y: 5.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ A:
+ x: 430.0
+ y: 140.0
+ orientation: flipped
+ width: 120.0
+ height: 60.0
+ B:
+ x: 175.0
+ y: -5.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ delay:
+ x: 545.0
+ y: 15.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ plant:
+ x: 176.0
+ y: -99.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ C:
+ x: 745.0
+ y: 15.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ connections:
+ c3:
+ route: [[415.0, 170.0], [407.0, 170.0], [407.0, 170.0], [334.0, 170.0], [334.0, 45.0], [342.0, 45.0]]
diff --git a/examples/basics/state_space/linear/gui/run.py b/examples/basics/state_space/linear/gui/run.py
index a19b1f6..5d68f4f 100644
--- a/examples/basics/state_space/linear/gui/run.py
+++ b/examples/basics/state_space/linear/gui/run.py
@@ -1,6 +1,5 @@
from pathlib import Path
-from pySimBlocks.core import Model, Simulator
-from pySimBlocks.project.load_project_config import load_project_config
+from pySimBlocks.project import load_simulator_from_project
from pySimBlocks.project.plot_from_config import plot_from_config
try:
@@ -8,16 +7,8 @@
except Exception:
BASE_DIR = Path("")
-sim_cfg, model_cfg, plot_cfg = load_project_config(BASE_DIR / 'parameters.yaml')
-
-model = Model(
- name="model",
- model_yaml=BASE_DIR / 'model.yaml',
- model_cfg=model_cfg
-)
-
-sim = Simulator(model, sim_cfg)
+sim, plot_cfg = load_simulator_from_project(BASE_DIR / "project.yaml")
logs = sim.run()
-if True:
+if True and plot_cfg is not None:
plot_from_config(logs, plot_cfg)
diff --git a/examples/basics/state_space/non_linear/layout.yaml b/examples/basics/state_space/non_linear/layout.yaml
deleted file mode 100644
index 276149e..0000000
--- a/examples/basics/state_space/non_linear/layout.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-version: 1
-blocks:
- Step:
- x: 10.0
- y: 0.0
- orientation: normal
- NLSS:
- x: 180.0
- y: 0.0
- orientation: normal
diff --git a/examples/basics/state_space/non_linear/model.yaml b/examples/basics/state_space/non_linear/model.yaml
deleted file mode 100644
index 7afee71..0000000
--- a/examples/basics/state_space/non_linear/model.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-blocks:
-- name: Step
- category: sources
- type: step
-- name: NLSS
- category: systems
- type: non_linear_state_space
-connections:
-- [Step.out, NLSS.u]
diff --git a/examples/basics/state_space/non_linear/parameters.yaml b/examples/basics/state_space/non_linear/parameters.yaml
deleted file mode 100644
index ea7ca39..0000000
--- a/examples/basics/state_space/non_linear/parameters.yaml
+++ /dev/null
@@ -1,22 +0,0 @@
-simulation:
- dt: 0.1
- T: 10.0
- solver: fixed
-blocks:
- Step:
- value_before: [[0.0]]
- value_after: [[1.0]]
- start_time: 1.0
- NLSS:
- file_path: nlss.py
- state_function_name: f
- output_function_name: g
- input_keys:
- - u
- output_keys:
- - y
- x0: [[0.0], [0.0]]
-logging:
-- Step.outputs.out
-- NLSS.outputs.y
-plots: []
diff --git a/examples/basics/state_space/non_linear/project.yaml b/examples/basics/state_space/non_linear/project.yaml
new file mode 100644
index 0000000..696cf80
--- /dev/null
+++ b/examples/basics/state_space/non_linear/project.yaml
@@ -0,0 +1,52 @@
+schema_version: 1
+
+project:
+ name: non_linear_state_space
+ metadata:
+ created_by: pySimBlocks
+ created_at: "2026-02-18T00:00:00Z"
+
+simulation:
+ dt: 0.1
+ T: 10.0
+ solver: fixed
+ clock: internal
+ logging:
+ - Step.outputs.out
+ - NLSS.outputs.y
+ plots: []
+
+diagram:
+ blocks:
+ - name: Step
+ category: sources
+ type: step
+ parameters:
+ value_before: [[0.0]]
+ value_after: [[1.0]]
+ start_time: 1.0
+ - name: NLSS
+ category: systems
+ type: non_linear_state_space
+ parameters:
+ file_path: nlss.py
+ state_function_name: f
+ output_function_name: g
+ input_keys: [u]
+ output_keys: [y]
+ x0: [[0.0], [0.0]]
+ connections:
+ - name: c1
+ ports: [Step.out, NLSS.u]
+
+gui:
+ layout:
+ blocks:
+ Step:
+ x: 10.0
+ y: 0.0
+ orientation: normal
+ NLSS:
+ x: 180.0
+ y: 0.0
+ orientation: normal
diff --git a/examples/basics/state_space/non_linear/run.py b/examples/basics/state_space/non_linear/run.py
index 339d9e1..5d68f4f 100644
--- a/examples/basics/state_space/non_linear/run.py
+++ b/examples/basics/state_space/non_linear/run.py
@@ -1,6 +1,5 @@
from pathlib import Path
-from pySimBlocks.core import Model, Simulator
-from pySimBlocks.project.load_project_config import load_project_config
+from pySimBlocks.project import load_simulator_from_project
from pySimBlocks.project.plot_from_config import plot_from_config
try:
@@ -8,16 +7,8 @@
except Exception:
BASE_DIR = Path("")
-sim_cfg, model_cfg, plot_cfg = load_project_config(BASE_DIR / "parameters.yaml")
-
-model = Model(
- name="model",
- model_yaml=BASE_DIR / "model.yaml",
- model_cfg=model_cfg
-)
-
-sim = Simulator(model, sim_cfg)
+sim, plot_cfg = load_simulator_from_project(BASE_DIR / "project.yaml")
logs = sim.run()
-if True:
+if True and plot_cfg is not None:
plot_from_config(logs, plot_cfg)
diff --git a/examples/basics/state_space/polytope/layout.yaml b/examples/basics/state_space/polytope/layout.yaml
deleted file mode 100644
index 2a4be81..0000000
--- a/examples/basics/state_space/polytope/layout.yaml
+++ /dev/null
@@ -1,18 +0,0 @@
-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/basics/state_space/polytope/model.yaml b/examples/basics/state_space/polytope/model.yaml
deleted file mode 100644
index 8763594..0000000
--- a/examples/basics/state_space/polytope/model.yaml
+++ /dev/null
@@ -1,17 +0,0 @@
-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/basics/state_space/polytope/parameters.yaml b/examples/basics/state_space/polytope/parameters.yaml
deleted file mode 100644
index d933810..0000000
--- a/examples/basics/state_space/polytope/parameters.yaml
+++ /dev/null
@@ -1,28 +0,0 @@
-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/basics/state_space/polytope/project.yaml b/examples/basics/state_space/polytope/project.yaml
new file mode 100644
index 0000000..0f3c120
--- /dev/null
+++ b/examples/basics/state_space/polytope/project.yaml
@@ -0,0 +1,76 @@
+schema_version: 1
+
+project:
+ name: polytopic_state_space
+ metadata:
+ created_by: pySimBlocks
+ created_at: "2026-02-18T00:00:00Z"
+
+simulation:
+ dt: 0.1
+ T: 10.0
+ solver: fixed
+ clock: internal
+ external_module: params.py
+ logging:
+ - Polytope.outputs.x
+ - Polytope.outputs.y
+ - weights.outputs.w
+ plots: []
+
+diagram:
+ blocks:
+ - name: Polytope
+ category: systems
+ type: polytopic_state_space
+ parameters:
+ A: '#A'
+ B: '#B'
+ C: '#C'
+ - name: weights
+ category: operators
+ type: algebraic_function
+ parameters:
+ file_path: weights.py
+ function_name: get_weights
+ input_keys: [c]
+ output_keys: [w]
+ - name: Step
+ category: sources
+ type: step
+ parameters:
+ value_before: [[0.0]]
+ value_after: [[1.0]]
+ start_time: 1.0
+ - name: Constant
+ category: sources
+ type: constant
+ parameters:
+ value: [[0.3]]
+ connections:
+ - name: c1
+ ports: [Step.out, Polytope.u]
+ - name: c2
+ ports: [weights.w, Polytope.w]
+ - name: c3
+ ports: [Constant.out, weights.c]
+
+gui:
+ layout:
+ 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/quick_start/gui/layout.yaml b/examples/quick_start/gui/layout.yaml
deleted file mode 100644
index 71fb349..0000000
--- a/examples/quick_start/gui/layout.yaml
+++ /dev/null
@@ -1,41 +0,0 @@
-version: 1
-blocks:
- v:
- x: -570.0
- y: -300.0
- orientation: normal
- width: 120.0
- height: 60.0
- x:
- x: -370.0
- y: -300.0
- orientation: normal
- width: 120.0
- height: 60.0
- Sum:
- x: -740.0
- y: -300.0
- orientation: normal
- width: 120.0
- height: 60.0
- damping:
- x: -570.0
- y: -210.0
- orientation: flipped
- width: 130.0
- height: 65.0
- stiffness:
- x: -555.0
- y: -410.0
- orientation: flipped
- width: 135.0
- height: 65.0
-connections:
- damping.out -> Sum.in2:
- ports: [damping.out, Sum.in2]
- route: [[-585.0, -177.5], [-755.0, -177.5], [-755.0, -225.0], [-755.0, -225.0],
- [-755.0, -260.0], [-746.0, -260.0]]
- stiffness.out -> Sum.in1:
- ports: [stiffness.out, Sum.in1]
- route: [[-570.0, -377.5], [-755.0, -377.5], [-755.0, -322.5], [-755.0, -322.5],
- [-755.0, -280.0], [-746.0, -280.0]]
diff --git a/examples/quick_start/gui/model.yaml b/examples/quick_start/gui/model.yaml
deleted file mode 100644
index 4675807..0000000
--- a/examples/quick_start/gui/model.yaml
+++ /dev/null
@@ -1,23 +0,0 @@
-blocks:
-- name: v
- category: operators
- type: discrete_integrator
-- name: x
- category: operators
- type: discrete_integrator
-- name: Sum
- category: operators
- type: sum
-- name: damping
- category: operators
- type: gain
-- name: stiffness
- category: operators
- type: gain
-connections:
-- [Sum.out, v.in]
-- [v.out, x.in]
-- [v.out, damping.in]
-- [x.out, stiffness.in]
-- [damping.out, Sum.in2]
-- [stiffness.out, Sum.in1]
diff --git a/examples/quick_start/gui/parameters.yaml b/examples/quick_start/gui/parameters.yaml
deleted file mode 100644
index 63b88aa..0000000
--- a/examples/quick_start/gui/parameters.yaml
+++ /dev/null
@@ -1,27 +0,0 @@
-simulation:
- dt: 0.05
- T: 30.0
- solver: fixed
-blocks:
- v:
- initial_state: 5.0
- method: euler forward
- x:
- initial_state: 2.0
- method: euler forward
- Sum:
- signs: --
- damping:
- gain: 0.5
- multiplication: Element wise (K * u)
- stiffness:
- gain: 2.0
- multiplication: Element wise (K * u)
-logging:
-- v.outputs.out
-- x.outputs.out
-plots:
-- title: Position and Velocity
- signals:
- - v.outputs.out
- - x.outputs.out
diff --git a/examples/quick_start/gui/project.yaml b/examples/quick_start/gui/project.yaml
new file mode 100644
index 0000000..9ea4d0c
--- /dev/null
+++ b/examples/quick_start/gui/project.yaml
@@ -0,0 +1,114 @@
+schema_version: 1
+project:
+ name: gui
+ metadata:
+ created_by: pySimBlocks
+ created_at: '2026-02-18T00:00:00Z'
+simulation:
+ dt: 0.05
+ T: 30.0
+ solver: fixed
+ logging:
+ - v.outputs.out
+ - x.outputs.out
+ plots:
+ - title: Position and Velocity
+ signals:
+ - v.outputs.out
+ - x.outputs.out
+diagram:
+ blocks:
+ - name: v
+ category: operators
+ type: discrete_integrator
+ parameters:
+ initial_state: 5.0
+ method: euler forward
+ - name: x
+ category: operators
+ type: discrete_integrator
+ parameters:
+ initial_state: 2.0
+ method: euler forward
+ - name: Sum
+ category: operators
+ type: sum
+ parameters:
+ signs: --
+ - name: damping
+ category: operators
+ type: gain
+ parameters:
+ gain: 0.5
+ multiplication: Element wise (K * u)
+ - name: stiffness
+ category: operators
+ type: gain
+ parameters:
+ gain: 2.0
+ multiplication: Element wise (K * u)
+ connections:
+ - name: c1
+ ports:
+ - Sum.out
+ - v.in
+ - name: c2
+ ports:
+ - v.out
+ - x.in
+ - name: c3
+ ports:
+ - v.out
+ - damping.in
+ - name: c4
+ ports:
+ - x.out
+ - stiffness.in
+ - name: c5
+ ports:
+ - damping.out
+ - Sum.in2
+ - name: c6
+ ports:
+ - stiffness.out
+ - Sum.in1
+gui:
+ layout:
+ blocks:
+ v:
+ x: -570.0
+ y: -300.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ x:
+ x: -370.0
+ y: -300.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ Sum:
+ x: -740.0
+ y: -300.0
+ orientation: normal
+ width: 120.0
+ height: 60.0
+ damping:
+ x: -570.0
+ y: -210.0
+ orientation: flipped
+ width: 130.0
+ height: 65.0
+ stiffness:
+ x: -555.0
+ y: -410.0
+ orientation: flipped
+ width: 135.0
+ height: 65.0
+ connections:
+ c4:
+ route: [[-235.0, -270.0], [-225.0, -270.0], [-225.0, -322.5], [-225.0, -322.5],
+ [-225.0, -377.5], [-414.0, -377.5]]
+ c6:
+ route: [[-570.0, -377.5], [-755.0, -377.5], [-755.0, -322.5], [-755.0, -322.5],
+ [-755.0, -280.0], [-746.0, -280.0]]
diff --git a/examples/quick_start/gui/run.py b/examples/quick_start/gui/run.py
index a19b1f6..8d74233 100644
--- a/examples/quick_start/gui/run.py
+++ b/examples/quick_start/gui/run.py
@@ -1,6 +1,5 @@
from pathlib import Path
-from pySimBlocks.core import Model, Simulator
-from pySimBlocks.project.load_project_config import load_project_config
+from pySimBlocks.project import load_simulator_from_project
from pySimBlocks.project.plot_from_config import plot_from_config
try:
@@ -8,16 +7,8 @@
except Exception:
BASE_DIR = Path("")
-sim_cfg, model_cfg, plot_cfg = load_project_config(BASE_DIR / 'parameters.yaml')
-
-model = Model(
- name="model",
- model_yaml=BASE_DIR / 'model.yaml',
- model_cfg=model_cfg
-)
-
-sim = Simulator(model, sim_cfg)
+sim, plot_cfg = load_simulator_from_project(BASE_DIR / 'project.yaml')
logs = sim.run()
-if True:
+if True and plot_cfg is not None:
plot_from_config(logs, plot_cfg)
diff --git a/pySimBlocks/__init__.py b/pySimBlocks/__init__.py
index e8f89de..9608b3e 100644
--- a/pySimBlocks/__init__.py
+++ b/pySimBlocks/__init__.py
@@ -18,11 +18,10 @@
# Authors: see Authors.txt
# ******************************************************************************
-from pySimBlocks.core import Model, Simulator, SimulationConfig, ModelConfig, PlotConfig
+from pySimBlocks.core import Model, Simulator, SimulationConfig, PlotConfig
__all__ = [
"Model",
- "ModelConfig",
"Simulator",
"SimulationConfig",
"PlotConfig"
diff --git a/pySimBlocks/blocks/operators/algebraic_function.py b/pySimBlocks/blocks/operators/algebraic_function.py
index 90cbe5f..7669dfa 100644
--- a/pySimBlocks/blocks/operators/algebraic_function.py
+++ b/pySimBlocks/blocks/operators/algebraic_function.py
@@ -107,7 +107,7 @@ def adapt_params(cls,
f"AlgebraicFunction adapter missing parameter: {e}"
)
- # --- 2. Resolve file path (RELATIVE TO parameters.yaml)
+ # --- 2. Resolve file path (relative to project.yaml directory)
path = Path(file_path)
if not path.is_absolute():
path = (params_dir / path).resolve()
diff --git a/pySimBlocks/blocks/systems/non_linear_state_space.py b/pySimBlocks/blocks/systems/non_linear_state_space.py
index 42fdcd0..f88996b 100644
--- a/pySimBlocks/blocks/systems/non_linear_state_space.py
+++ b/pySimBlocks/blocks/systems/non_linear_state_space.py
@@ -120,7 +120,7 @@ def adapt_params(cls,
f"NonLinearStateSpace adapter missing parameter: {e}"
)
- # --- 2. Resolve file path (relative to parameters.yaml)
+ # --- 2. Resolve file path (relative to project.yaml directory)
path = Path(file_path)
if not path.is_absolute():
path = (params_dir / path).resolve()
diff --git a/pySimBlocks/blocks/systems/sofa/sofa_controller.py b/pySimBlocks/blocks/systems/sofa/sofa_controller.py
index 22ec0b0..e552336 100644
--- a/pySimBlocks/blocks/systems/sofa/sofa_controller.py
+++ b/pySimBlocks/blocks/systems/sofa/sofa_controller.py
@@ -20,7 +20,6 @@
from pathlib import Path
from typing import Dict, Any, List
-import yaml
import numpy as np
import Sofa
@@ -42,7 +41,7 @@ class SofaPysimBlocksController(Sofa.Core.Controller):
Description:
- SOFA_MASTER: SOFA is the time master.
- model_yaml + parameters_yaml required
+ project_yaml required
pysimblocks time step must be a multiple of sofa time step.
At each pysimblocks step, the controller:
1) reads measurements from the scene
@@ -87,8 +86,7 @@ def __init__(self, root: Sofa.Core.Node, name: str ="SofaControllerGui"):
self.sim: Simulator | None = None
self.step_index: int = 0
- self.model_yaml: str | None = None
- self.parameters_yaml: str | None = None
+ self.project_yaml: str | None = None
# --------------------------------------------------------------------------
# Public methods
@@ -218,12 +216,14 @@ def _build_model(self):
- the exchange block self.sofa_block (SofaExchangeIO)
- self.variables_to_log
"""
- if self.parameters_yaml is not None:
- self.sim_cfg, self.model_cfg, self.plot_cfg = load_project_config(self.parameters_yaml)
- if self.model_yaml is not None:
- model_dict = self._adapt_model_for_sofa(self.model_yaml)
- self.model = Model("sofa_model")
- build_model_from_dict(self.model, model_dict, self.model_cfg)
+ project_path = self.project_yaml
+ if project_path is None:
+ raise RuntimeError("SOFA_MASTER=True requires project_yaml to be set.")
+
+ self.sim_cfg, model_dict, self.plot_cfg, _, params_dir = load_project_config(project_path)
+ model_dict = self._adapt_model_for_sofa(model_dict)
+ self.model = Model("sofa_model")
+ build_model_from_dict(self.model, model_dict, params_dir=params_dir)
# ------------------------------------------------------------------
@@ -232,8 +232,8 @@ def _prepare_pysimblocks(self):
Called once SOFA is initialized AND if SOFA is the master.
Initialize the pysimblock struture.
"""
- if self.SOFA_MASTER and (self.model_yaml is None or self.parameters_yaml is None):
- raise RuntimeError("SOFA_MASTER=True requires model_yaml and parameters_yaml.")
+ if self.SOFA_MASTER and self.project_yaml is None:
+ raise RuntimeError("SOFA_MASTER=True requires project_yaml.")
if self.dt is None:
raise ValueError("Sample time dt Must be set at initialization.")
@@ -387,40 +387,37 @@ def _update_sofa_slider(self):
setattr(block, key, np.array(new_values).reshape(shape))
# ------------------------------------------------------------------
- def _adapt_model_for_sofa(self, model_yaml: str) -> Dict[str, Any]:
+ def _adapt_model_for_sofa(self, model_data: Dict[str, Any]) -> Dict[str, Any]:
"""
- Load model.yaml and adapt it for SOFA execution.
+ Adapt model data for SOFA execution.
This replaces any SofaPlant block by a SofaExchangeIO block,
while preserving block name and connections.
Parameters
----------
- model_yaml : Path
- Path to model.yaml
+ model_data : dict
+ Model dictionary.
Returns
-------
dict
Adapted model dictionary
"""
- model_path = Path(model_yaml)
- if not model_path.exists():
- raise FileNotFoundError(f"Model YAML file not found: {model_yaml}")
-
- with model_path.open("r") as f:
- model_data = yaml.safe_load(f) or {}
-
adapted = dict(model_data)
adapted_blocks = []
for block in model_data.get("blocks", []):
- if block["type"].lower() == "sofa_plant":
- adapted_blocks.append({
- "name": block["name"],
- "category": "systems",
- "type": "sofa_exchange_i_o",
- })
+ if not isinstance(block, dict):
+ adapted_blocks.append(block)
+ continue
+
+ block_type = str(block.get("type", "")).lower()
+ if block_type == "sofa_plant":
+ patched = dict(block).copy()
+ patched["type"] = "sofa_exchange_i_o"
+ patched.pop("scene_file", None)
+ adapted_blocks.append(patched)
else:
adapted_blocks.append(block)
diff --git a/pySimBlocks/core/__init__.py b/pySimBlocks/core/__init__.py
index 61aeb33..e9335d8 100644
--- a/pySimBlocks/core/__init__.py
+++ b/pySimBlocks/core/__init__.py
@@ -19,13 +19,12 @@
# ******************************************************************************
from pySimBlocks.core.block import Block
-from pySimBlocks.core.config import ModelConfig, SimulationConfig, PlotConfig
+from pySimBlocks.core.config import SimulationConfig, PlotConfig
from pySimBlocks.core.model import Model
from pySimBlocks.core.simulator import Simulator
__all__ = [
"Block",
- "ModelConfig",
"Model",
"PlotConfig",
"SimulationConfig",
diff --git a/pySimBlocks/core/config.py b/pySimBlocks/core/config.py
index 7c1ce79..6c736e9 100644
--- a/pySimBlocks/core/config.py
+++ b/pySimBlocks/core/config.py
@@ -19,8 +19,7 @@
# ******************************************************************************
from dataclasses import dataclass, field
-from pathlib import Path
-from typing import Any, Dict, List, Optional
+from typing import Any, Dict, List
# ---------------------------------------------------------------------
@@ -65,42 +64,6 @@ def validate(self) -> None:
)
-@dataclass
-class ModelConfig:
- """
- Model numerical parameters configuration.
-
- Stores parameters for each block, indexed by block name.
- No structural information is allowed here.
- """
-
- blocks: Dict[str, Dict[str, Any]] = field(default_factory=dict)
- parameters_dir: Path | None = None
-
- def has_block(self, name: str) -> bool:
- """Check if parameters are defined for a given block name."""
- return name in self.blocks
-
- def get_block_params(self, name: str) -> Dict[str, Any]:
- """Retrieve parameters for a given block name."""
- if name not in self.blocks:
- raise KeyError(f"No parameters defined for block '{name}'")
- return self.blocks[name]
-
- def validate(self, block_names: Optional[List[str]] = None) -> None:
- """
- Optional validation against a list of model block names.
- """
- if block_names is None:
- return
-
- unknown = set(self.blocks.keys()) - set(block_names)
- if unknown:
- raise ValueError(
- f"Parameters defined for unknown blocks: {sorted(unknown)}"
- )
-
-
@dataclass
class PlotConfig:
"""
diff --git a/pySimBlocks/core/model.py b/pySimBlocks/core/model.py
index b586860..0dc80fc 100644
--- a/pySimBlocks/core/model.py
+++ b/pySimBlocks/core/model.py
@@ -20,10 +20,9 @@
from collections import deque
from pathlib import Path
-from typing import Dict, List, Tuple
+from typing import Any, Dict, List, Tuple
from pySimBlocks.core.block import Block
-from pySimBlocks.core.config import ModelConfig
# A connection is:
# ( (src_block, src_port), (dst_block, dst_port) )
@@ -50,8 +49,8 @@ class Model:
def __init__(
self,
name: str = "model",
- model_yaml: str | Path | None = None,
- model_cfg: ModelConfig | None = None,
+ model_data: Dict[str, Any] | None = None,
+ params_dir: Path | None = None,
verbose: bool = False,
):
self.name = name
@@ -66,14 +65,9 @@ def __init__(
self._downstream_map: Dict[str, List[Connection]] = {}
self._connections_dirty: bool = True
- if model_yaml is not None:
- if model_cfg is not None and not isinstance(model_cfg, ModelConfig):
- raise TypeError("model_cfg must be a ModelConfig")
-
- from pySimBlocks.project.build_model import build_model_from_yaml
- build_model_from_yaml(self, Path(model_yaml), model_cfg)
- if model_cfg is not None:
- model_cfg.validate(list(self.blocks.keys()))
+ if model_data is not None:
+ from pySimBlocks.project.build_model import build_model_from_dict
+ build_model_from_dict(self, model_data, params_dir=params_dir)
# --------------------------------------------------------------------------
# Public methods
diff --git a/pySimBlocks/gui/__init__.py b/pySimBlocks/gui/__init__.py
index e69de29..37d4fe0 100644
--- a/pySimBlocks/gui/__init__.py
+++ b/pySimBlocks/gui/__init__.py
@@ -0,0 +1,20 @@
+# ******************************************************************************
+# 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
+# ******************************************************************************
+
diff --git a/pySimBlocks/gui/addons/sofa/sofa_service.py b/pySimBlocks/gui/addons/sofa/sofa_service.py
index 5e3c087..cc12e65 100644
--- a/pySimBlocks/gui/addons/sofa/sofa_service.py
+++ b/pySimBlocks/gui/addons/sofa/sofa_service.py
@@ -26,7 +26,11 @@
from pySimBlocks.gui.models.project_state import ProjectState
from pySimBlocks.gui.project_controller import ProjectController
-from pySimBlocks.gui.services.yaml_tools import save_yaml
+from pySimBlocks.gui.services.yaml_tools import (
+ cleanup_runtime_project_yaml,
+ runtime_project_yaml_path,
+ save_yaml,
+)
from pySimBlocks.project.generate_sofa_controller import generate_sofa_controller
@@ -74,6 +78,8 @@ def can_use_sofa(self):
def export_controller(self, window, saver):
if window.confirm_discard_or_save("exporting sofa"):
saver.save(self.project_controller.project_state, self.project_controller.view.block_items)
+ if self.project_state.directory_path is None:
+ raise ValueError("Project directory is not set.\nPlease define it in settings.")
generate_sofa_controller(self.project_state.directory_path)
def run(self):
@@ -87,16 +93,14 @@ def run(self):
if not self.scene_file or not os.path.exists(self.scene_file):
return False, "scene file not found", ""
- # save yaml on temp dir
project_dir = self.project_state.directory_path
if project_dir is None:
return {}, False, "Project directory is not set.\nPlease define it in settings."
- temp_dir = project_dir / ".temp"
- if temp_dir.exists():
- shutil.rmtree(temp_dir)
- temp_dir.mkdir(parents=True)
- save_yaml(project_state=self.project_state, temp=True)
- generate_sofa_controller(temp_dir)
+
+ runtime_yaml = runtime_project_yaml_path(project_dir)
+ cleanup_runtime_project_yaml(project_dir)
+ save_yaml(project_state=self.project_state, runtime=True)
+ generate_sofa_controller(project_yaml=runtime_yaml)
# set command
plugins = "SofaPython3"
@@ -111,25 +115,28 @@ def run(self):
self.process.setArguments(args)
self.process.setProcessChannelMode(QProcess.MergedChannels)
- self.process.start()
- if not self.process.waitForStarted():
- return False, "Launch failed", "runSofa could not start"
- self.process.waitForFinished(-1)
-
try:
- generate_sofa_controller(project_dir)
- except Exception as e:
- return False, "Could not regenerate controller", "model and parameters yaml does not exists.\n" + str(e)
-
- # get output results
- output = self.process.readAllStandardOutput().data().decode()
- errors = self.process.readAllStandardError().data().decode()
- full_log = output + "\n" + errors
- exit_code = self.process.exitCode()
- if exit_code != 0:
- return False, "SOFA exited with error", f"exit code = {exit_code}\n\n{full_log}"
-
- return True, "SOFA finished", "Process terminated correctly"
+ self.process.start()
+ if not self.process.waitForStarted():
+ return False, "Launch failed", "runSofa could not start"
+ self.process.waitForFinished(-1)
+
+ try:
+ generate_sofa_controller(project_dir)
+ except Exception as e:
+ return False, "Could not regenerate controller", "project.yaml does not exist.\n" + str(e)
+
+ # get output results
+ output = self.process.readAllStandardOutput().data().decode()
+ errors = self.process.readAllStandardError().data().decode()
+ full_log = output + "\n" + errors
+ exit_code = self.process.exitCode()
+ if exit_code != 0:
+ return False, "SOFA exited with error", f"exit code = {exit_code}\n\n{full_log}"
+
+ return True, "SOFA finished", "Process terminated correctly"
+ finally:
+ cleanup_runtime_project_yaml(project_dir)
def _check_sofa_environnment(self):
sofa_root = os.environ.get("SOFA_ROOT")
diff --git a/pySimBlocks/gui/blocks/operators/algebraic_function.py b/pySimBlocks/gui/blocks/operators/algebraic_function.py
index 62faea8..9fbba46 100644
--- a/pySimBlocks/gui/blocks/operators/algebraic_function.py
+++ b/pySimBlocks/gui/blocks/operators/algebraic_function.py
@@ -52,7 +52,7 @@ def __init__(self):
name="file_path",
type="string",
required=True,
- description="Path to the Python file containing the function relative to the parameters.yaml file."
+ description="Path to the Python file containing the function relative to the project.yaml file."
),
ParameterMeta(
name="function_name",
diff --git a/pySimBlocks/gui/blocks/sources/chirp.py b/pySimBlocks/gui/blocks/sources/chirp.py
index bed8412..82f44c6 100644
--- a/pySimBlocks/gui/blocks/sources/chirp.py
+++ b/pySimBlocks/gui/blocks/sources/chirp.py
@@ -1,4 +1,7 @@
# ******************************************************************************
+# 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
diff --git a/pySimBlocks/gui/blocks/sources/function_source.py b/pySimBlocks/gui/blocks/sources/function_source.py
index 80aed1d..d5048b5 100644
--- a/pySimBlocks/gui/blocks/sources/function_source.py
+++ b/pySimBlocks/gui/blocks/sources/function_source.py
@@ -1,4 +1,7 @@
# ******************************************************************************
+# 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
@@ -47,7 +50,7 @@ def __init__(self):
required=True,
description=(
"Path to the Python file containing the function, relative to "
- "the parameters.yaml file."
+ "the project.yaml file."
),
),
ParameterMeta(
diff --git a/pySimBlocks/gui/blocks/systems/non_linear_state_space.py b/pySimBlocks/gui/blocks/systems/non_linear_state_space.py
index 316fb91..956d2ce 100644
--- a/pySimBlocks/gui/blocks/systems/non_linear_state_space.py
+++ b/pySimBlocks/gui/blocks/systems/non_linear_state_space.py
@@ -52,7 +52,7 @@ def __init__(self):
name="file_path",
type="string",
required=True,
- description="Path to the Python file containing the functions relative to the parameters.yaml file."
+ description="Path to the Python file containing the functions relative to the project.yaml file."
),
ParameterMeta(
name="state_function_name",
diff --git a/pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py b/pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py
index 7d78730..5004e41 100644
--- a/pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py
+++ b/pySimBlocks/gui/blocks/systems/sofa/sofa_exchange_i_o.py
@@ -49,7 +49,7 @@ def __init__(self):
ParameterMeta(
name="scene_file",
type="string",
- description="Path to the SOFA scene file used for automatic generation relative to parameters.yaml file."
+ description="Path to the SOFA scene file used for automatic generation relative to project.yaml file."
),
ParameterMeta(
name="input_keys",
diff --git a/pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py b/pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py
index ede6b49..b730def 100644
--- a/pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py
+++ b/pySimBlocks/gui/blocks/systems/sofa/sofa_plant.py
@@ -48,7 +48,7 @@ def __init__(self):
name="scene_file",
type="string",
required=True,
- description="Path to the SOFA scene file, relative to the parameters.yaml file."
+ description="Path to the SOFA scene file, relative to the project.yaml file."
),
ParameterMeta(
name="input_keys",
diff --git a/pySimBlocks/gui/dialogs/display_yaml_dialog.py b/pySimBlocks/gui/dialogs/display_yaml_dialog.py
index fa5d0e0..077d887 100644
--- a/pySimBlocks/gui/dialogs/display_yaml_dialog.py
+++ b/pySimBlocks/gui/dialogs/display_yaml_dialog.py
@@ -18,8 +18,6 @@
# Authors: see Authors.txt
# ******************************************************************************
-import yaml
-
from PySide6.QtWidgets import (
QDialog,
QVBoxLayout,
@@ -31,7 +29,7 @@
from PySide6.QtGui import QFont
from pySimBlocks.gui.models.project_state import ProjectState
-from pySimBlocks.gui.services.yaml_tools import dump_parameter_yaml, dump_model_yaml, dump_layout_yaml
+from pySimBlocks.gui.services.yaml_tools import dump_project_yaml
from pySimBlocks.gui.widgets.diagram_view import DiagramView
@@ -42,7 +40,7 @@ def __init__(self,
parent=None):
super().__init__(parent)
- self.setWindowTitle("Generated YAML files")
+ self.setWindowTitle("project.yaml Preview")
self.resize(900, 600)
self.project_state = project
@@ -55,29 +53,14 @@ def __init__(self,
# -------------------------------------------------
tabs = QTabWidget()
- # Parameters.yaml
- ptext = dump_parameter_yaml(self.project_state)
- tabs.addTab(
- self._make_code_view(ptext),
- "parameters.yaml"
- )
-
- # Model.yaml
- mtext = dump_model_yaml(self.project_state)
- tabs.addTab(
- self._make_code_view(mtext),
- "model.yaml"
- )
-
- # Layout.yaml
if self.view.block_items:
blocks_items = self.view.block_items
else:
blocks_items = {}
- ltext = dump_layout_yaml(blocks_items)
+ project_text = dump_project_yaml(self.project_state, blocks_items)
tabs.addTab(
- self._make_code_view(ltext),
- "layout.yaml"
+ self._make_code_view(project_text),
+ "project.yaml"
)
main_layout.addWidget(tabs)
diff --git a/pySimBlocks/gui/dialogs/settings/project.py b/pySimBlocks/gui/dialogs/settings/project.py
index 5b01010..8596809 100644
--- a/pySimBlocks/gui/dialogs/settings/project.py
+++ b/pySimBlocks/gui/dialogs/settings/project.py
@@ -55,7 +55,7 @@ def __init__(self, project_state: ProjectState, project_controller: ProjectContr
load_btn = QPushButton("Load")
load_btn.clicked.connect(self.load_project)
label = QLabel("Load Project:")
- label.setToolTip("Auto Load project from directory with parameters and model yaml.")
+ label.setToolTip("Auto load project from directory containing project.yaml.")
layout.addRow(label, load_btn)
ext = project_state.external or ""
diff --git a/pySimBlocks/gui/main_window.py b/pySimBlocks/gui/main_window.py
index 48e9c47..9078afd 100644
--- a/pySimBlocks/gui/main_window.py
+++ b/pySimBlocks/gui/main_window.py
@@ -18,8 +18,6 @@
# Authors: see Authors.txt
# ******************************************************************************
-from ast import Constant
-import shutil
from pathlib import Path
from typing import Dict, List
@@ -34,6 +32,7 @@
from pySimBlocks.gui.services.project_loader import ProjectLoaderYaml
from pySimBlocks.gui.services.project_saver import ProjectSaverYaml
from pySimBlocks.gui.services.simulation_runner import SimulationRunner
+from pySimBlocks.gui.services.yaml_tools import cleanup_runtime_project_yaml
from pySimBlocks.gui.widgets.block_list import BlockList
from pySimBlocks.gui.widgets.diagram_view import DiagramView
from pySimBlocks.gui.widgets.toolbar_view import ToolBarView
@@ -103,13 +102,8 @@ def resolve_block_meta(self, category: str, block_type: str) -> BlockMeta:
# Auto Load
# --------------------------------------------------------------------------
def auto_load_detection(self, project_path: Path) -> bool:
- param_yaml = self._auto_detect_yaml(
- project_path, ["parameters.yaml"])
- model_yaml = self._auto_detect_yaml(
- project_path, ["model.yaml"])
- if param_yaml and model_yaml:
- return True
- return False
+ project_yaml = self._auto_detect_yaml(project_path, ["project.yaml"])
+ return project_yaml is not None
# ------------------------------------------------------------------
def _auto_detect_yaml(self, project_path: Path, names: list[str]) -> str | None:
@@ -141,9 +135,7 @@ def on_project_loaded(self, project_path: Path):
# ------------------------------------------------------------------
def cleanup(self):
- temp_path = self.project_state.directory_path / ".temp"
- if temp_path.exists():
- shutil.rmtree(temp_path, ignore_errors=True)
+ cleanup_runtime_project_yaml(self.project_state.directory_path)
# ------------------------------------------------------------------
def closeEvent(self, event):
diff --git a/pySimBlocks/gui/project_controller.py b/pySimBlocks/gui/project_controller.py
index 1e13ebb..5988ebd 100644
--- a/pySimBlocks/gui/project_controller.py
+++ b/pySimBlocks/gui/project_controller.py
@@ -19,7 +19,6 @@
# ******************************************************************************
import copy
-import shutil
from pathlib import Path
from typing import TYPE_CHECKING, Any, Callable
@@ -34,6 +33,7 @@
)
from pySimBlocks.gui.widgets.diagram_view import DiagramView
from pySimBlocks.gui.blocks.block_meta import BlockMeta
+from pySimBlocks.gui.services.yaml_tools import cleanup_runtime_project_yaml
if TYPE_CHECKING:
from pySimBlocks.gui.services.project_loader import ProjectLoader
@@ -241,10 +241,7 @@ def clear(self):
# ------------------------------------------------------------------
def update_project_param(self, new_path: Path, ext: str):
- if self.project_state.directory_path:
- temp = self.project_state.directory_path / ".temp"
- if temp.exists():
- shutil.rmtree(temp, ignore_errors=True)
+ cleanup_runtime_project_yaml(self.project_state.directory_path)
if new_path != self.project_state.directory_path:
self.make_dirty()
self.project_state.directory_path = new_path
diff --git a/pySimBlocks/gui/services/__init__.py b/pySimBlocks/gui/services/__init__.py
index e69de29..37d4fe0 100644
--- a/pySimBlocks/gui/services/__init__.py
+++ b/pySimBlocks/gui/services/__init__.py
@@ -0,0 +1,20 @@
+# ******************************************************************************
+# 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
+# ******************************************************************************
+
diff --git a/pySimBlocks/gui/services/project_loader.py b/pySimBlocks/gui/services/project_loader.py
index c19e876..909b365 100644
--- a/pySimBlocks/gui/services/project_loader.py
+++ b/pySimBlocks/gui/services/project_loader.py
@@ -1,76 +1,110 @@
+# ******************************************************************************
+# 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 abc import ABC, abstractmethod
from pathlib import Path
-from pySimBlocks.gui.services.yaml_tools import load_yaml_file
-from pySimBlocks.gui.project_controller import ProjectController
from PySide6.QtCore import QPointF
+from pySimBlocks.gui.project_controller import ProjectController
+from pySimBlocks.gui.services.yaml_tools import load_yaml_file
+
+
class ProjectLoader(ABC):
-
@abstractmethod
def load(self, controller: ProjectController, directory: Path):
pass
-class ProjectLoaderYaml(ProjectLoader):
+class ProjectLoaderYaml(ProjectLoader):
def load(self, controller: ProjectController, directory: Path):
- model_yaml = directory / "model.yaml"
- params_yaml = directory / "parameters.yaml"
- layout_yaml = directory / "layout.yaml"
-
- # 1. Parse YAML
- model_data = load_yaml_file(str(model_yaml))
- params_data = load_yaml_file(str(params_yaml))
- layout_blocks, layout_conns, layout_warnings = self._load_layout_data(
- layout_yaml
- )
+ project_yaml = directory / "project.yaml"
+ project_data = load_yaml_file(str(project_yaml))
+
+ if not isinstance(project_data, dict):
+ raise ValueError("project.yaml is not a valid mapping.")
+
+ sim_data = project_data.get("simulation", {})
+ diagram_data = project_data.get("diagram", {})
+ gui_data = project_data.get("gui", {})
+
+ layout_blocks, layout_conns, layout_warnings = self._load_layout_data(gui_data)
for w in layout_warnings:
print(f"[Layout warning] {w}")
- # 2. Reset current state
controller.clear()
- # 3. build model
- self._load_simulation(controller, params_data)
- self._load_blocks(controller, model_data, params_data, layout_blocks)
- self._load_connections(controller, model_data, layout_conns)
- self._load_logging(controller, params_data)
- self._load_plots(controller, params_data)
+ self._load_simulation(controller, sim_data)
+ self._load_blocks(controller, diagram_data, layout_blocks)
+ self._load_connections(controller, diagram_data, layout_conns)
+ self._load_logging(controller, sim_data)
+ self._load_plots(controller, sim_data)
controller.clear_dirty()
-
- def _load_simulation(self, controller: ProjectController, params_data: dict):
- sim_data: dict = params_data.get("simulation", {})
- controller.project_state.load_simulation(sim_data, params_data.get("external", None))
+ def _load_simulation(self, controller: ProjectController, sim_data: dict):
+ if not isinstance(sim_data, dict):
+ sim_data = {}
+ controller.project_state.load_simulation(
+ sim_data, sim_data.get("external_module", None)
+ )
- def _load_blocks(self,
- controller: ProjectController,
- model_data: dict,
- params_data: dict,
- layout_blocks: dict = {}):
+ def _load_blocks(
+ self,
+ controller: ProjectController,
+ diagram_data: dict,
+ layout_blocks: dict | None = None,
+ ):
positions, position_warnings = self._compute_block_positions(
- model_data, layout_blocks
- )
+ diagram_data, layout_blocks
+ )
for w in position_warnings:
print(f"[Layout blocks warning] {w}")
-
- blocks = model_data.get("blocks", [])
- params_blocks = params_data.get("blocks", {})
- for block in blocks:
- name = block["name"]
- category = block["category"]
- block_type = block["type"]
-
- block_layout = self._sanitize_block_layout(layout_blocks.get(name, {}))
+ blocks = diagram_data.get("blocks", [])
+ if not isinstance(blocks, list):
+ raise ValueError("'diagram.blocks' must be a list.")
+
+ for desc in blocks:
+ if not isinstance(desc, dict):
+ print("[Block warning] Invalid block entry in diagram.blocks, ignored.")
+ continue
+
+ name = desc.get("name")
+ category = desc.get("category")
+ block_type = desc.get("type")
+ if not isinstance(name, str) or not isinstance(category, str) or not isinstance(block_type, str):
+ print("[Block warning] Block missing required fields (name/category/type), ignored.")
+ continue
+
+ block_layout = self._sanitize_block_layout((layout_blocks or {}).get(name, {}))
controller.view.drop_event_pos = positions.get(name, QPointF(0, 0))
block = controller.add_block(category, block_type, block_layout)
controller.rename_block(block, name)
- # ---- parameters ----
- raw_params = params_blocks.get(name, {})
+ raw_params = desc.get("parameters", {})
+ if not isinstance(raw_params, dict):
+ print(f"[Block warning] Invalid parameters for block '{name}', ignored.")
+ raw_params = {}
+
for pmeta in block.meta.parameters:
pname = pmeta.name
if pname in raw_params:
@@ -101,34 +135,49 @@ def _sanitize_block_layout(self, block_layout: dict | None) -> dict:
return out
- def _load_connections(self,
- controller: ProjectController,
- model_data: dict,
- layout_conns: dict | None):
- connections = model_data.get("connections", [])
-
- routes, routes_warnings = self._parse_manual_routes(
- model_data, layout_conns
- )
+ def _load_connections(
+ self,
+ controller: ProjectController,
+ diagram_data: dict,
+ layout_conns: dict | None,
+ ):
+ connections = diagram_data.get("connections", [])
+ if not isinstance(connections, list):
+ raise ValueError("'diagram.connections' must be a list.")
+
+ routes, routes_warnings = self._parse_manual_routes(diagram_data, layout_conns)
for w in routes_warnings:
print(f"[Layout connections warning] {w}")
- for src, dst in connections:
-
+ for conn in connections:
+ if not isinstance(conn, dict):
+ print("[Connection warning] Invalid connection entry, ignored.")
+ continue
+
+ conn_name = conn.get("name", None)
+ ports = conn.get("ports", None)
+ if not isinstance(ports, list) or len(ports) != 2:
+ print("[Connection warning] Connection has invalid ports, ignored.")
+ continue
+ src, dst = ports
+ if not isinstance(src, str) or not isinstance(dst, str):
+ print("[Connection warning] Connection ports must be strings, ignored.")
+ continue
+
src_block_name, src_port_name = src.split(".")
dst_block_name, dst_port_name = dst.split(".")
src_block = controller.project_state.get_block(src_block_name)
dst_block = controller.project_state.get_block(dst_block_name)
- src_port = next(
- (p for p in src_block.ports if p.name == src_port_name),
- None
- )
- dst_port = next(
- (p for p in dst_block.ports if p.name == dst_port_name),
- None
- )
+ if src_block is None or dst_block is None:
+ print(
+ f"[Connection warning] Cannot create connection {src} -> {dst}, missing block(s)."
+ )
+ continue
+
+ src_port = next((p for p in src_block.ports if p.name == src_port_name), None)
+ dst_port = next((p for p in dst_block.ports if p.name == dst_port_name), None)
if src_port is None or dst_port is None:
missing = []
@@ -136,83 +185,79 @@ def _load_connections(self,
missing.append(f"{src_block_name}.{src_port_name}")
if dst_port is None:
missing.append(f"{dst_block_name}.{dst_port_name}")
- print(f"[Connection warning] Cannot create connection {src} -> {dst}, missing port(s): {', '.join(missing)}")
+ print(
+ f"[Connection warning] Cannot create connection {src} -> {dst}, "
+ f"missing port(s): {', '.join(missing)}"
+ )
continue
- points = routes.get(f"{src} -> {dst}", None)
+ points = routes.get(conn_name, None) if isinstance(conn_name, str) else None
controller.add_connection(src_port, dst_port, points)
- def _load_logging(self,
- controller: ProjectController,
- params_data: dict):
- log_data = params_data.get("logging", {})
- controller.project_state.logging = log_data
+ def _load_logging(self, controller: ProjectController, sim_data: dict):
+ log_data = sim_data.get("logging", [])
+ controller.project_state.logging = log_data if isinstance(log_data, list) else []
- def _load_plots(self,
- controller: ProjectController,
- params_data: dict):
- plot_data = params_data.get("plots", {})
- controller.project_state.plots = plot_data
+ def _load_plots(self, controller: ProjectController, sim_data: dict):
+ plot_data = sim_data.get("plots", [])
+ controller.project_state.plots = plot_data if isinstance(plot_data, list) else []
- def _load_layout_data(self, layout_path: Path) -> tuple[dict, dict, list[str]]:
+ def _load_layout_data(self, gui_data: dict) -> tuple[dict, dict, list[str]]:
"""
- Load layout.yaml if it exists.
-
- Returns:
- layout_data: dict or None
- warnings: list of warning strings
+ Load layout data from:
+ gui.layout.blocks
+ gui.layout.connections
"""
warnings = []
- if not layout_path.exists():
+ if not isinstance(gui_data, dict):
return {}, {}, warnings
- try:
- data = load_yaml_file(str(layout_path))
- except Exception as e:
- warnings.append(f"Failed to parse layout.yaml: {e}")
+ layout = gui_data.get("layout", {})
+ if layout is None:
return {}, {}, warnings
-
- if not isinstance(data, dict):
- warnings.append("layout.yaml is not a valid mapping, ignored.")
+ if not isinstance(layout, dict):
+ warnings.append("project.yaml gui.layout is invalid, ignored.")
return {}, {}, warnings
- blocks = data.get("blocks", {})
+ blocks = layout.get("blocks", {})
if not isinstance(blocks, dict):
- warnings.append("layout.yaml.blocks is invalid, ignored.")
+ warnings.append("project.yaml gui.layout.blocks is invalid, ignored.")
return {}, {}, warnings
- conns = data.get("connections", {})
+ conns = layout.get("connections", {})
if conns is not None and not isinstance(conns, dict):
- warnings.append("layout.yaml.connections is invalid, ignored.")
+ warnings.append("project.yaml gui.layout.connections is invalid, ignored.")
conns = {}
return blocks, conns, warnings
def _compute_block_positions(
self,
- model_data: dict, layout_blocks: dict | None
+ diagram_data: dict,
+ layout_blocks: dict | None,
) -> tuple[dict[str, QPointF], list[str]]:
- """
- Decide final positions for each block in the model.
-
- Returns:
- positions: dict[name -> QPointF]
- warnings: list of warning strings
- """
warnings = []
positions = {}
- # automatic layout parameters
x, y = 0, 0
dx, dy = 180, 120
- blocks = model_data.get("blocks", [])
+ blocks = diagram_data.get("blocks", [])
+ if not isinstance(blocks, list):
+ return positions, ["'diagram.blocks' must be a list."]
- model_block_names = {b["name"] for b in blocks}
+ model_block_names = {
+ b.get("name")
+ for b in blocks
+ if isinstance(b, dict) and isinstance(b.get("name"), str)
+ }
layout_block_names = set(layout_blocks.keys()) if layout_blocks else set()
for block in blocks:
+ if not isinstance(block, dict) or not isinstance(block.get("name"), str):
+ continue
+
name = block["name"]
if layout_blocks and name in layout_blocks:
@@ -223,82 +268,93 @@ def _compute_block_positions(
if isinstance(x_val, (int, float)) and isinstance(y_val, (int, float)):
positions[name] = QPointF(float(x_val), float(y_val))
continue
- else:
- warnings.append(
- f"Invalid position for block '{name}' in layout.yaml, auto-placed."
- )
+ warnings.append(
+ f"Invalid position for block '{name}' in project.yaml gui.layout.blocks, auto-placed."
+ )
else:
if layout_blocks is not None:
warnings.append(
- f"Block '{name}' not found in layout.yaml, auto-placed."
+ f"Block '{name}' not found in project.yaml gui.layout.blocks, auto-placed."
)
- # fallback automatic placement
positions[name] = QPointF(x, y)
x += dx
if x > 800:
x = 0
y += dy
- # layout blocks not in model
for name in layout_block_names - model_block_names:
warnings.append(
- f"layout.yaml contains block '{name}' not present in model.yaml."
+ f"project.yaml gui.layout.blocks contains '{name}' not present in diagram.blocks."
)
return positions, warnings
-
def _parse_manual_routes(
self,
- model_data: dict,
- layout_connections: dict | None
+ diagram_data: dict,
+ layout_connections: dict | None,
) -> tuple[dict[str, list[QPointF]], list[str]]:
- """
- Returns:
- routes: dict[key -> list[QPointF]] for VALID routes only
- warnings: list[str]
- """
warnings = []
routes: dict[str, list[QPointF]] = {}
if not layout_connections:
return routes, warnings
- blocks = model_data.get("blocks", [])
- model_block_names = {b["name"] for b in blocks}
- model_conn_keys = model_data.get("connections", [])
+ blocks = diagram_data.get("blocks", [])
+ connections = diagram_data.get("connections", [])
+ if not isinstance(blocks, list) or not isinstance(connections, list):
+ return routes, warnings
- for key, payload in layout_connections.items():
+ model_block_names = {
+ b.get("name")
+ for b in blocks
+ if isinstance(b, dict) and isinstance(b.get("name"), str)
+ }
+ model_connections_by_name = {}
+ for conn in connections:
+ if not isinstance(conn, dict):
+ continue
+ name = conn.get("name")
+ ports = conn.get("ports")
+ if isinstance(name, str) and isinstance(ports, list) and len(ports) == 2:
+ model_connections_by_name[name] = ports
- try:
- ports = payload["ports"]
- src_block, src_port = [s.strip() for s in ports[0].split(".", 1)]
- dst_block, dst_port = [s.strip() for s in ports[1].split(".", 1)]
- except Exception:
- warnings.append(f"Invalid connection key '{key}' in layout.yaml, ignored.")
+ for conn_name, payload in layout_connections.items():
+ if conn_name not in model_connections_by_name:
+ warnings.append(
+ f"project.yaml gui.layout.connections contains unknown connection '{conn_name}', ignored."
+ )
continue
- if src_block not in model_block_names or dst_block not in model_block_names:
+ ports = model_connections_by_name[conn_name]
+ try:
+ src_block, _src_port = [s.strip() for s in ports[0].split(".", 1)]
+ dst_block, _dst_port = [s.strip() for s in ports[1].split(".", 1)]
+ except Exception:
warnings.append(
- f"layout.yaml contains connection '{key}' but a block is missing in model.yaml, ignored."
+ f"Invalid ports for connection '{conn_name}' in diagram.connections, ignored."
)
continue
- if ports not in model_conn_keys:
+ if src_block not in model_block_names or dst_block not in model_block_names:
warnings.append(
- f"layout.yaml contains connection '{ports}' not present in model.yaml, ignored."
+ f"project.yaml layout connection '{conn_name}' references missing block(s), ignored."
)
continue
if not isinstance(payload, dict) or "route" not in payload:
- warnings.append(f"layout.yaml connection '{key}' has no valid 'route', ignored.")
+ warnings.append(
+ f"project.yaml layout connection '{conn_name}' has no valid 'route', ignored."
+ )
continue
raw_route = payload["route"]
if not isinstance(raw_route, list) or len(raw_route) < 2:
- warnings.append(f"layout.yaml connection '{key}' route is invalid/too short, ignored.")
+ warnings.append(
+ f"project.yaml layout connection '{conn_name}' route is invalid/too short, ignored."
+ )
continue
pts: list[QPointF] = []
@@ -315,9 +371,11 @@ def _parse_manual_routes(
pts.append(QPointF(float(pt[0]), float(pt[1])))
if not ok:
- warnings.append(f"layout.yaml connection '{key}' route has invalid points, ignored.")
+ warnings.append(
+ f"project.yaml layout connection '{conn_name}' route has invalid points, ignored."
+ )
continue
- routes[key] = pts
+ routes[conn_name] = pts
return routes, warnings
diff --git a/pySimBlocks/gui/services/project_saver.py b/pySimBlocks/gui/services/project_saver.py
index e2ec36f..8b3655c 100644
--- a/pySimBlocks/gui/services/project_saver.py
+++ b/pySimBlocks/gui/services/project_saver.py
@@ -1,3 +1,23 @@
+# ******************************************************************************
+# 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 abc import ABC, abstractmethod
from pySimBlocks.gui.models import ProjectState
@@ -23,18 +43,26 @@ class ProjectSaverYaml(ProjectSaver):
def save(self,
project_state: ProjectState,
block_items: dict[str, BlockItem] | None = None
- ):
- save_yaml(project_state, block_items if block_items is not None else {})
+ ):
+ save_yaml(
+ project_state,
+ block_items if block_items is not None else {},
+ )
def export(self,
project_state: ProjectState,
block_items: dict[str, BlockItem] | None = None
):
- save_yaml(project_state, block_items if block_items is not None else {})
+ if project_state.directory_path is None:
+ raise ValueError("Project directory is not set.")
+
+ save_yaml(
+ project_state,
+ block_items if block_items is not None else {},
+ )
run_py = project_state.directory_path / "run.py"
run_py.write_text(
- generate_python_content(
- model_yaml_path="model.yaml", parameters_yaml_path="parameters.yaml"
- )
+ generate_python_content(project_yaml_path="project.yaml")
)
+
diff --git a/pySimBlocks/gui/services/simulation_runner.py b/pySimBlocks/gui/services/simulation_runner.py
index bae73b6..b2e552f 100644
--- a/pySimBlocks/gui/services/simulation_runner.py
+++ b/pySimBlocks/gui/services/simulation_runner.py
@@ -1,8 +1,31 @@
+# ******************************************************************************
+# 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 os
-import shutil
import sys
from pySimBlocks.gui.models import ProjectState
-from pySimBlocks.gui.services.yaml_tools import save_yaml
+from pySimBlocks.gui.services.yaml_tools import (
+ cleanup_runtime_project_yaml,
+ runtime_project_yaml_path,
+ save_yaml,
+)
from pySimBlocks.project.generate_run_script import generate_python_content
@@ -15,21 +38,13 @@ def run(self, project_state: ProjectState):
False,
"Project directory is not set.\nPlease define it in settings.",
)
-
- temp_dir = project_dir / ".temp"
-
- if temp_dir.exists():
- shutil.rmtree(temp_dir)
- temp_dir.mkdir(parents=True)
- save_yaml(project_state, temp=True)
- model_path = temp_dir / "model.yaml"
- param_path = temp_dir / "parameters.yaml"
+ project_path = runtime_project_yaml_path(project_dir)
+ cleanup_runtime_project_yaml(project_dir)
+ save_yaml(project_state, runtime=True)
code = generate_python_content(
- model_yaml_path=str(model_path),
- parameters_yaml_path=str(param_path),
- parameters_dir=str(project_dir),
+ project_yaml_path=str(project_path),
enable_plots=False,
)
@@ -37,7 +52,7 @@ def run(self, project_state: ProjectState):
old_sys_path = list(sys.path)
env = {}
try:
- os.chdir(temp_dir)
+ os.chdir(project_dir)
sys.path.insert(0, str(project_dir))
exec(code, env, env)
logs = env.get("logs")
@@ -47,4 +62,5 @@ def run(self, project_state: ProjectState):
return logs, False, f"Error: {e}"
finally:
os.chdir(old_cwd)
- sys.path[:] = old_sys_path
\ No newline at end of file
+ sys.path[:] = old_sys_path
+ cleanup_runtime_project_yaml(project_dir)
diff --git a/pySimBlocks/gui/services/yaml_tools.py b/pySimBlocks/gui/services/yaml_tools.py
index 3f5e6b7..ebb9d37 100644
--- a/pySimBlocks/gui/services/yaml_tools.py
+++ b/pySimBlocks/gui/services/yaml_tools.py
@@ -18,7 +18,7 @@
# Authors: see Authors.txt
# ******************************************************************************
-import os
+from pathlib import Path
import yaml
@@ -30,15 +30,13 @@ def load_yaml_file(path: str) -> dict:
with open(path, "r") as f:
return yaml.safe_load(f) or {}
-# ===============================================================
-# Custom list type for flow-style sequences
-# ===============================================================
+
class FlowStyleList(list):
"""Marker class for YAML flow-style lists."""
pass
-class ModelYamlDumper(yaml.SafeDumper):
+class ProjectYamlDumper(yaml.SafeDumper):
pass
@@ -50,11 +48,8 @@ def _repr_flow_list(dumper, data):
)
-
class FlowMatrix(list):
- """
- Marker type for matrices that must be dumped in YAML flow-style.
- """
+ """Marker type for matrices that must be dumped in YAML flow-style."""
pass
@@ -70,9 +65,6 @@ def _is_matrix(obj):
def _wrap_flow_matrices(obj):
- """
- Recursively wrap matrices into FlowMatrix for YAML dumping.
- """
if _is_matrix(obj):
return FlowMatrix([_wrap_flow_matrices(row) for row in obj])
@@ -84,161 +76,116 @@ def _wrap_flow_matrices(obj):
return obj
-ModelYamlDumper.add_representer(FlowMatrix, _repr_flow_list)
-ModelYamlDumper.add_representer(FlowStyleList, _repr_flow_list)
-
-# ===============================================================
-# Dump helpers
-# ===============================================================
-def dump_parameter_yaml(
- project_state: ProjectState | None = None,
- raw: dict | None = None
- ) -> str:
- if project_state:
- data = build_parameters_yaml(project_state)
- elif raw:
- data = raw
- else:
- raise ValueError("project or raw must be set")
-
- data = _wrap_flow_matrices(data)
- return yaml.dump(
- data,
- Dumper=ModelYamlDumper,
- sort_keys=False
- )
+ProjectYamlDumper.add_representer(FlowMatrix, _repr_flow_list)
+ProjectYamlDumper.add_representer(FlowStyleList, _repr_flow_list)
-def dump_model_yaml(
- project_state: ProjectState | None = None,
- raw: dict | None = None
- ) -> str:
- if project_state:
- data = build_model_yaml(project_state)
- elif raw:
- data = raw
- else:
- raise ValueError("project or raw must be set")
-
- if "connections" in data:
- data["connections"] = [
- FlowStyleList(conn) for conn in data["connections"]
- ]
- return yaml.dump(
- data,
- Dumper=ModelYamlDumper,
- sort_keys=False,
- )
-
-def dump_layout_yaml(
- block_items: dict[str, BlockItem] | None = None,
- raw: dict | None = None
- ) -> str:
- if block_items is not None:
- data = build_layout_yaml(block_items)
- elif raw:
- data = raw
- else:
- raise ValueError("block_items or raw must be set")
+def dump_project_yaml(
+ project_state: ProjectState | None = None,
+ block_items: dict[str, BlockItem] | None = None,
+ raw: dict | None = None,
+) -> str:
+ if raw is None:
+ if project_state is None:
+ raise ValueError("project_state or raw must be set")
+ raw = build_project_yaml(project_state, block_items if block_items is not None else {})
+ data = _wrap_flow_matrices(raw)
return yaml.dump(
data,
- Dumper=ModelYamlDumper,
+ Dumper=ProjectYamlDumper,
sort_keys=False,
)
-# ===============================================================
-# Save functions
-# ===============================================================
-def save_yaml(
- project_state: ProjectState,
- block_items: dict[str, BlockItem] | None = None,
- temp: bool = False) -> None:
+def save_yaml(
+ project_state: ProjectState,
+ block_items: dict[str, BlockItem] | None = None,
+ runtime: bool = False,
+) -> None:
directory = project_state.directory_path
- params_yaml = build_parameters_yaml(project_state)
- model_yaml = build_model_yaml(project_state)
-
- if temp:
- temp_dir = directory / ".temp"
- if "external" in params_yaml:
- external_abs = directory / params_yaml["external"]
- external_temp = os.path.relpath(external_abs, temp_dir)
- params_yaml["external"] = external_temp
- directory = temp_dir
+ if directory is None:
+ raise ValueError("project_state.directory_path must be set")
+ project_raw = build_project_yaml(project_state, block_items if block_items is not None else {})
directory.mkdir(parents=True, exist_ok=True)
+ target = ".project.runtime.yaml" if runtime else "project.yaml"
+ (directory / target).write_text(dump_project_yaml(raw=project_raw))
- (directory / "parameters.yaml").write_text(dump_parameter_yaml(raw=params_yaml))
- (directory / "model.yaml").write_text(dump_model_yaml(raw=model_yaml))
- if not temp and block_items is not None:
- layout_yaml = build_layout_yaml(block_items)
- (directory / "layout.yaml").write_text(dump_layout_yaml(raw=layout_yaml))
-
-# ===============================================================
-# Build function
-# ===============================================================
-def build_parameters_yaml(project_state: ProjectState) -> dict:
- data = {
- "simulation": project_state.simulation.__dict__.copy(),
- "blocks": {},
- "logging": project_state.logging,
- "plots": project_state.plots,
- }
- if data["simulation"]["clock"] == "internal":
- data["simulation"].pop("clock")
+def runtime_project_yaml_path(project_dir: Path) -> Path:
+ return project_dir / ".project.runtime.yaml"
- for b in project_state.blocks:
- params = {
- k: v for k, v in b.parameters.items()
- if v is not None and b.meta.is_parameter_active(k, b.parameters)
- }
- data["blocks"][b.name] = params
+
+def cleanup_runtime_project_yaml(project_dir: Path | None) -> None:
+ if project_dir is None:
+ return
+
+ runtime_yaml = runtime_project_yaml_path(project_dir)
+ if runtime_yaml.exists():
+ runtime_yaml.unlink(missing_ok=True)
+
+
+def _build_simulation_section(project_state: ProjectState) -> dict:
+ simulation = project_state.simulation.__dict__.copy()
+ if simulation.get("clock") == "internal":
+ simulation.pop("clock", None)
if project_state.external is not None:
- data["external"] = project_state.external
+ simulation["external_module"] = project_state.external
- return data
+ simulation["logging"] = list(project_state.logging)
+ simulation["plots"] = list(project_state.plots)
+ return simulation
-def build_model_yaml(project_state: ProjectState) -> dict:
- return {
- "blocks": [
+def _build_blocks_section(project_state: ProjectState) -> list[dict]:
+ blocks = []
+ for b in project_state.blocks:
+ params = {
+ k: v for k, v in b.parameters.items()
+ if v is not None and b.meta.is_parameter_active(k, b.parameters)
+ }
+ blocks.append(
{
"name": b.name,
"category": b.meta.category,
"type": b.meta.type,
+ "parameters": params,
}
- for b in project_state.blocks
- ],
- "connections": [
- [f"{c.src_block().name}.{c.src_port.name}",
- f"{c.dst_block().name}.{c.dst_port.name}",
- ]
- for c in project_state.connections
- ],
- }
+ )
+ return blocks
-def build_layout_yaml(block_items: dict[str, BlockItem]) -> dict:
- """
- Build layout.yaml content from the current diagram view.
- This function extracts ONLY visual information.
- """
+def _build_connections_section(project_state: ProjectState) -> tuple[list[dict], dict[tuple[str, str], str]]:
+ connections = []
+ conn_name_map: dict[tuple[str, str], str] = {}
- data = {
- "version": 1,
- "blocks": {}
- }
+ for i, c in enumerate(project_state.connections, start=1):
+ src = f"{c.src_block().name}.{c.src_port.name}"
+ dst = f"{c.dst_block().name}.{c.dst_port.name}"
+ conn_name = f"c{i}"
+ conn_name_map[(src, dst)] = conn_name
+ connections.append(
+ {
+ "name": conn_name,
+ "ports": FlowStyleList([src, dst]),
+ }
+ )
+ return connections, conn_name_map
+
+
+def _build_layout_section(
+ block_items: dict[str, BlockItem],
+ conn_name_map: dict[tuple[str, str], str],
+) -> dict:
+ data: dict = {"blocks": {}}
manual_connections = {}
seen = set()
for block in block_items.values():
-
- # block logic
name = block.instance.name
pos = block.pos()
data["blocks"][name] = {
@@ -249,41 +196,66 @@ def build_layout_yaml(block_items: dict[str, BlockItem]) -> dict:
"height": float(block.rect().height()),
}
- if len(data["blocks"]) == 0:
+ if not block_items:
return data
- view = block.view
-
+ view = next(iter(block_items.values())).view
for conn in view.connections.values():
if conn in seen:
continue
seen.add(conn)
-
if not conn.is_manual:
continue
- src_port = conn.src_port
- src_bname = src_port.parent_block.instance.name
- src_pname = src_port.instance.name
- dst_port = conn.dst_port
- dst_bname = dst_port.parent_block.instance.name
- dst_pname = dst_port.instance.name
-
- key = f"{src_bname}.{src_pname} -> {dst_bname}.{dst_pname}"
- value = {
- "ports": FlowStyleList( [
- f"{src_bname}.{src_pname}", f"{dst_bname}.{dst_pname}"
- ]),
+ src = f"{conn.src_port.parent_block.instance.name}.{conn.src_port.instance.name}"
+ dst = f"{conn.dst_port.parent_block.instance.name}.{conn.dst_port.instance.name}"
+ conn_name = conn_name_map.get((src, dst), None)
+ if conn_name is None:
+ continue
+
+ manual_connections[conn_name] = {
"route": FlowStyleList([
FlowStyleList([float(p.x()), float(p.y())])
for p in conn.route.points
- ])
- }
-
- manual_connections[key] = value
+ ])
+ }
if manual_connections:
data["connections"] = manual_connections
-
return data
+
+
+def build_project_yaml(
+ project_state: ProjectState,
+ block_items: dict[str, BlockItem] | None = None,
+) -> dict:
+ block_items = block_items if block_items is not None else {}
+ project_name = (
+ project_state.directory_path.name
+ if project_state.directory_path is not None
+ else "project"
+ )
+
+ blocks = _build_blocks_section(project_state)
+ connections, conn_name_map = _build_connections_section(project_state)
+ layout = _build_layout_section(block_items, conn_name_map)
+
+ return {
+ "schema_version": 1,
+ "project": {
+ "name": project_name,
+ "metadata": {
+ "created_by": "pySimBlocks",
+ "created_at": "2026-02-18T00:00:00Z",
+ },
+ },
+ "simulation": _build_simulation_section(project_state),
+ "diagram": {
+ "blocks": blocks,
+ "connections": connections,
+ },
+ "gui": {
+ "layout": layout,
+ },
+ }
diff --git a/pySimBlocks/gui/widgets/toolbar_view.py b/pySimBlocks/gui/widgets/toolbar_view.py
index 069c3e9..af6e683 100644
--- a/pySimBlocks/gui/widgets/toolbar_view.py
+++ b/pySimBlocks/gui/widgets/toolbar_view.py
@@ -53,7 +53,7 @@ def __init__(self,
export_action.triggered.connect(self.on_export_project)
self.addAction(export_action)
- display_action = QAction("Display files", self)
+ display_action = QAction("Project YAML", self)
display_action.triggered.connect(self.on_open_display_yaml)
self.addAction(display_action)
diff --git a/pySimBlocks/project/__init__.py b/pySimBlocks/project/__init__.py
index c5fb391..60bbb67 100644
--- a/pySimBlocks/project/__init__.py
+++ b/pySimBlocks/project/__init__.py
@@ -18,18 +18,18 @@
# Authors: see Authors.txt
# ******************************************************************************
-from pySimBlocks.project.build_model import build_model_from_yaml
from pySimBlocks.project.generate_run_script import generate_run_script, generate_python_content
from pySimBlocks.project.load_project_config import load_project_config
+from pySimBlocks.project.load_simulator import load_simulator_from_project
from pySimBlocks.project.load_simulation_config import load_simulation_config
from pySimBlocks.project.plot_from_config import plot_from_config
__all__ = [
- "build_model_from_yaml",
"generate_run_script",
"generate_python_content",
"load_project_config",
+ "load_simulator_from_project",
"load_simulation_config",
"plot_from_config"
]
diff --git a/pySimBlocks/project/build_model.py b/pySimBlocks/project/build_model.py
index 07e39ca..b2b8e1f 100644
--- a/pySimBlocks/project/build_model.py
+++ b/pySimBlocks/project/build_model.py
@@ -20,34 +20,20 @@
import importlib
from pathlib import Path
-import yaml
from typing import Dict, Any
+import yaml
from pySimBlocks.core.model import Model
-from pySimBlocks.core.config import ModelConfig
# ============================================================
# Public API
# ============================================================
-def build_model_from_yaml(
- model: Model,
- model_yaml: Path,
- model_cfg: ModelConfig | None,
-) -> None:
- """
- Build a Model instance from a model.yaml file.
- """
- with model_yaml.open("r") as f:
- model_data = yaml.safe_load(f) or {}
-
- build_model_from_dict(model, model_data, model_cfg)
-
def build_model_from_dict(
model: Model,
model_data: Dict[str, Any],
- model_cfg: ModelConfig | None,
+ params_dir: Path | None = None,
) -> None:
"""
Build a Model instance from an already loaded model dictionary.
@@ -71,6 +57,10 @@ def build_model_from_dict(
try:
block_info = blocks_index[category][block_type]
except KeyError:
+ print(f"Available blocks in category '{category}':")
+ for bt in blocks_index.get(category, {}):
+ print(f" - {bt}")
+ print(desc)
raise ValueError(
f"Unknown block '{block_type}' in category '{category}'."
)
@@ -84,26 +74,11 @@ def build_model_from_dict(
# --------------------------------------------------------
# Load parameters
# --------------------------------------------------------
- has_inline_params = "parameters" in desc
-
- if model_cfg is not None:
- if has_inline_params:
- raise ValueError(
- f"Block '{name}' defines inline parameters but a ModelConfig "
- f"is also provided. Choose exactly one source of parameters."
- )
- params = (
- model_cfg.get_block_params(name)
- if model_cfg.has_block(name)
- else {}
- )
- else:
- params = desc.get("parameters", {})
+ params = desc.get("parameters", {})
# --------------------------------------------------------
# Instantiate block
# --------------------------------------------------------
- params_dir = model_cfg.parameters_dir if model_cfg else None
params = BlockClass.adapt_params(params, params_dir=params_dir)
block = BlockClass(name=name, **params)
model.add_block(block)
diff --git a/pySimBlocks/project/generate.py b/pySimBlocks/project/generate.py
index a519eb1..195ee18 100644
--- a/pySimBlocks/project/generate.py
+++ b/pySimBlocks/project/generate.py
@@ -29,33 +29,55 @@ def main():
)
parser.add_argument(
- "folder",
- nargs="?",
- help="Project folder containing model.yaml and parameters.yaml",
+ "-f",
+ "--file",
+ "--project",
+ dest="project_file",
+ help="Path to project.yaml",
)
-
- parser.add_argument("--model", help="Path to model.yaml")
- parser.add_argument("--param", help="Path to parameters.yaml")
- parser.add_argument("--out", help="Output run.py path")
parser.add_argument(
- "--sofa-controller",
- action="store_true",
- help="Path to sofa controller to update with pysimblocks. Will be overwritten if nothing specify."
- )
+ "-d",
+ "--directory",
+ dest="project_dir",
+ help="Project directory containing project.yaml",
+ )
+ parser.add_argument(
+ "-o",
+ "--out",
+ help="Output run.py path",
+ )
+ parser.add_argument(
+ "-s",
+ "--sofa-controller",
+ action="store_true",
+ help="Update SOFA controller from project.yaml instead of generating run.py.",
+ )
args = parser.parse_args()
+ if args.project_file and args.project_dir:
+ parser.error("Use either --file/-f or --directory/-d, not both.")
+
+ project_yaml = None
+ project_dir = None
+
+ if args.project_file:
+ project_yaml = Path(args.project_file)
+ else:
+ if args.project_dir:
+ project_dir = Path(args.project_dir)
+ else:
+ project_dir = Path(".")
+
if args.sofa_controller:
from pySimBlocks.project.generate_sofa_controller import generate_sofa_controller
generate_sofa_controller(
- project_dir=Path(args.folder) if args.folder else None,
- model_yaml=Path(args.model) if args.model else None,
- parameters_yaml=Path(args.param) if args.param else None,
+ project_dir=project_dir,
+ project_yaml=project_yaml,
)
else:
generate_run_script(
- project_dir=Path(args.folder) if args.folder else None,
- model_yaml=Path(args.model) if args.model else None,
- parameters_yaml=Path(args.param) if args.param else None,
+ project_dir=project_dir,
+ project_yaml=project_yaml,
output=Path(args.out) if args.out else None,
)
diff --git a/pySimBlocks/project/generate_run_script.py b/pySimBlocks/project/generate_run_script.py
index d12e42a..73430df 100644
--- a/pySimBlocks/project/generate_run_script.py
+++ b/pySimBlocks/project/generate_run_script.py
@@ -23,8 +23,7 @@
RUN_TEMPLATE = """\
from pathlib import Path
-from pySimBlocks.core import Model, Simulator
-from pySimBlocks.project.load_project_config import load_project_config
+from pySimBlocks.project import load_simulator_from_project
from pySimBlocks.project.plot_from_config import plot_from_config
try:
@@ -32,38 +31,19 @@
except Exception:
BASE_DIR = Path("")
-sim_cfg, model_cfg, plot_cfg = load_project_config(BASE_DIR / {parameters_path!r}{parameters_dir_line})
-
-model = Model(
- name="model",
- model_yaml=BASE_DIR / {model_path!r},
- model_cfg=model_cfg
-)
-
-sim = Simulator(model, sim_cfg)
+sim, plot_cfg = load_simulator_from_project(BASE_DIR / {project_path!r})
logs = sim.run()
-if {enable_plots}:
+if {enable_plots} and plot_cfg is not None:
plot_from_config(logs, plot_cfg)
"""
-
def generate_python_content(
- model_yaml_path: str,
- parameters_yaml_path: str,
- parameters_dir: str | None = None,
+ project_yaml_path: str,
enable_plots: bool = True,
) -> str:
-
- if parameters_dir is not None:
- parameters_dir_line = f", parameters_dir=Path({parameters_dir!r})"
- else:
- parameters_dir_line = ""
-
return RUN_TEMPLATE.format(
- model_path=model_yaml_path,
- parameters_path=parameters_yaml_path,
- parameters_dir_line=parameters_dir_line,
+ project_path=project_yaml_path,
enable_plots=enable_plots,
)
@@ -72,70 +52,58 @@ def generate_python_content(
def generate_run_script(
*,
project_dir: Path | None = None,
- model_yaml: Path | None = None,
- parameters_yaml: Path | None = None,
+ project_yaml: Path | None = None,
output: Path | None = None,
) -> None:
"""
Generate a canonical run.py script for a pySimBlocks project.
Exactly one of the following modes must be used:
- - project_dir
- - model_yaml + parameters_yaml
+ - project_dir (project.yaml expected)
+ - project_yaml
Parameters
----------
project_dir : Path, optional
- Path to a project folder containing model.yaml and parameters.yaml.
-
- model_yaml : Path, optional
- Path to model.yaml (explicit mode).
+ Path to a project folder containing project.yaml.
- parameters_yaml : Path, optional
- Path to parameters.yaml (explicit mode).
+ project_yaml : Path, optional
+ Path to project.yaml (explicit unified mode).
output : Path, optional
Output run.py path (default: /run.py).
"""
- # ------------------------------------------------------------
- # Mode validation
- # ------------------------------------------------------------
- explicit_mode = model_yaml is not None or parameters_yaml is not None
+ has_project_yaml_mode = project_yaml is not None
- if project_dir and explicit_mode:
+ if project_dir and has_project_yaml_mode:
raise ValueError(
- "Cannot use project_dir together with --model/--param."
+ "Cannot use project_dir with project_yaml."
)
- if not project_dir and not explicit_mode:
+ if not project_dir and not has_project_yaml_mode:
raise ValueError(
- "You must specify either a project directory or --model and --param."
+ "You must specify one mode: project_dir or project_yaml."
)
- # ------------------------------------------------------------
- # Resolve paths
- # ------------------------------------------------------------
+ # Unified project_dir mode
if project_dir:
project_dir = Path(project_dir)
- model_yaml = project_dir / "model.yaml"
- parameters_yaml = project_dir / "parameters.yaml"
+ project_yaml = project_dir / "project.yaml"
output = output or (project_dir / "run.py")
- else:
- model_yaml = Path(model_yaml)
- parameters_yaml = Path(parameters_yaml)
- output = Path(output or "run.py")
+ if not project_yaml.exists():
+ raise FileNotFoundError(f"project.yaml not found: {project_yaml}")
- if not model_yaml.exists():
- raise FileNotFoundError(f"model.yaml not found: {model_yaml}")
+ content = generate_python_content(project_yaml_path=project_yaml.name)
- if not parameters_yaml.exists():
- raise FileNotFoundError(f"parameters.yaml not found: {parameters_yaml}")
+ # Unified explicit project_yaml mode
+ elif has_project_yaml_mode:
+ project_yaml = Path(project_yaml)
+ output = Path(output or "run.py")
+ if not project_yaml.exists():
+ raise FileNotFoundError(f"project.yaml not found: {project_yaml}")
- # ------------------------------------------------------------
- # Write run.py
- # ------------------------------------------------------------
- content = generate_python_content(model_yaml.name, parameters_yaml.name)
+ content = generate_python_content(project_yaml_path=str(project_yaml))
output.write_text(content)
print(f"[pySimBlocks] run script generated: {output}")
diff --git a/pySimBlocks/project/generate_sofa_controller.py b/pySimBlocks/project/generate_sofa_controller.py
index 93bb8f0..611998a 100644
--- a/pySimBlocks/project/generate_sofa_controller.py
+++ b/pySimBlocks/project/generate_sofa_controller.py
@@ -18,39 +18,21 @@
# Authors: see Authors.txt
# ******************************************************************************
-import sys
+import importlib.util
+import inspect
import os
import re
-import inspect
+import sys
+from multiprocessing import Pipe, Process
from pathlib import Path
-from multiprocessing import Process, Pipe
-import importlib.util
-import yaml
-
-# =============================================================================
-#
-# =============================================================================
-def normalize_block_for_controller(block):
- """
- Force SofaPlant → SofaExchangeIO for controller generation.
- Both share 'input_keys' and 'output_keys'.
- """
- if block["type"].lower() == "sofa_plant":
- return {
- "name": block["name"],
- "type": "sofa_exchange_i_o",
- "from": "systems",
- "input_keys": block["input_keys"],
- "output_keys": block["output_keys"],
- }
- return block
+import yaml
def _load_scene_in_subprocess(scene_path, conn):
- # """
- # Load Sofa Scene in subprocess, get path file from controller.
- # """
+ """
+ Load SOFA scene in subprocess and return controller file path.
+ """
try:
scene_path = Path(scene_path).resolve()
scene_dir = scene_path.parent
@@ -71,7 +53,6 @@ def _load_scene_in_subprocess(scene_path, conn):
controller = out[1]
controller_file = inspect.getsourcefile(controller.__class__)
-
conn.send(controller_file)
except Exception as e:
@@ -82,7 +63,7 @@ def _load_scene_in_subprocess(scene_path, conn):
conn.close()
-def detect_controller_file_from_scene(scene_file):
+def detect_controller_file_from_scene(scene_file: Path) -> Path:
"""
Automatically get controller path from scene.
"""
@@ -109,118 +90,116 @@ def inject_base_dir(src: str) -> str:
"BASE_DIR = Path(__file__).resolve().parent\n\n"
)
- # Cherche la fin du bloc d'import
import_block = list(re.finditer(r"^(import|from)\s+.+$", src, re.MULTILINE))
-
if import_block:
last = import_block[-1]
insert_at = last.end()
return src[:insert_at] + "\n\n" + injection + src[insert_at:]
- else:
- # Aucun import → injecter en tête
- return injection + src
-def inject_yaml_paths_into_controller(
+ return injection + src
+
+
+def inject_project_path_into_controller(
controller_file: Path,
- model_yaml: Path,
- parameters_yaml: Path,
-):
+ project_yaml: Path,
+) -> None:
"""
- Inject or replace model_yaml and parameters_yaml attributes
- inside the SofaPysimBlocksController __init__.
+ Inject or replace project_yaml attribute inside SofaPysimBlocksController __init__.
"""
src = controller_file.read_text()
src = inject_base_dir(src)
- def replace_or_add(attr, rel_path):
- expr = (
- f'self.{attr} = str((BASE_DIR / "{rel_path}").resolve())'
+ controller_dir = controller_file.parent
+ rel_project = os.path.relpath(project_yaml, controller_dir)
+ expr = f'self.project_yaml = str((BASE_DIR / "{rel_project}").resolve())'
+
+ pattern = r"self\.project_yaml\s*=.*"
+ if re.search(pattern, src):
+ src = re.sub(pattern, expr, src)
+ else:
+ src = src.replace(
+ "super().__init__(name=name)",
+ f"super().__init__(name=name)\n {expr}",
)
- pattern = rf"self\.{attr}\s*=.*"
- if re.search(pattern, src):
- return re.sub(pattern, expr, src)
- else:
- return src.replace(
- "super().__init__(name=name)",
- f"super().__init__(name=name)\n {expr}"
- )
+ controller_file.write_text(src)
- controller_dir = controller_file.parent
- rel_model = os.path.relpath(model_yaml, controller_dir)
- rel_param = os.path.relpath(parameters_yaml, controller_dir)
+def _load_project_yaml(project_yaml: Path) -> dict:
+ if not project_yaml.exists():
+ raise FileNotFoundError(f"project.yaml not found: {project_yaml}")
- src = replace_or_add("model_yaml", rel_model)
- src = replace_or_add("parameters_yaml", rel_param)
+ raw = yaml.safe_load(project_yaml.read_text()) or {}
+ if not isinstance(raw, dict):
+ raise ValueError("project.yaml must define a YAML mapping")
+ return raw
- controller_file.write_text(src)
-# =============================================================================
-# MAIN
-# =============================================================================
-def generate_sofa_controller(
- project_dir: Path | None = None,
- model_yaml: Path | None = None,
- parameters_yaml: Path | None = None,
-):
- explicit_mode = model_yaml is not None or parameters_yaml is not None
+def _find_sofa_block(raw_project: dict) -> dict:
+ diagram = raw_project.get("diagram", {})
+ if not isinstance(diagram, dict):
+ raise ValueError("'diagram' section must be a mapping")
- if project_dir and explicit_mode:
- raise ValueError(
- "Cannot use project_dir together with --model/--param."
+ blocks = diagram.get("blocks", [])
+ if not isinstance(blocks, list):
+ raise ValueError("'diagram.blocks' section must be a list")
+
+ sofa_block = next(
+ (
+ b
+ for b in blocks
+ if isinstance(b, dict)
+ and str(b.get("type", "")).lower() in ("sofa_plant", "sofa_exchange_i_o")
+ ),
+ None,
+ )
+ if sofa_block is None:
+ raise RuntimeError(
+ "No SofaPlant or SofaExchangeIO block found in project.yaml"
)
+ return sofa_block
+
- if not project_dir and not explicit_mode:
+def _resolve_scene_file(project_yaml: Path, sofa_block: dict) -> Path:
+ params = sofa_block.get("parameters", {})
+ if not isinstance(params, dict):
raise ValueError(
- "You must specify either a project directory or --model and --param."
+ f"'diagram.blocks[{sofa_block.get('name', '?')}].parameters' must be a mapping"
)
-
- if explicit_mode:
- if model_yaml is None or parameters_yaml is None:
- raise ValueError(
- "Both --model and --param must be provided together."
+ scene_file = params.get("scene_file", None)
+ if not isinstance(scene_file, str) or not scene_file:
+ raise KeyError(
+ f"'scene_file' must be defined in parameters for block '{sofa_block.get('name', '?')}'"
)
+ path = Path(scene_file)
+ if not path.is_absolute():
+ path = (project_yaml.parent / path).resolve()
+ return path
- if project_dir:
- project_dir = Path(project_dir).resolve()
- model_yaml = project_dir / "model.yaml"
- parameters_yaml = project_dir / "parameters.yaml"
- if not model_yaml.exists():
- raise FileNotFoundError(model_yaml)
- if not parameters_yaml.exists():
- raise FileNotFoundError(parameters_yaml)
+def generate_sofa_controller(
+ project_dir: Path | None = None,
+ project_yaml: Path | None = None,
+) -> None:
+ has_project_path = project_yaml is not None
- # 1. Charger model.yaml pour trouver le bloc SOFA
- model_data = yaml.safe_load(model_yaml.read_text())
- params_data = yaml.safe_load(parameters_yaml.read_text())
+ if project_dir and has_project_path:
+ raise ValueError("Cannot use project_dir together with project_yaml.")
+ if not project_dir and not has_project_path:
+ raise ValueError("You must specify either project_dir or project_yaml.")
- blocks = model_data.get("blocks", [])
- sofa_block = next(
- (b for b in blocks if b["type"].lower() in ("sofa_plant", "sofa_exchange_i_o")),
- None
- )
- if sofa_block is None:
- raise RuntimeError("No SofaPlant or SofaExchangeIO block found in model.yaml")
+ if project_dir:
+ project_yaml = Path(project_dir).resolve() / "project.yaml"
+ else:
+ project_yaml = Path(project_yaml).resolve()
- # 2. Détection automatique du contrôleur depuis la scène
- try:
- scene_file = Path(params_data["blocks"][sofa_block["name"]]["scene_file"])
- except KeyError:
- raise KeyError(
- f"'scene_file' must be defined in parameters.yaml for block '{sofa_block['name']}'"
- )
+ raw_project = _load_project_yaml(project_yaml)
+ sofa_block = _find_sofa_block(raw_project)
+ scene_file = _resolve_scene_file(project_yaml, sofa_block)
controller_file = detect_controller_file_from_scene(scene_file)
- # 3. Injection des chemins YAML
- inject_yaml_paths_into_controller(
- controller_file,
- model_yaml,
- parameters_yaml,
- )
-
+ inject_project_path_into_controller(controller_file, project_yaml)
print(f"[pySimBlocks] SOFA controller updated: {controller_file}")
diff --git a/pySimBlocks/project/load_project_config.py b/pySimBlocks/project/load_project_config.py
index 288b974..840af5e 100644
--- a/pySimBlocks/project/load_project_config.py
+++ b/pySimBlocks/project/load_project_config.py
@@ -18,44 +18,178 @@
# Authors: see Authors.txt
# ******************************************************************************
-from pySimBlocks.project.load_simulation_config import load_simulation_config, _load_yaml
+from pathlib import Path
+from typing import Any, Dict, Tuple
+from pySimBlocks.core.config import PlotConfig, SimulationConfig
+from pySimBlocks.project.load_simulation_config import (
+ _check_no_external_refs,
+ _load_external_module,
+ _load_yaml,
+ _resolve_external_refs,
+ eval_recursive,
+)
-from pathlib import Path
-from typing import Tuple
-from pySimBlocks.core.config import SimulationConfig, ModelConfig, PlotConfig
+def _validate_schema_version(raw: Dict[str, Any]) -> None:
+ schema_version = raw.get("schema_version", None)
+ if schema_version != 1:
+ raise ValueError(
+ f"Unsupported or missing 'schema_version': {schema_version!r}. "
+ "Expected: 1"
+ )
-def load_project_config(
- parameters_yaml: str | Path,
- parameters_dir: Path | None = None,
-) -> Tuple[SimulationConfig, ModelConfig, PlotConfig | None]:
- """
- Load a full pySimBlocks project configuration.
- This function parses:
- - simulation configuration,
- - model numerical parameters,
- - optional plot configuration.
+def _load_scope(raw: Dict[str, Any], project_yaml: Path) -> Tuple[Any, Dict[str, Any]]:
+ simulation = raw.get("simulation", {})
+ if not isinstance(simulation, dict):
+ raise ValueError("'simulation' section must be a mapping")
+
+ external_module_path = simulation.get("external_module", None)
+ if external_module_path is None:
+ _check_no_external_refs(raw)
+ return None, {}
+
+ if not isinstance(external_module_path, str):
+ raise ValueError("'simulation.external_module' must be a path to a Python file")
+
+ external_path = project_yaml.parent / external_module_path
+ external_module, scope = _load_external_module(external_path)
+ return external_module, scope
+
+
+def _build_plot_config(sim_data: Dict[str, Any]) -> PlotConfig | None:
+ plots_data = sim_data.get("plots", None)
+ if plots_data is None:
+ return None
+
+ if not isinstance(plots_data, list):
+ raise ValueError("'simulation.plots' section must be a list")
+
+ plot_cfg = PlotConfig(plots=plots_data)
+ plot_cfg.validate()
+ return plot_cfg
+
+
+def _adapt_diagram_to_model_dict(
+ diagram_data: Dict[str, Any],
+ scope: Dict[str, Any],
+) -> Dict[str, Any]:
+ blocks = diagram_data.get("blocks", [])
+ if not isinstance(blocks, list):
+ raise ValueError("'diagram.blocks' section must be a list")
+
+ model_blocks: list[dict[str, Any]] = []
+ for desc in blocks:
+ if not isinstance(desc, dict):
+ raise ValueError("Each block in 'diagram.blocks' must be a mapping")
+
+ name = desc.get("name")
+ category = desc.get("category")
+ block_type = desc.get("type")
+ if not isinstance(name, str) or not isinstance(category, str) or not isinstance(block_type, str):
+ raise ValueError(
+ "Each block in 'diagram.blocks' must define string fields: "
+ "'name', 'category', and 'type'"
+ )
+
+ params_raw = desc.get("parameters", {})
+ if not isinstance(params_raw, dict):
+ raise ValueError(
+ f"'diagram.blocks[{name}].parameters' must be a mapping"
+ )
- Parameters:
- parameters_yaml: path to parameters.yaml
+ model_blocks.append(
+ {
+ "name": name,
+ "category": category,
+ "type": block_type,
+ "parameters": eval_recursive(params_raw, scope),
+ }
+ )
+
+ connections_raw = diagram_data.get("connections", [])
+ if not isinstance(connections_raw, list):
+ raise ValueError("'diagram.connections' section must be a list")
+
+ model_connections: list[list[str]] = []
+ for conn in connections_raw:
+ if not isinstance(conn, dict):
+ raise ValueError("Each connection in 'diagram.connections' must be a mapping")
+
+ ports = conn.get("ports", None)
+ if not isinstance(ports, list) or len(ports) != 2:
+ raise ValueError(
+ "Each connection in 'diagram.connections' must define "
+ "'ports: [src, dst]'"
+ )
+
+ src, dst = ports
+ if not isinstance(src, str) or not isinstance(dst, str):
+ raise ValueError(
+ "Connection ports in 'diagram.connections' must be strings"
+ )
+ model_connections.append([src, dst])
+
+ return {
+ "blocks": model_blocks,
+ "connections": model_connections,
+ }
+
+
+def load_project_config(
+ project_yaml: str | Path,
+) -> Tuple[SimulationConfig, Dict[str, Any], PlotConfig | None, str, Path]:
+ """
+ Load a full pySimBlocks unified project.yaml configuration.
Returns:
- (SimulationConfig, ModelConfig, PlotConfig or None)
+ (SimulationConfig, model_dict, PlotConfig | None, project_name, params_dir)
"""
- sim_cfg, model_cfg = load_simulation_config(parameters_yaml, parameters_dir)
+ project_yaml = Path(project_yaml)
+ raw = _load_yaml(project_yaml)
+
+ _validate_schema_version(raw)
+
+ external_module, scope = _load_scope(raw, project_yaml)
+
+ resolved = _resolve_external_refs(raw, external_module) if external_module else raw
+
+ project_data = resolved.get("project", {})
+ if not isinstance(project_data, dict):
+ raise ValueError("'project' section must be a mapping")
+ project_name = project_data.get("name", "model")
+ if not isinstance(project_name, str) or not project_name.strip():
+ raise ValueError("'project.name' must be a non-empty string")
+
+ sim_data = resolved.get("simulation", {})
+ if not isinstance(sim_data, dict):
+ raise ValueError("'simulation' section must be a mapping")
+
+ required = {"dt", "T"}
+ missing = required - sim_data.keys()
+ if missing:
+ raise ValueError(
+ f"Missing required simulation parameters in 'simulation': {sorted(missing)}"
+ )
- raw = _load_yaml(Path(parameters_yaml))
+ sim_eval_data = eval_recursive(sim_data, scope)
+ sim_cfg = SimulationConfig(
+ dt=sim_eval_data["dt"],
+ T=sim_eval_data["T"],
+ t0=sim_eval_data.get("t0", 0.0),
+ solver=sim_eval_data.get("solver", "fixed"),
+ logging=sim_data.get("logging", []),
+ clock=sim_eval_data.get("clock", "internal"),
+ )
+ sim_cfg.validate()
- plots_data = raw.get("plots", None)
- plot_cfg = None
+ plot_cfg = _build_plot_config(sim_data)
- if plots_data is not None:
- if not isinstance(plots_data, list):
- raise ValueError("'plots' section must be a list")
+ diagram_data = resolved.get("diagram", {})
+ if not isinstance(diagram_data, dict):
+ raise ValueError("'diagram' section must be a mapping")
- plot_cfg = PlotConfig(plots=plots_data)
- plot_cfg.validate()
+ model_dict = _adapt_diagram_to_model_dict(diagram_data, scope)
- return sim_cfg, model_cfg, plot_cfg
+ return sim_cfg, model_dict, plot_cfg, project_name, project_yaml.parent.resolve()
diff --git a/pySimBlocks/project/load_simulation_config.py b/pySimBlocks/project/load_simulation_config.py
index 6f5d7b8..4ba5c2e 100644
--- a/pySimBlocks/project/load_simulation_config.py
+++ b/pySimBlocks/project/load_simulation_config.py
@@ -24,40 +24,23 @@
import yaml
import numpy as np
import re
-from pySimBlocks.core.config import ModelConfig, SimulationConfig
+from pySimBlocks.core.config import SimulationConfig
# ---------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------
def _load_yaml(path: Path) -> Dict[str, Any]:
if not path.exists():
- raise FileNotFoundError(f"Parameters file not found: {path}")
+ raise FileNotFoundError(f"Project file not found: {path}")
with path.open("r") as f:
data = yaml.safe_load(f) or {}
if not isinstance(data, dict):
- raise ValueError("parameters.yaml must define a YAML mapping")
+ raise ValueError("project.yaml must define a YAML mapping")
return data
-def convert_to_str(raw: dict) -> dict:
- def _convert(v):
- if v is None:
- return None
- return str(v)
-
- if "simulation" in raw:
- raw["simulation"] = {k: _convert(v) for k, v in raw["simulation"].items()}
-
- if "blocks" in raw:
- raw["blocks"] = {
- b: {k: _convert(v) for k, v in params.items()}
- for b, params in raw["blocks"].items()
- }
-
- return raw
-
############################################################
@@ -116,7 +99,7 @@ def _check_no_external_refs(obj):
if refs:
raise ValueError(
f"Found external references {sorted(refs)} "
- "but no 'external' module is defined"
+ "but no external module is defined"
)
elif isinstance(obj, list):
@@ -167,93 +150,17 @@ def eval_recursive(obj: Any, scope: dict):
# Public API
# ---------------------------------------------------------------------
def load_simulation_config(
- parameters_yaml: str | Path,
- parameters_dir: Path | None = None,
-) -> Tuple[SimulationConfig, ModelConfig]:
+ project_yaml: str | Path,
+) -> Tuple[SimulationConfig, Dict[str, Any], Path]:
"""
- Load the configuration required to run a simulation.
- If a plot config is needed, use: load_project_config
-
- This function parses:
- - simulation configuration,
- - model numerical parameters,
-
- Parameters:
- parameters_yaml: path to parameters.yaml
+ Load simulation and diagram configuration from unified project.yaml.
Returns:
- (SimulationConfig, ModelConfig)
+ (SimulationConfig, model_dict, params_dir)
"""
- parameters_yaml = Path(parameters_yaml)
- raw = _load_yaml(parameters_yaml)
- raw = convert_to_str(raw)
-
- # ------------------------------------------------------------
- # External module handling
- # ------------------------------------------------------------
- external_module = None
- scope = {}
-
- if "external" in raw:
- external = raw["external"]
- if not isinstance(external, str):
- raise ValueError("'external' must be a path to a Python file")
-
- external_path = parameters_yaml.parent / external
- external_module, scope = _load_external_module(external_path)
- else:
- _check_no_external_refs(raw)
-
- # ------------------------------------------------------------
- # Resolve external references
- # ------------------------------------------------------------
- resolved = (
- _resolve_external_refs(raw, external_module)
- if external_module is not None
- else raw
- )
+ from pySimBlocks.project.load_project_config import load_project_config
- # ------------------------------------------------------------
- # SimulationConfig
- # ------------------------------------------------------------
- sim_data = resolved.get("simulation", {})
- if not sim_data:
- raise ValueError("Missing 'simulation' section in parameters.yaml")
-
- required = {"dt", "T"}
- missing = required - sim_data.keys()
- if missing:
- raise ValueError(
- f"Missing required simulation parameters: {sorted(missing)}"
- )
-
- sim_data = eval_recursive(sim_data, scope)
-
- sim_cfg = SimulationConfig(
- dt=sim_data["dt"],
- T=sim_data["T"],
- t0=sim_data.get("t0", 0.0),
- solver=sim_data.get("solver", "fixed"),
- logging=resolved.get("logging", []),
- clock=sim_data.get("clock", "internal")
+ sim_cfg, model_dict, _plot_cfg, _project_name, params_dir = load_project_config(
+ project_yaml
)
-
- sim_cfg.validate()
-
- # ------------------------------------------------------------
- # ModelConfig
- # ------------------------------------------------------------
- blocks_data = resolved.get("blocks", {})
- blocks_data = eval_recursive(blocks_data, scope)
- if not isinstance(blocks_data, dict):
- raise ValueError("'blocks' section must be a mapping")
-
- if parameters_dir is None:
- parameters_dir = parameters_yaml.parent.resolve()
-
- model_cfg = ModelConfig(
- blocks=blocks_data,
- parameters_dir=parameters_dir,
- )
-
- return sim_cfg, model_cfg
+ return sim_cfg, model_dict, params_dir
diff --git a/pySimBlocks/project/load_simulator.py b/pySimBlocks/project/load_simulator.py
new file mode 100644
index 0000000..72301d7
--- /dev/null
+++ b/pySimBlocks/project/load_simulator.py
@@ -0,0 +1,45 @@
+# ******************************************************************************
+# 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 pathlib import Path
+from typing import Tuple
+
+from pySimBlocks.core.config import PlotConfig
+from pySimBlocks.core.model import Model
+from pySimBlocks.core.simulator import Simulator
+from pySimBlocks.project.build_model import build_model_from_dict
+from pySimBlocks.project.load_project_config import load_project_config
+
+
+def load_simulator_from_project(
+ project_yaml: str | Path,
+) -> Tuple[Simulator, PlotConfig | None]:
+ """
+ Build and return a ready-to-run Simulator from a unified project.yaml.
+ """
+ sim_cfg, model_dict, plot_cfg, project_name, params_dir = load_project_config(
+ project_yaml
+ )
+
+ model = Model(name=project_name)
+ build_model_from_dict(model, model_dict, params_dir=params_dir)
+
+ sim = Simulator(model, sim_cfg)
+ return sim, plot_cfg
diff --git a/tests/gui/main_window_open_test.py b/tests/gui/main_window_open_test.py
index 1d93018..05b8f38 100644
--- a/tests/gui/main_window_open_test.py
+++ b/tests/gui/main_window_open_test.py
@@ -21,98 +21,90 @@ def test_main_window_opens(qtbot, tmp_path):
@pytest.fixture
def minimal_project(tmp_path):
- """Crée un projet pySimBlocks minimal avec YAMLs et layout."""
- # --- layout.yaml ---
- layout_yaml = tmp_path / "layout.yaml"
- layout_yaml.write_text(
- """version: 1
-blocks:
- ref:
- x: 0.0
- y: -80.0
- orientation: normal
- error:
- x: 195.0
- y: -71.0
- orientation: normal
- pid:
- x: 364.0
- y: -71.0
- orientation: normal
- plant:
- x: 535.0
- y: -70.0
- orientation: normal
-"""
- )
-
- # --- model.yaml ---
- model_yaml = tmp_path / "model.yaml"
- model_yaml.write_text(
- """blocks:
-- name: ref
- category: sources
- type: step
-- name: error
- category: operators
- type: sum
-- name: pid
- category: controllers
- type: pid
-- name: plant
- category: systems
- type: linear_state_space
-connections:
-- [ref.out, error.in1]
-- [plant.y, error.in2]
-- [error.out, pid.e]
-- [pid.u, plant.u]
-"""
- )
-
- # --- parameters.yaml ---
- parameters_yaml = tmp_path / "parameters.yaml"
- parameters_yaml.write_text(
- """simulation:
+ """Create a minimal pySimBlocks project.yaml."""
+ project_yaml = tmp_path / "project.yaml"
+ project_yaml.write_text(
+ """schema_version: 1
+project:
+ name: test_gui_project
+simulation:
dt: 0.01
T: 3.0
solver: fixed
-blocks:
- ref:
- value_before: [[0.0]]
- value_after: [[1.0]]
- start_time: 0.5
- error:
- signs: +-
- pid:
- controller: PI
- Kp: [[2.0]]
- Ki: [[1.0]]
- Kd: 0.0
- plant:
- A: [[0.95]]
- B: [[0.5]]
- C: [[1.0]]
- x0: [[0.0]]
-logging:
-- ref.outputs.out
-- plant.outputs.y
-- pid.outputs.u
-plots:
-- title: Ref vs Output
- signals:
+ logging:
- ref.outputs.out
- plant.outputs.y
-- title: Command
- signals:
- pid.outputs.u
+ plots:
+ - title: Ref vs Output
+ signals: [ref.outputs.out, plant.outputs.y]
+ - title: Command
+ signals: [pid.outputs.u]
+diagram:
+ blocks:
+ - name: ref
+ category: sources
+ type: step
+ parameters:
+ value_before: [[0.0]]
+ value_after: [[1.0]]
+ start_time: 0.5
+ - name: error
+ category: operators
+ type: sum
+ parameters:
+ signs: +-
+ - name: pid
+ category: controllers
+ type: pid
+ parameters:
+ controller: PI
+ Kp: [[2.0]]
+ Ki: [[1.0]]
+ Kd: 0.0
+ - name: plant
+ category: systems
+ type: linear_state_space
+ parameters:
+ A: [[0.95]]
+ B: [[0.5]]
+ C: [[1.0]]
+ x0: [[0.0]]
+ connections:
+ - name: c1
+ ports: [ref.out, error.in1]
+ - name: c2
+ ports: [plant.y, error.in2]
+ - name: c3
+ ports: [error.out, pid.e]
+ - name: c4
+ ports: [pid.u, plant.u]
+gui:
+ layout:
+ blocks:
+ ref:
+ x: 0.0
+ y: -80.0
+ orientation: normal
+ error:
+ x: 195.0
+ y: -71.0
+ orientation: normal
+ pid:
+ x: 364.0
+ y: -71.0
+ orientation: normal
+ plant:
+ x: 535.0
+ y: -70.0
+ orientation: normal
"""
)
return tmp_path
def test_main_window_loads_project(qtbot, minimal_project):
- """Test that MainWindow opens and auto-loads a project from YAMLs."""
+ """Test that MainWindow opens and auto-loads a project from project.yaml."""
window = MainWindow(minimal_project)
qtbot.addWidget(window)
window.show()