From acdcd520575d0f3ca90f4e03dc2216d9481debae Mon Sep 17 00:00:00 2001 From: Alessandrini Antoine Date: Mon, 16 Feb 2026 08:36:34 +0100 Subject: [PATCH 1/4] feat(sources): v1 source from file csv and np format --- pySimBlocks/blocks/sources/__init__.py | 2 + pySimBlocks/blocks/sources/file_source.py | 222 ++++++++++++++++++ .../docs/blocks/sources/file_source.md | 47 ++++ pySimBlocks/gui/blocks/sources/file_source.py | 94 ++++++++ .../project/pySimBlocks_blocks_index.yaml | 3 + test/blocks/sources/test_file_source.py | 128 ++++++++++ test/gui/test_file_source_meta.py | 14 ++ 7 files changed, 510 insertions(+) create mode 100644 pySimBlocks/blocks/sources/file_source.py create mode 100644 pySimBlocks/docs/blocks/sources/file_source.md create mode 100644 pySimBlocks/gui/blocks/sources/file_source.py create mode 100644 test/blocks/sources/test_file_source.py create mode 100644 test/gui/test_file_source_meta.py diff --git a/pySimBlocks/blocks/sources/__init__.py b/pySimBlocks/blocks/sources/__init__.py index 11ebee0..01d57b5 100644 --- a/pySimBlocks/blocks/sources/__init__.py +++ b/pySimBlocks/blocks/sources/__init__.py @@ -19,6 +19,7 @@ # ****************************************************************************** from pySimBlocks.blocks.sources.constant import Constant +from pySimBlocks.blocks.sources.file_source import FileSource from pySimBlocks.blocks.sources.ramp import Ramp from pySimBlocks.blocks.sources.step import Step from pySimBlocks.blocks.sources.sinusoidal import Sinusoidal @@ -26,6 +27,7 @@ __all__ = [ "Constant", + "FileSource", "Ramp", "Step", "Sinusoidal", diff --git a/pySimBlocks/blocks/sources/file_source.py b/pySimBlocks/blocks/sources/file_source.py new file mode 100644 index 0000000..da14faf --- /dev/null +++ b/pySimBlocks/blocks/sources/file_source.py @@ -0,0 +1,222 @@ +# ****************************************************************************** +# 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 Any, Dict + +import numpy as np + +from pySimBlocks.core.block_source import BlockSource + + +class FileSource(BlockSource): + """ + Source block that plays samples loaded from a file. + + Supported file types: + - npz: load an array from a key in a .npz archive (key mandatory) + - npy: load an array from a .npy file (no key) + - csv: load one numeric column by name (key=column name) + + Output policy: + - loaded data must be 1D or 2D + - each simulation step emits one row as a column vector + - when the end is reached: + * repeat=True -> restart from first sample + * repeat=False -> output zeros + """ + + VALID_FILE_TYPES = {"npz", "npy", "csv"} + + def __init__( + self, + name: str, + file_path: str, + file_type: str = "npz", + key: str | None = None, + repeat: bool = False, + sample_time: float | None = None, + ): + super().__init__(name, sample_time) + + if file_type not in self.VALID_FILE_TYPES: + raise ValueError( + f"[{self.name}] file_type must be one of {self.VALID_FILE_TYPES}." + ) + + self.file_path = str(file_path) + self.file_type = file_type + self.key = key + self.repeat = self._to_bool(repeat, "repeat") + + self._samples = self._load_samples() + self._index = 0 + self._output_shape = (self._samples.shape[1], 1) + + self.outputs["out"] = np.zeros(self._output_shape, dtype=float) + + # -------------------------------------------------------------------------- + # Class Methods + # -------------------------------------------------------------------------- + @classmethod + def adapt_params( + cls, + params: Dict[str, Any], + params_dir: Path | None = None, + ) -> Dict[str, Any]: + """ + Resolve relative file_path against parameters directory when provided. + """ + adapted = dict(params) + file_path = adapted.get("file_path") + if file_path is None: + return adapted + + path = Path(file_path) + if not path.is_absolute() and params_dir is not None: + path = (params_dir / path).resolve() + + adapted["file_path"] = str(path) + return adapted + + # -------------------------------------------------------------------------- + # Public methods + # -------------------------------------------------------------------------- + def initialize(self, t0: float) -> None: + self._index = 0 + self.outputs["out"] = self._current_output() + + # ------------------------------------------------------------------ + def output_update(self, t: float, dt: float) -> None: + self.outputs["out"] = self._current_output() + self._index += 1 + + # ------------------------------------------------------------------ + def state_update(self, t: float, dt: float) -> None: + pass + + # -------------------------------------------------------------------------- + # Private methods + # -------------------------------------------------------------------------- + def _load_samples(self) -> np.ndarray: + path = Path(self.file_path) + if not path.exists(): + raise FileNotFoundError(f"[{self.name}] File not found: {path}") + + if self.file_type == "npz": + arr = self._load_npz(path) + elif self.file_type == "npy": + arr = self._load_npy(path) + else: + arr = self._load_csv(path) + + if arr.ndim == 1: + arr = arr.reshape(-1, 1) + elif arr.ndim != 2: + raise ValueError( + f"[{self.name}] Loaded data must be 1D or 2D. Got shape {arr.shape}." + ) + + if arr.shape[0] == 0: + raise ValueError(f"[{self.name}] Loaded file contains no samples.") + + return arr.astype(float, copy=False) + + # ------------------------------------------------------------------ + def _load_npz(self, path: Path) -> np.ndarray: + with np.load(path) as data: + keys = list(data.files) + if len(keys) == 0: + raise ValueError(f"[{self.name}] NPZ archive contains no arrays.") + + selected_key = self.key + if not selected_key: + raise ValueError( + f"[{self.name}] key is mandatory for NPZ input." + ) + + if selected_key not in data: + raise KeyError( + f"[{self.name}] key '{selected_key}' not found in NPZ. " + f"Available keys: {keys}" + ) + + return np.asarray(data[selected_key], dtype=float) + + # ------------------------------------------------------------------ + def _load_npy(self, path: Path) -> np.ndarray: + if self.key not in (None, ""): + raise ValueError( + f"[{self.name}] key is not used for NPY input." + ) + return np.asarray(np.load(path), dtype=float) + + # ------------------------------------------------------------------ + def _load_csv(self, path: Path) -> np.ndarray: + if not self.key: + raise ValueError( + f"[{self.name}] key is mandatory for CSV input and must be a column name." + ) + + arr = np.genfromtxt(path, delimiter=",", names=True, dtype=float) + + if arr.size == 0: + raise ValueError(f"[{self.name}] CSV file is empty.") + if arr.dtype.names is None: + raise ValueError( + f"[{self.name}] CSV must contain a header row with column names." + ) + if self.key not in arr.dtype.names: + raise KeyError( + f"[{self.name}] column '{self.key}' not found in CSV. " + f"Available columns: {list(arr.dtype.names)}" + ) + + col = np.asarray(arr[self.key], dtype=float).reshape(-1, 1) + if np.isnan(col).any(): + raise ValueError( + f"[{self.name}] CSV column '{self.key}' contains non-numeric or missing values." + ) + return col + + # ------------------------------------------------------------------ + def _to_bool(self, value: bool | str, name: str) -> bool: + if isinstance(value, bool): + return value + if isinstance(value, str): + lowered = value.strip().lower() + if lowered in {"true", "1", "yes"}: + return True + if lowered in {"false", "0", "no"}: + return False + raise ValueError(f"[{self.name}] '{name}' must be a bool.") + + # ------------------------------------------------------------------ + def _current_output(self) -> np.ndarray: + n = self._samples.shape[0] + if self._index < n: + idx = self._index + elif self.repeat: + idx = self._index % n + else: + return np.zeros(self._output_shape, dtype=float) + + row = self._samples[idx] + return np.asarray(row, dtype=float).reshape(-1, 1) diff --git a/pySimBlocks/docs/blocks/sources/file_source.md b/pySimBlocks/docs/blocks/sources/file_source.md new file mode 100644 index 0000000..241ae4f --- /dev/null +++ b/pySimBlocks/docs/blocks/sources/file_source.md @@ -0,0 +1,47 @@ +# FileSource Block + +## Description + +The FileSource block loads a sequence of numeric samples from a file and outputs one sample per simulation step. + +Supported file formats: +- `npz` +- `npy` +- `csv` + +--- + +## Parameters + +| Name | Type | Description | Optional | +|------|------|-------------|----------| +| `file_path` | str | Path to source file. | False | +| `file_type` | enum (`npz`, `npy`, `csv`) | File format. | False (default: `npz`) | +| `key` | str | Mandatory for `npz` (array key) and `csv` (column name). Unused for `npy`. | True | +| `repeat` | bool | End-of-file behavior. If `false`, outputs zeros after the last sample. If `true`, restarts from the first sample. | True (default: `False`) | +| `sample_time` | float | Block sample time. If omitted, global simulation step is used. | True | + +--- + +## Inputs + +None. + +--- + +## Outputs + +| Port | Description | +|------|-------------| +| `out` | Current sample as a column vector. | + +--- + +## Notes + +- `npz`: loaded array must be 1D or 2D. +- `npy`: loaded array must be 1D or 2D. +- `csv`: `key` selects one named numeric column, producing shape `(1,1)` at each step. + +--- +© 2026 Université de Lille & INRIA - Licensed under LGPL-3.0-or-later diff --git a/pySimBlocks/gui/blocks/sources/file_source.py b/pySimBlocks/gui/blocks/sources/file_source.py new file mode 100644 index 0000000..3203bfd --- /dev/null +++ b/pySimBlocks/gui/blocks/sources/file_source.py @@ -0,0 +1,94 @@ +# ****************************************************************************** +# pySimBlocks +# Copyright (c) 2026 Université de Lille & INRIA +# ****************************************************************************** +# This program is free software: you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at your +# option) any later version. +# +# This program is distributed in the hope that it will be useful, but WITHOUT +# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +# FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License +# for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this program. If not, see . +# ****************************************************************************** +# Authors: see Authors.txt +# ****************************************************************************** + +from typing import Any, Dict + +from pySimBlocks.gui.blocks.block_meta import BlockMeta +from pySimBlocks.gui.blocks.parameter_meta import ParameterMeta +from pySimBlocks.gui.blocks.port_meta import PortMeta + + +class FileSourceMeta(BlockMeta): + + def __init__(self): + self.name = "FileSource" + self.category = "sources" + self.type = "file_source" + self.summary = "Read a sequence of samples from a CSV, NPY, or NPZ file." + self.description = ( + "Loads samples from file and outputs one sample per simulation step.\n\n" + "- `file_type = npz`: reads one array from the archive (`key` mandatory).\n" + "- `file_type = npy`: reads one array from a NPY file (`key` unused).\n" + "- `file_type = csv`: reads one numeric CSV column (`key` = column name).\n\n" + "Each step emits one row as a column vector." + ) + + self.parameters = [ + ParameterMeta( + name="file_path", + type="str", + required=True, + description="Path to the source file." + ), + ParameterMeta( + name="file_type", + type="enum", + required=True, + autofill=True, + default="npz", + enum=["npz", "npy", "csv"], + description="Input file format." + ), + ParameterMeta( + name="key", + type="str", + description="NPZ array key or CSV column name." + ), + ParameterMeta( + name="repeat", + type="enum", + autofill=True, + default=False, + enum=[False, True], + description="If true, replay samples from the beginning after end of file." + ), + ParameterMeta( + name="sample_time", + type="float", + description="Block execution period." + ), + ] + + self.outputs = [ + PortMeta( + name="out", + display_as="out", + shape=["n", 1], + description="Current output sample." + ) + ] + + def is_parameter_active(self, param_name: str, instance_values: Dict[str, Any]) -> bool: + file_type = instance_values.get("file_type", "npz") + + if param_name == "key": + return file_type in {"npz", "csv"} + + return super().is_parameter_active(param_name, instance_values) diff --git a/pySimBlocks/project/pySimBlocks_blocks_index.yaml b/pySimBlocks/project/pySimBlocks_blocks_index.yaml index 225a559..6c6e166 100644 --- a/pySimBlocks/project/pySimBlocks_blocks_index.yaml +++ b/pySimBlocks/project/pySimBlocks_blocks_index.yaml @@ -67,6 +67,9 @@ sources: constant: class: Constant module: pySimBlocks.blocks.sources.constant + file_source: + class: FileSource + module: pySimBlocks.blocks.sources.file_source ramp: class: Ramp module: pySimBlocks.blocks.sources.ramp diff --git a/test/blocks/sources/test_file_source.py b/test/blocks/sources/test_file_source.py new file mode 100644 index 0000000..746a7cf --- /dev/null +++ b/test/blocks/sources/test_file_source.py @@ -0,0 +1,128 @@ +from pathlib import Path + +import numpy as np +import pytest + +from pySimBlocks.blocks.sources.file_source import FileSource + + +def test_file_source_npz_single_array(tmp_path: Path): + path = tmp_path / "data.npz" + np.savez(path, y=np.array([[1.0, 2.0], [3.0, 4.0]])) + + blk = FileSource("src", file_path=str(path), file_type="npz", key="y") + blk.initialize(0.0) + assert np.allclose(blk.outputs["out"], [[1.0], [2.0]]) + + blk.output_update(0.0, 0.1) + assert np.allclose(blk.outputs["out"], [[1.0], [2.0]]) + + blk.output_update(0.1, 0.1) + assert np.allclose(blk.outputs["out"], [[3.0], [4.0]]) + + +def test_file_source_npz_requires_key(tmp_path: Path): + path = tmp_path / "data.npz" + np.savez(path, a=np.array([1.0]), b=np.array([2.0])) + + with pytest.raises(ValueError): + FileSource("src", file_path=str(path), file_type="npz") + + +def test_file_source_npz_invalid_key(tmp_path: Path): + path = tmp_path / "data.npz" + np.savez(path, a=np.array([1.0])) + + with pytest.raises(KeyError): + FileSource("src", file_path=str(path), file_type="npz", key="missing") + + +def test_file_source_csv(tmp_path: Path): + path = tmp_path / "data.csv" + path.write_text("a,b\n1.0,2.0\n3.0,4.0\n", encoding="utf-8") + + blk = FileSource("src", file_path=str(path), file_type="csv", key="b") + blk.initialize(0.0) + assert np.allclose(blk.outputs["out"], [[2.0]]) + + blk.output_update(0.0, 0.1) + assert np.allclose(blk.outputs["out"], [[2.0]]) + + blk.output_update(0.1, 0.1) + assert np.allclose(blk.outputs["out"], [[4.0]]) + + +def test_file_source_repeat_false_outputs_zeros_after_end(tmp_path: Path): + path = tmp_path / "data.npz" + np.savez(path, y=np.array([[5.0], [7.0]])) + + blk = FileSource( + "src", + file_path=str(path), + file_type="npz", + key="y", + repeat=False, + ) + blk.initialize(0.0) + assert np.allclose(blk.outputs["out"], [[5.0]]) + + blk.output_update(0.0, 0.1) + assert np.allclose(blk.outputs["out"], [[5.0]]) + + blk.output_update(0.1, 0.1) + assert np.allclose(blk.outputs["out"], [[7.0]]) + + blk.output_update(0.2, 0.1) + assert np.allclose(blk.outputs["out"], [[0.0]]) + + +def test_file_source_repeat_true_restarts_after_end(tmp_path: Path): + path = tmp_path / "data.npy" + np.save(path, np.array([[10.0], [20.0]])) + + blk = FileSource( + "src", + file_path=str(path), + file_type="npy", + repeat=True, + ) + blk.initialize(0.0) + assert np.allclose(blk.outputs["out"], [[10.0]]) + + blk.output_update(0.0, 0.1) + assert np.allclose(blk.outputs["out"], [[10.0]]) + + blk.output_update(0.1, 0.1) + assert np.allclose(blk.outputs["out"], [[20.0]]) + + blk.output_update(0.2, 0.1) + assert np.allclose(blk.outputs["out"], [[10.0]]) + + +def test_file_source_npy_key_not_allowed(tmp_path: Path): + path = tmp_path / "data.npy" + np.save(path, np.array([1.0, 2.0])) + + with pytest.raises(ValueError): + FileSource("src", file_path=str(path), file_type="npy", key="k") + + +def test_file_source_csv_missing_key(tmp_path: Path): + path = tmp_path / "data.csv" + path.write_text("a,b\n1.0,2.0\n", encoding="utf-8") + + with pytest.raises(ValueError): + FileSource("src", file_path=str(path), file_type="csv") + + +def test_file_source_invalid_file_type(tmp_path: Path): + path = tmp_path / "data.npz" + np.savez(path, y=np.array([1.0])) + + with pytest.raises(ValueError): + FileSource("src", file_path=str(path), file_type="txt") + + +def test_file_source_missing_file(): + with pytest.raises(FileNotFoundError): + FileSource("src", file_path="does-not-exist.npz", file_type="npz") diff --git a/test/gui/test_file_source_meta.py b/test/gui/test_file_source_meta.py new file mode 100644 index 0000000..038a303 --- /dev/null +++ b/test/gui/test_file_source_meta.py @@ -0,0 +1,14 @@ +from pySimBlocks.gui.blocks.sources.file_source import FileSourceMeta + + +def test_file_source_meta_conditional_parameters(): + meta = FileSourceMeta() + + npz_values = {"file_type": "npz"} + assert meta.is_parameter_active("key", npz_values) is True + + npy_values = {"file_type": "npy"} + assert meta.is_parameter_active("key", npy_values) is False + + csv_values = {"file_type": "csv"} + assert meta.is_parameter_active("key", csv_values) is True From 8a15f7c688be86948ded3bbad934fbba8dfe7099 Mon Sep 17 00:00:00 2001 From: Alessandrini Antoine Date: Mon, 16 Feb 2026 08:43:01 +0100 Subject: [PATCH 2/4] feat(sources): remove file_type on file source & deduce from path --- pySimBlocks/blocks/sources/file_source.py | 20 +++++++++------ .../docs/blocks/sources/file_source.md | 4 +-- pySimBlocks/gui/blocks/sources/file_source.py | 20 +++++---------- pySimBlocks/gui/dialogs/block_dialog.py | 3 +++ test/blocks/sources/test_file_source.py | 25 ++++++++----------- test/gui/test_file_source_meta.py | 6 ++--- 6 files changed, 38 insertions(+), 40 deletions(-) diff --git a/pySimBlocks/blocks/sources/file_source.py b/pySimBlocks/blocks/sources/file_source.py index da14faf..b8e4ec4 100644 --- a/pySimBlocks/blocks/sources/file_source.py +++ b/pySimBlocks/blocks/sources/file_source.py @@ -49,20 +49,14 @@ def __init__( self, name: str, file_path: str, - file_type: str = "npz", key: str | None = None, repeat: bool = False, sample_time: float | None = None, ): super().__init__(name, sample_time) - if file_type not in self.VALID_FILE_TYPES: - raise ValueError( - f"[{self.name}] file_type must be one of {self.VALID_FILE_TYPES}." - ) - self.file_path = str(file_path) - self.file_type = file_type + self.file_type = self._infer_file_type(self.file_path) self.key = key self.repeat = self._to_bool(repeat, "repeat") @@ -94,6 +88,8 @@ def adapt_params( path = (params_dir / path).resolve() adapted["file_path"] = str(path) + # Backward compatibility with older models that still contain file_type + adapted.pop("file_type", None) return adapted # -------------------------------------------------------------------------- @@ -208,6 +204,16 @@ def _to_bool(self, value: bool | str, name: str) -> bool: return False raise ValueError(f"[{self.name}] '{name}' must be a bool.") + # ------------------------------------------------------------------ + def _infer_file_type(self, file_path: str) -> str: + ext = Path(file_path).suffix.lower().lstrip(".") + if ext not in self.VALID_FILE_TYPES: + raise ValueError( + f"[{self.name}] Unsupported file extension '.{ext}'. " + f"Supported extensions: {sorted(self.VALID_FILE_TYPES)}" + ) + return ext + # ------------------------------------------------------------------ def _current_output(self) -> np.ndarray: n = self._samples.shape[0] diff --git a/pySimBlocks/docs/blocks/sources/file_source.md b/pySimBlocks/docs/blocks/sources/file_source.md index 241ae4f..c308cce 100644 --- a/pySimBlocks/docs/blocks/sources/file_source.md +++ b/pySimBlocks/docs/blocks/sources/file_source.md @@ -16,8 +16,7 @@ Supported file formats: | Name | Type | Description | Optional | |------|------|-------------|----------| | `file_path` | str | Path to source file. | False | -| `file_type` | enum (`npz`, `npy`, `csv`) | File format. | False (default: `npz`) | -| `key` | str | Mandatory for `npz` (array key) and `csv` (column name). Unused for `npy`. | True | +| `key` | str | Mandatory for `*.npz` (array key) and `*.csv` (column name). Unused for `*.npy`. | True | | `repeat` | bool | End-of-file behavior. If `false`, outputs zeros after the last sample. If `true`, restarts from the first sample. | True (default: `False`) | | `sample_time` | float | Block sample time. If omitted, global simulation step is used. | True | @@ -39,6 +38,7 @@ None. ## Notes +- File format is inferred from `file_path` extension (`.npz`, `.npy`, `.csv`). - `npz`: loaded array must be 1D or 2D. - `npy`: loaded array must be 1D or 2D. - `csv`: `key` selects one named numeric column, producing shape `(1,1)` at each step. diff --git a/pySimBlocks/gui/blocks/sources/file_source.py b/pySimBlocks/gui/blocks/sources/file_source.py index 3203bfd..4d3d938 100644 --- a/pySimBlocks/gui/blocks/sources/file_source.py +++ b/pySimBlocks/gui/blocks/sources/file_source.py @@ -34,9 +34,9 @@ def __init__(self): self.summary = "Read a sequence of samples from a CSV, NPY, or NPZ file." self.description = ( "Loads samples from file and outputs one sample per simulation step.\n\n" - "- `file_type = npz`: reads one array from the archive (`key` mandatory).\n" - "- `file_type = npy`: reads one array from a NPY file (`key` unused).\n" - "- `file_type = csv`: reads one numeric CSV column (`key` = column name).\n\n" + "- `.npz`: reads one array from the archive (`key` mandatory).\n" + "- `.npy`: reads one array from a NPY file (`key` unused).\n" + "- `.csv`: reads one numeric CSV column (`key` = column name).\n\n" "Each step emits one row as a column vector." ) @@ -47,15 +47,6 @@ def __init__(self): required=True, description="Path to the source file." ), - ParameterMeta( - name="file_type", - type="enum", - required=True, - autofill=True, - default="npz", - enum=["npz", "npy", "csv"], - description="Input file format." - ), ParameterMeta( name="key", type="str", @@ -86,9 +77,10 @@ def __init__(self): ] def is_parameter_active(self, param_name: str, instance_values: Dict[str, Any]) -> bool: - file_type = instance_values.get("file_type", "npz") + file_path = str(instance_values.get("file_path", "") or "") + ext = file_path.rsplit(".", 1)[-1].lower() if "." in file_path else "" if param_name == "key": - return file_type in {"npz", "csv"} + return ext in {"npz", "csv"} return super().is_parameter_active(param_name, instance_values) diff --git a/pySimBlocks/gui/dialogs/block_dialog.py b/pySimBlocks/gui/dialogs/block_dialog.py index 65ab64c..2f526c0 100644 --- a/pySimBlocks/gui/dialogs/block_dialog.py +++ b/pySimBlocks/gui/dialogs/block_dialog.py @@ -245,6 +245,9 @@ def _create_param_widget(self, edit.setText(str(value)) elif meta.default: edit.setText(str(meta.default)) + edit.textChanged.connect( + lambda val, name=param_name: self._on_param_changed(name, val) + ) return edit diff --git a/test/blocks/sources/test_file_source.py b/test/blocks/sources/test_file_source.py index 746a7cf..67c79af 100644 --- a/test/blocks/sources/test_file_source.py +++ b/test/blocks/sources/test_file_source.py @@ -10,7 +10,7 @@ def test_file_source_npz_single_array(tmp_path: Path): path = tmp_path / "data.npz" np.savez(path, y=np.array([[1.0, 2.0], [3.0, 4.0]])) - blk = FileSource("src", file_path=str(path), file_type="npz", key="y") + blk = FileSource("src", file_path=str(path), key="y") blk.initialize(0.0) assert np.allclose(blk.outputs["out"], [[1.0], [2.0]]) @@ -26,7 +26,7 @@ def test_file_source_npz_requires_key(tmp_path: Path): np.savez(path, a=np.array([1.0]), b=np.array([2.0])) with pytest.raises(ValueError): - FileSource("src", file_path=str(path), file_type="npz") + FileSource("src", file_path=str(path)) def test_file_source_npz_invalid_key(tmp_path: Path): @@ -34,14 +34,14 @@ def test_file_source_npz_invalid_key(tmp_path: Path): np.savez(path, a=np.array([1.0])) with pytest.raises(KeyError): - FileSource("src", file_path=str(path), file_type="npz", key="missing") + FileSource("src", file_path=str(path), key="missing") def test_file_source_csv(tmp_path: Path): path = tmp_path / "data.csv" path.write_text("a,b\n1.0,2.0\n3.0,4.0\n", encoding="utf-8") - blk = FileSource("src", file_path=str(path), file_type="csv", key="b") + blk = FileSource("src", file_path=str(path), key="b") blk.initialize(0.0) assert np.allclose(blk.outputs["out"], [[2.0]]) @@ -59,7 +59,6 @@ def test_file_source_repeat_false_outputs_zeros_after_end(tmp_path: Path): blk = FileSource( "src", file_path=str(path), - file_type="npz", key="y", repeat=False, ) @@ -83,7 +82,6 @@ def test_file_source_repeat_true_restarts_after_end(tmp_path: Path): blk = FileSource( "src", file_path=str(path), - file_type="npy", repeat=True, ) blk.initialize(0.0) @@ -104,7 +102,7 @@ def test_file_source_npy_key_not_allowed(tmp_path: Path): np.save(path, np.array([1.0, 2.0])) with pytest.raises(ValueError): - FileSource("src", file_path=str(path), file_type="npy", key="k") + FileSource("src", file_path=str(path), key="k") def test_file_source_csv_missing_key(tmp_path: Path): @@ -112,17 +110,16 @@ def test_file_source_csv_missing_key(tmp_path: Path): path.write_text("a,b\n1.0,2.0\n", encoding="utf-8") with pytest.raises(ValueError): - FileSource("src", file_path=str(path), file_type="csv") + FileSource("src", file_path=str(path)) -def test_file_source_invalid_file_type(tmp_path: Path): - path = tmp_path / "data.npz" - np.savez(path, y=np.array([1.0])) - +def test_file_source_invalid_extension(tmp_path: Path): + path = tmp_path / "data.txt" + path.write_text("1.0\n", encoding="utf-8") with pytest.raises(ValueError): - FileSource("src", file_path=str(path), file_type="txt") + FileSource("src", file_path=str(path)) def test_file_source_missing_file(): with pytest.raises(FileNotFoundError): - FileSource("src", file_path="does-not-exist.npz", file_type="npz") + FileSource("src", file_path="does-not-exist.npz") diff --git a/test/gui/test_file_source_meta.py b/test/gui/test_file_source_meta.py index 038a303..4fba594 100644 --- a/test/gui/test_file_source_meta.py +++ b/test/gui/test_file_source_meta.py @@ -4,11 +4,11 @@ def test_file_source_meta_conditional_parameters(): meta = FileSourceMeta() - npz_values = {"file_type": "npz"} + npz_values = {"file_path": "data.npz"} assert meta.is_parameter_active("key", npz_values) is True - npy_values = {"file_type": "npy"} + npy_values = {"file_path": "data.npy"} assert meta.is_parameter_active("key", npy_values) is False - csv_values = {"file_type": "csv"} + csv_values = {"file_path": "data.csv"} assert meta.is_parameter_active("key", csv_values) is True From a926b3b9d6da4c82625c97e54ec0bab01862ca44 Mon Sep 17 00:00:00 2001 From: Alessandrini Antoine Date: Mon, 16 Feb 2026 09:59:14 +0100 Subject: [PATCH 3/4] feat(sources): add time in file source block --- pySimBlocks/blocks/sources/file_source.py | 92 ++++++++++++++++--- .../docs/blocks/sources/file_source.md | 4 + pySimBlocks/gui/blocks/sources/file_source.py | 11 +++ test/blocks/sources/test_file_source.py | 60 ++++++++++++ test/gui/test_file_source_meta.py | 3 + 5 files changed, 157 insertions(+), 13 deletions(-) diff --git a/pySimBlocks/blocks/sources/file_source.py b/pySimBlocks/blocks/sources/file_source.py index b8e4ec4..0263662 100644 --- a/pySimBlocks/blocks/sources/file_source.py +++ b/pySimBlocks/blocks/sources/file_source.py @@ -51,6 +51,7 @@ def __init__( file_path: str, key: str | None = None, repeat: bool = False, + use_time: bool = False, sample_time: float | None = None, ): super().__init__(name, sample_time) @@ -59,7 +60,18 @@ def __init__( self.file_type = self._infer_file_type(self.file_path) self.key = key self.repeat = self._to_bool(repeat, "repeat") + self.use_time = self._to_bool(use_time, "use_time") + if self.use_time and self.file_type == "npy": + raise ValueError( + f"[{self.name}] use_time is supported only for NPZ and CSV inputs." + ) + if self.use_time and self.repeat: + raise ValueError( + f"[{self.name}] repeat cannot be used when use_time=True." + ) + + self._time: np.ndarray | None = None self._samples = self._load_samples() self._index = 0 self._output_shape = (self._samples.shape[1], 1) @@ -96,13 +108,19 @@ def adapt_params( # Public methods # -------------------------------------------------------------------------- def initialize(self, t0: float) -> None: - self._index = 0 - self.outputs["out"] = self._current_output() + if self.use_time: + self.outputs["out"] = self._current_output_at_time(t0) + else: + self._index = 0 + self.outputs["out"] = self._current_output() # ------------------------------------------------------------------ def output_update(self, t: float, dt: float) -> None: - self.outputs["out"] = self._current_output() - self._index += 1 + if self.use_time: + self.outputs["out"] = self._current_output_at_time(t) + else: + self.outputs["out"] = self._current_output() + self._index += 1 # ------------------------------------------------------------------ def state_update(self, t: float, dt: float) -> None: @@ -117,11 +135,11 @@ def _load_samples(self) -> np.ndarray: raise FileNotFoundError(f"[{self.name}] File not found: {path}") if self.file_type == "npz": - arr = self._load_npz(path) + arr, time = self._load_npz(path) elif self.file_type == "npy": - arr = self._load_npy(path) + arr, time = self._load_npy(path) else: - arr = self._load_csv(path) + arr, time = self._load_csv(path) if arr.ndim == 1: arr = arr.reshape(-1, 1) @@ -133,10 +151,12 @@ def _load_samples(self) -> np.ndarray: if arr.shape[0] == 0: raise ValueError(f"[{self.name}] Loaded file contains no samples.") + self._time = time + return arr.astype(float, copy=False) # ------------------------------------------------------------------ - def _load_npz(self, path: Path) -> np.ndarray: + def _load_npz(self, path: Path) -> tuple[np.ndarray, np.ndarray | None]: with np.load(path) as data: keys = list(data.files) if len(keys) == 0: @@ -154,18 +174,27 @@ def _load_npz(self, path: Path) -> np.ndarray: f"Available keys: {keys}" ) - return np.asarray(data[selected_key], dtype=float) + arr = np.asarray(data[selected_key], dtype=float) + time = None + if self.use_time: + if "time" not in data: + raise KeyError( + f"[{self.name}] use_time=True requires NPZ key 'time'." + ) + time = np.asarray(data["time"], dtype=float).reshape(-1) + self._validate_time(time, arr.shape[0]) + return arr, time # ------------------------------------------------------------------ - def _load_npy(self, path: Path) -> np.ndarray: + def _load_npy(self, path: Path) -> tuple[np.ndarray, np.ndarray | None]: if self.key not in (None, ""): raise ValueError( f"[{self.name}] key is not used for NPY input." ) - return np.asarray(np.load(path), dtype=float) + return np.asarray(np.load(path), dtype=float), None # ------------------------------------------------------------------ - def _load_csv(self, path: Path) -> np.ndarray: + def _load_csv(self, path: Path) -> tuple[np.ndarray, np.ndarray | None]: if not self.key: raise ValueError( f"[{self.name}] key is mandatory for CSV input and must be a column name." @@ -190,7 +219,15 @@ def _load_csv(self, path: Path) -> np.ndarray: raise ValueError( f"[{self.name}] CSV column '{self.key}' contains non-numeric or missing values." ) - return col + time = None + if self.use_time: + if "time" not in arr.dtype.names: + raise KeyError( + f"[{self.name}] use_time=True requires CSV column 'time'." + ) + time = np.asarray(arr["time"], dtype=float).reshape(-1) + self._validate_time(time, col.shape[0]) + return col, time # ------------------------------------------------------------------ def _to_bool(self, value: bool | str, name: str) -> bool: @@ -226,3 +263,32 @@ def _current_output(self) -> np.ndarray: row = self._samples[idx] return np.asarray(row, dtype=float).reshape(-1, 1) + + # ------------------------------------------------------------------ + def _current_output_at_time(self, t: float) -> np.ndarray: + if self._time is None: + raise RuntimeError( + f"[{self.name}] Internal error: use_time=True but time data is missing." + ) + + idx = int(np.searchsorted(self._time, t, side="right") - 1) + if idx < 0: + idx = 0 + + row = self._samples[idx] + return np.asarray(row, dtype=float).reshape(-1, 1) + + # ------------------------------------------------------------------ + def _validate_time(self, time: np.ndarray, n_samples: int) -> None: + if time.ndim != 1: + raise ValueError(f"[{self.name}] time must be a 1D array.") + if time.shape[0] != n_samples: + raise ValueError( + f"[{self.name}] time length ({time.shape[0]}) must match number of samples ({n_samples})." + ) + if np.isnan(time).any(): + raise ValueError(f"[{self.name}] time contains NaN values.") + if not np.all(np.diff(time) > 0.0): + raise ValueError( + f"[{self.name}] time must be strictly increasing." + ) diff --git a/pySimBlocks/docs/blocks/sources/file_source.md b/pySimBlocks/docs/blocks/sources/file_source.md index c308cce..eadde2a 100644 --- a/pySimBlocks/docs/blocks/sources/file_source.md +++ b/pySimBlocks/docs/blocks/sources/file_source.md @@ -18,6 +18,7 @@ Supported file formats: | `file_path` | str | Path to source file. | False | | `key` | str | Mandatory for `*.npz` (array key) and `*.csv` (column name). Unused for `*.npy`. | True | | `repeat` | bool | End-of-file behavior. If `false`, outputs zeros after the last sample. If `true`, restarts from the first sample. | True (default: `False`) | +| `use_time` | bool | If `true` (only for `*.npz` and `*.csv`), uses a `time` signal and applies ZOH: at time `t`, output sample at largest index `i` such that `T[i] <= t`. | True (default: `False`) | | `sample_time` | float | Block sample time. If omitted, global simulation step is used. | True | --- @@ -42,6 +43,9 @@ None. - `npz`: loaded array must be 1D or 2D. - `npy`: loaded array must be 1D or 2D. - `csv`: `key` selects one named numeric column, producing shape `(1,1)` at each step. +- With `use_time=true`, `time` must exist and be strictly increasing. + - `npz`: requires key `time`. + - `csv`: requires column `time`. --- © 2026 Université de Lille & INRIA - Licensed under LGPL-3.0-or-later diff --git a/pySimBlocks/gui/blocks/sources/file_source.py b/pySimBlocks/gui/blocks/sources/file_source.py index 4d3d938..35ca298 100644 --- a/pySimBlocks/gui/blocks/sources/file_source.py +++ b/pySimBlocks/gui/blocks/sources/file_source.py @@ -60,6 +60,14 @@ def __init__(self): enum=[False, True], description="If true, replay samples from the beginning after end of file." ), + ParameterMeta( + name="use_time", + type="enum", + autofill=True, + default=False, + enum=[False, True], + description="If true (NPZ/CSV), use 'time' data and apply ZOH at simulation time t." + ), ParameterMeta( name="sample_time", type="float", @@ -83,4 +91,7 @@ def is_parameter_active(self, param_name: str, instance_values: Dict[str, Any]) if param_name == "key": return ext in {"npz", "csv"} + if param_name == "use_time": + return ext in {"npz", "csv"} + return super().is_parameter_active(param_name, instance_values) diff --git a/test/blocks/sources/test_file_source.py b/test/blocks/sources/test_file_source.py index 67c79af..59dad77 100644 --- a/test/blocks/sources/test_file_source.py +++ b/test/blocks/sources/test_file_source.py @@ -105,6 +105,14 @@ def test_file_source_npy_key_not_allowed(tmp_path: Path): FileSource("src", file_path=str(path), key="k") +def test_file_source_npy_use_time_not_allowed(tmp_path: Path): + path = tmp_path / "data.npy" + np.save(path, np.array([1.0, 2.0])) + + with pytest.raises(ValueError): + FileSource("src", file_path=str(path), use_time=True) + + def test_file_source_csv_missing_key(tmp_path: Path): path = tmp_path / "data.csv" path.write_text("a,b\n1.0,2.0\n", encoding="utf-8") @@ -123,3 +131,55 @@ def test_file_source_invalid_extension(tmp_path: Path): def test_file_source_missing_file(): with pytest.raises(FileNotFoundError): FileSource("src", file_path="does-not-exist.npz") + + +def test_file_source_npz_use_time_zoh(tmp_path: Path): + path = tmp_path / "data.npz" + t = np.array([0.0, 0.2, 0.5], dtype=float) + y = np.array([[10.0], [20.0], [50.0]], dtype=float) + np.savez(path, time=t, y=y) + + blk = FileSource("src", file_path=str(path), key="y", use_time=True) + + blk.initialize(0.0) + assert np.allclose(blk.outputs["out"], [[10.0]]) + + blk.output_update(0.19, 0.1) + assert np.allclose(blk.outputs["out"], [[10.0]]) + + blk.output_update(0.20, 0.1) + assert np.allclose(blk.outputs["out"], [[20.0]]) + + blk.output_update(0.8, 0.1) + assert np.allclose(blk.outputs["out"], [[50.0]]) + + +def test_file_source_csv_use_time_zoh(tmp_path: Path): + path = tmp_path / "data.csv" + path.write_text( + "time,y\n0.0,1.0\n0.5,2.0\n1.0,3.0\n", + encoding="utf-8", + ) + + blk = FileSource("src", file_path=str(path), key="y", use_time=True) + blk.initialize(0.1) + assert np.allclose(blk.outputs["out"], [[1.0]]) + + blk.output_update(0.75, 0.1) + assert np.allclose(blk.outputs["out"], [[2.0]]) + + +def test_file_source_npz_time_must_be_strictly_increasing(tmp_path: Path): + path = tmp_path / "data.npz" + np.savez(path, time=np.array([0.0, 0.2, 0.2]), y=np.array([1.0, 2.0, 3.0])) + + with pytest.raises(ValueError): + FileSource("src", file_path=str(path), key="y", use_time=True) + + +def test_file_source_csv_use_time_requires_time_column(tmp_path: Path): + path = tmp_path / "data.csv" + path.write_text("y\n1.0\n2.0\n", encoding="utf-8") + + with pytest.raises(KeyError): + FileSource("src", file_path=str(path), key="y", use_time=True) diff --git a/test/gui/test_file_source_meta.py b/test/gui/test_file_source_meta.py index 4fba594..939374d 100644 --- a/test/gui/test_file_source_meta.py +++ b/test/gui/test_file_source_meta.py @@ -6,9 +6,12 @@ def test_file_source_meta_conditional_parameters(): npz_values = {"file_path": "data.npz"} assert meta.is_parameter_active("key", npz_values) is True + assert meta.is_parameter_active("use_time", npz_values) is True npy_values = {"file_path": "data.npy"} assert meta.is_parameter_active("key", npy_values) is False + assert meta.is_parameter_active("use_time", npy_values) is False csv_values = {"file_path": "data.csv"} assert meta.is_parameter_active("key", csv_values) is True + assert meta.is_parameter_active("use_time", csv_values) is True From a6d28d0aa0d8b3fccf55ed5cbc5a888b79094450 Mon Sep 17 00:00:00 2001 From: Alessandrini Antoine Date: Mon, 16 Feb 2026 09:59:38 +0100 Subject: [PATCH 4/4] ex(sources): add file source example --- examples/generate/sources/data.csv | 101 ++++++++++++++++++++++ examples/generate/sources/data.npy | Bin 0 -> 928 bytes examples/generate/sources/data.npz | Bin 0 -> 2096 bytes examples/generate/sources/layout.yaml | 20 +++++ examples/generate/sources/model.yaml | 11 +++ examples/generate/sources/parameters.yaml | 23 +++++ examples/generate/sources/temp.py | 16 ++++ 7 files changed, 171 insertions(+) create mode 100644 examples/generate/sources/data.csv create mode 100644 examples/generate/sources/data.npy create mode 100644 examples/generate/sources/data.npz create mode 100644 examples/generate/sources/layout.yaml create mode 100644 examples/generate/sources/model.yaml create mode 100644 examples/generate/sources/parameters.yaml create mode 100644 examples/generate/sources/temp.py diff --git a/examples/generate/sources/data.csv b/examples/generate/sources/data.csv new file mode 100644 index 0000000..812a8b0 --- /dev/null +++ b/examples/generate/sources/data.csv @@ -0,0 +1,101 @@ +time,y +0.0,0.0 +0.1,0.09983341664682815 +0.2,0.19866933079506122 +0.30000000000000004,0.2955202066613396 +0.4,0.3894183423086505 +0.5,0.479425538604203 +0.6000000000000001,0.5646424733950355 +0.7000000000000001,0.6442176872376911 +0.8,0.7173560908995228 +0.9,0.7833269096274834 +1.0,0.8414709848078965 +1.1,0.8912073600614354 +1.2000000000000002,0.9320390859672264 +1.3,0.963558185417193 +1.4000000000000001,0.9854497299884603 +1.5,0.9974949866040544 +1.6,0.9995736030415051 +1.7000000000000002,0.9916648104524686 +1.8,0.9738476308781951 +1.9000000000000001,0.9463000876874145 +2.0,0.9092974268256817 +2.1,0.8632093666488737 +2.2,0.8084964038195901 +2.3000000000000003,0.74570521217672 +2.4000000000000004,0.6754631805511506 +2.5,0.5984721441039565 +2.6,0.5155013718214642 +2.7,0.4273798802338298 +2.8000000000000003,0.33498815015590466 +2.9000000000000004,0.23924932921398198 +3.0,0.1411200080598672 +3.1,0.04158066243329049 +3.2,-0.058374143427580086 +3.3000000000000003,-0.15774569414324865 +3.4000000000000004,-0.25554110202683167 +3.5,-0.35078322768961984 +3.6,-0.44252044329485246 +3.7,-0.5298361409084934 +3.8000000000000003,-0.6118578909427193 +3.9000000000000004,-0.6877661591839741 +4.0,-0.7568024953079282 +4.1000000000000005,-0.8182771110644108 +4.2,-0.8715757724135882 +4.3,-0.9161659367494549 +4.4,-0.951602073889516 +4.5,-0.977530117665097 +4.6000000000000005,-0.9936910036334645 +4.7,-0.9999232575641008 +4.800000000000001,-0.9961646088358406 +4.9,-0.9824526126243325 +5.0,-0.9589242746631385 +5.1000000000000005,-0.9258146823277321 +5.2,-0.8834546557201531 +5.300000000000001,-0.8322674422239008 +5.4,-0.7727644875559871 +5.5,-0.7055403255703919 +5.6000000000000005,-0.6312666378723208 +5.7,-0.5506855425976376 +5.800000000000001,-0.4646021794137566 +5.9,-0.373876664830236 +6.0,-0.27941549819892586 +6.1000000000000005,-0.18216250427209502 +6.2,-0.0830894028174964 +6.300000000000001,0.0168139004843506 +6.4,0.11654920485049364 +6.5,0.21511998808781552 +6.6000000000000005,0.3115413635133787 +6.7,0.4048499206165983 +6.800000000000001,0.49411335113860894 +6.9,0.5784397643882001 +7.0,0.6569865987187891 +7.1000000000000005,0.7289690401258765 +7.2,0.7936678638491531 +7.300000000000001,0.8504366206285648 +7.4,0.8987080958116269 +7.5,0.9379999767747389 +7.6000000000000005,0.9679196720314865 +7.7,0.9881682338770004 +7.800000000000001,0.998543345374605 +7.9,0.998941341839772 +8.0,0.9893582466233818 +8.1,0.9698898108450863 +8.200000000000001,0.9407305566797726 +8.3,0.9021718337562933 +8.4,0.8545989080882804 +8.5,0.7984871126234903 +8.6,0.7343970978741133 +8.700000000000001,0.662969230082182 +8.8,0.5849171928917617 +8.9,0.5010208564578846 +9.0,0.4121184852417566 +9.1,0.3190983623493521 +9.200000000000001,0.22288991410024592 +9.3,0.1244544235070617 +9.4,0.024775425453357765 +9.5,-0.0751511204618093 +9.600000000000001,-0.1743267812229814 +9.700000000000001,-0.2717606264109442 +9.8,-0.3664791292519284 +9.9,-0.45753589377532133 diff --git a/examples/generate/sources/data.npy b/examples/generate/sources/data.npy new file mode 100644 index 0000000000000000000000000000000000000000..5436a4922abdbf9e64064e43b63086cda4c648a4 GIT binary patch literal 928 zcmbWr{ZrEg9KdnJ=myRk(0S+x*#SC;Jus$@Q2GD@fvABX5K2UBnkl&3c6bNHfmft* z=$R{qp10xQh21$Nh{XUu+zJ*aHNg?Q%jyS%yk*i2T9_;P+l$>EK_;o>J4 z{)xJFzjz9t>sJn@95Z7|D9iQPsudLju8jie9FBE{Gg-cisN89u>ESJ7|8KVHJhlT( z!A~rI&N*?{l}(BJbKOWpr*j&_9;{2&Cb{1yQ0*#tEB7&h$6P#0=>~xtN#vVSD}im! zh^k~Ef!r4BTKxkL_C%Q8&QQ3~7^VI(HQj~0;J$pK)qz64_~y^m%UJj@;OOv;MeJJp zE~J1vj|y#d7EeBlBbKY$fW8?F?cc(exs5n*=i!y-vwA%Fifi1FtHrR8Aji!nxdo7F3YA{80V$wD zLwa)o8q4{!-FZtu8J^pv7T6(65@sIS=Y$tAel9he1Y1g4yZWORsHV3`sY#s}t=dK^%o##)YlpIduSeg?PAgwx NL}53rH7#=n{{g9Iz)t`G literal 0 HcmV?d00001 diff --git a/examples/generate/sources/data.npz b/examples/generate/sources/data.npz new file mode 100644 index 0000000000000000000000000000000000000000..021d7fc5216e4c840e84e77da13b30ad552ed11a GIT binary patch literal 2096 zcmcK5drVVj6aesB9$K)D@)9Vo(xTujEm*Y-WvBRHGMuZT!c@kXMaL9ufM~?cpm9tk zxH#Da4FL>BTq4S3Fsk?&Hei5&g(09I1q3O5T-;D3HnP?pOfJMrYQ55Nx4uN$tg*~RK)=X#eSJHJM*47POsnmf$Sp&wU0U& zAWur)kroif4Op==G(f-&*ts_?BP}sCVQ-o|$()Z&+>@SU%BO#ps7Nxc14Dv?1q%h- zFS-9Z>@8hT)Y$@a)PUvM1WUx`=)`p|SezR3!?0_oIYx0`1Fg5sp+h%;`N!!0#QjOE zdy4KEx@mOJ(HYUbKu1X}_ot+m{uolr|655deOgN`{n?gbpmRm%if$gd zdFbY$b3^Bb&K;dQI(Kv)=seJI&~eal(0QWsMCXOh3!N7_Z*<=1e9-xz^Fil}&KDgQ zpBEQB7x%eX$3^Fd&JUd*IzM!N=y>RO=y>RO=y>S(*bg5a9~~bZUn+ZJvLBpQTzoaz zwx-EG@V|{UH{`zz_Uo}0wB)pupR9o!9pU_$6w_EMmEW~$I$-y%MVc^O4@}rbXN;9U zfUOZ1+;Sa`0HW7Wi}h>d+D!c(aGd{Rw{Z|P>XaMsy8HBCyjynRvyO~eK2Cx@>XNDZ&YE3#Nmy8V?uu3V!d5~mxR=#p`w zuw8uS3*{Jzy=uR{>GUY6eDb~97V$$;+?W|9DHPgx`@hR+%p~TFJBI_Xxmxl4J*?p*>tU#ZC?EasDmGG literal 0 HcmV?d00001 diff --git a/examples/generate/sources/layout.yaml b/examples/generate/sources/layout.yaml new file mode 100644 index 0000000..02fed48 --- /dev/null +++ b/examples/generate/sources/layout.yaml @@ -0,0 +1,20 @@ +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/generate/sources/model.yaml b/examples/generate/sources/model.yaml new file mode 100644 index 0000000..f3a1509 --- /dev/null +++ b/examples/generate/sources/model.yaml @@ -0,0 +1,11 @@ +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/generate/sources/parameters.yaml b/examples/generate/sources/parameters.yaml new file mode 100644 index 0000000..1c1bba0 --- /dev/null +++ b/examples/generate/sources/parameters.yaml @@ -0,0 +1,23 @@ +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/generate/sources/temp.py b/examples/generate/sources/temp.py new file mode 100644 index 0000000..3eec5a3 --- /dev/null +++ b/examples/generate/sources/temp.py @@ -0,0 +1,16 @@ +import csv +import numpy as np + +t = np.arange(0, 10, 0.1) +y = np.sin(t) +y = y.reshape(-1, 1) + +np.savez('data.npz', time=t, y=y) + +np.save('data.npy', y) + +with open('data.csv', 'w', newline='') as csvfile: + writer = csv.writer(csvfile) + writer.writerow(['time', 'y']) # Write header + for t_val, y_val in zip(t, y): + writer.writerow([t_val, y_val[0]]) # Write each row of data