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()