diff --git a/examples/DrivingForce.fmu b/examples/DrivingForce.fmu index 35768c2..0d80ab0 100644 Binary files a/examples/DrivingForce.fmu and b/examples/DrivingForce.fmu differ diff --git a/examples/DrivingForce6D.fmu b/examples/DrivingForce6D.fmu new file mode 100644 index 0000000..6e658b9 Binary files /dev/null and b/examples/DrivingForce6D.fmu differ diff --git a/examples/HarmonicOscillator.fmu b/examples/HarmonicOscillator.fmu index 2bf2cbb..7867db0 100644 Binary files a/examples/HarmonicOscillator.fmu and b/examples/HarmonicOscillator.fmu differ diff --git a/examples/HarmonicOscillator6D.fmu b/examples/HarmonicOscillator6D.fmu index f2f67bc..c9fb30c 100644 Binary files a/examples/HarmonicOscillator6D.fmu and b/examples/HarmonicOscillator6D.fmu differ diff --git a/examples/bouncing_ball_3d.py b/examples/bouncing_ball_3d.py index 2e05770..30afef3 100644 --- a/examples/bouncing_ball_3d.py +++ b/examples/bouncing_ball_3d.py @@ -99,9 +99,9 @@ def next_bounce(self): p_bounce[2] = 0 return (self.time + dt_bounce, p_bounce) - def setup_experiment(self, start_time: float = 0.0): + def setup_experiment(self, start_time: float = 0.0, stop_time: float | None = None, tolerance: float | None = None): """Set initial (non-interface) variables.""" - super().setup_experiment(start_time) + super().setup_experiment(start_time, stop_time, tolerance) self.stopped = False self.time = start_time diff --git a/examples/bouncing_ball_3d_pythonfmu.py b/examples/bouncing_ball_3d_pythonfmu.py index 5ac8408..73f549c 100644 --- a/examples/bouncing_ball_3d_pythonfmu.py +++ b/examples/bouncing_ball_3d_pythonfmu.py @@ -113,9 +113,9 @@ def next_bounce(self) -> tuple[float, np.ndarray]: p_bounceY = self.posY + self.speedY * dt_bounce return (self.time + dt_bounce, np.array((p_bounceX, p_bounceY, 0), float)) - def setup_experiment(self, start_time: float): + def setup_experiment(self, start_time: float, stop_time: float | None = None, tolerance: float | None = None): """Set initial (non-interface) variables.""" - super().setup_experiment(start_time) + super().setup_experiment(start_time, stop_time, tolerance) # print(f"SETUP_EXPERIMENT g={self.g}, e={self.e}") self.stopped = False self.time = start_time diff --git a/pyproject.toml b/pyproject.toml index fe9a859..8db1f5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -60,7 +60,7 @@ dependencies = [ "numpy>=2.0", "pint>=0.24.4", "jsonpath-ng>=1.7.0", - "pythonfmu==0.6.9", + "pythonfmu>=0.7.0", "flexparser>=0.4", ] diff --git a/src/component_model/model.py b/src/component_model/model.py index d3c98f7..447af20 100644 --- a/src/component_model/model.py +++ b/src/component_model/model.py @@ -62,7 +62,7 @@ class Model(Fmi2Slave): Make sure that `super().do_step(time, dt)` is always called first in the extended function. * Optionally extend any other fmi2 function, i.e. - - def setup_experiment(self, start) + - def setup_experiment(self, start_time) - def enter_initialization_mode(self): - def exit_initialization_mode(self): - def terminate(self): @@ -152,7 +152,7 @@ def __init__( self.time = self.default_experiment.start_time # keeping track of time when dynamic calculations are performed self.derivatives: dict = {} # dict of non-explicit derivatives {dername : basevar, ...} - def setup_experiment(self, start_time: float = 0.0): + def setup_experiment(self, start_time: float = 0.0, stop_time: None | float = None, tolerance: None | float = None): """Minimum version of setup_experiment, just setting the start_time. Derived models may need to extend this.""" self.time = start_time diff --git a/src/component_model/utils/analysis.py b/src/component_model/utils/analysis.py index ef0c71f..45b87db 100644 --- a/src/component_model/utils/analysis.py +++ b/src/component_model/utils/analysis.py @@ -23,7 +23,7 @@ def extremum(x: tuple | list | np.ndarray, y: tuple | list | np.ndarray, aerr: f else: return (-1, (x0, z0)) else: - return (0, (0, 0)) + return (0.0, (0.0, 0.0)) def extremum_series(t: tuple | list | np.ndarray, y: tuple | list | np.ndarray, which: str = "max"): diff --git a/src/component_model/utils/controls.py b/src/component_model/utils/controls.py index 20f08ba..6c26f6f 100644 --- a/src/component_model/utils/controls.py +++ b/src/component_model/utils/controls.py @@ -103,7 +103,6 @@ def idx(self, name: str) -> int: def limit(self, ident: int | str, order: int, minmax: int, value: float | None = None) -> float: """Get/Set the single limit for 'idx', 'order', 'minmax'.""" idx = ident if isinstance(ident, int) else self.names.index(ident) - assert 0 <= idx < 3, f"Only idx = 0,1,2 allowed. Found {idx}" assert 0 <= order < 3, f"Only order = 0,1,2 allowed. Found {order}" assert 0 <= minmax < 2, f"Only minmax = 0,1 allowed. Found {minmax}" if value is not None: @@ -152,8 +151,6 @@ def setgoal(self, ident: int | str, order: int, value: float | None, t0: float = """ idx = ident if isinstance(ident, int) else self.names.index(ident) # check the index, the order and the value with respect to limits - if not 0 <= idx < 3: - raise ValueError(f"Only idx = 0,1,2 allowed. Found {idx}") from None if not 0 <= order < 3: raise ValueError(f"Only order = 0,1,2 allowed. Found {order}") from None # assert value is None or self.goals[idx] is None, "Change of goals is currently not implemented." @@ -236,27 +233,29 @@ def step(self, time: float, dt: float): for idx in range(self.dim): goals = self.goals[idx] if goals is not None: + _time = time # copies needed in case that there are several goals + _dt = dt _current = self.current[idx] _t, _current[2] = goals[self.rows[idx]] - while time > _t: # move row so that it starts in the right time-acc row + while _time > _t: # move row so that it starts in the right time-acc row self.rows[idx] += 1 _t, _current[2] = goals[self.rows[idx]] - while dt > 0: - if time + dt < _t: # covers the whole + while _dt > 0: + if _time + _dt < _t: # covers the whole _current[0] = self.check_limit( - idx, 0, _current[0] + _current[1] * dt + 0.5 * _current[2] * dt * dt + idx, 0, _current[0] + _current[1] * _dt + 0.5 * _current[2] * _dt * _dt ) - _current[1] = self.check_limit(idx, 1, _current[1] + _current[2] * dt) - dt = 0 + _current[1] = self.check_limit(idx, 1, _current[1] + _current[2] * _dt) + _dt = 0 else: # dt must be split - dt1 = _t - time + dt1 = _t - _time _current[0] = self.check_limit( idx, 0, _current[0] + _current[1] * dt1 + 0.5 * _current[2] * dt1 * dt1 ) _current[1] = self.check_limit(idx, 1, _current[1] + _current[2] * dt1) - time = _t - dt -= dt1 + _time = _t + _dt -= dt1 self.rows[idx] += 1 _t, _current[2] = goals[self.rows[idx]] diff --git a/tests/test_basic.py b/tests/test_basic.py index 78b128d..d930b5a 100644 --- a/tests/test_basic.py +++ b/tests/test_basic.py @@ -17,6 +17,10 @@ @pytest.fixture(scope="package") def build_fmu(): + return _build_fmu() + + +def _build_fmu(): build_path = Path.cwd() build_path.mkdir(exist_ok=True) fmu_path = FmuBuilder.build_FMU(__file__, project_files=[], dest=build_path) @@ -85,7 +89,7 @@ def __init__(self, **kwargs): # it is also possible to explicitly define getters and setters as lambdas in case the variable is not backed by a Python field. # self.register_variable(Real("myReal", causality=Fmi2Causality.output, getter=lambda: self.realOut, setter=lambda v: set_real_out(v)) - def setup_experiment(self, start_time: float): + def setup_experiment(self, start_time: float, stop_time=None, tolerance=None): """1. After instantiation the expriment is set up. In addition to start and end time also constant input variables are set.""" assert [self.vars[idx].getter() for idx in range(5)] == [ 1, @@ -141,3 +145,8 @@ def test_use_fmu(build_fmu): if __name__ == "__main__": retcode = pytest.main(["-rA", "-v", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" + import os + + os.chdir(Path(__file__).parent / "test_working_directory") + # test_make_fmu( _build_fmu()) + # test_use_fmu( _build_fmu()) diff --git a/tests/test_bouncing_ball_3d_fmu.py b/tests/test_bouncing_ball_3d_fmu.py index 216fab2..2e26e41 100644 --- a/tests/test_bouncing_ball_3d_fmu.py +++ b/tests/test_bouncing_ball_3d_fmu.py @@ -363,12 +363,12 @@ def test_from_fmu(bouncing_ball_fmu): if __name__ == "__main__": - retcode = 0 # pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) + retcode = pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" import os os.chdir(Path(__file__).parent / "test_working_directory") # test_bouncing_ball_class(show=False) # test_make_bouncing_ball(_bouncing_ball_fmu()) - test_use_fmu(_bouncing_ball_fmu(), True) + # test_use_fmu(_bouncing_ball_fmu(), True) # test_from_fmu( _bouncing_ball_fmu())