diff --git a/.github/workflows/_build_package.yml b/.github/workflows/_build_package.yml
index 69b5fb3..9388bd2 100644
--- a/.github/workflows/_build_package.yml
+++ b/.github/workflows/_build_package.yml
@@ -21,6 +21,6 @@ jobs:
run: python -m build
- name: Run twine check
run: twine check --strict dist/*
- - uses: actions/upload-artifact@v3
+ - uses: actions/upload-artifact@v4
with:
path: ./dist/*.tar.gz
diff --git a/.github/workflows/_publish_package.yml b/.github/workflows/_publish_package.yml
index 7c547fe..661ffdd 100644
--- a/.github/workflows/_publish_package.yml
+++ b/.github/workflows/_publish_package.yml
@@ -10,7 +10,7 @@ jobs:
permissions:
id-token: write
steps:
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
with:
name: artifact
path: ./dist/
diff --git a/.github/workflows/_publish_package_test.yml b/.github/workflows/_publish_package_test.yml
index 0a3d7b9..9d37f6c 100644
--- a/.github/workflows/_publish_package_test.yml
+++ b/.github/workflows/_publish_package_test.yml
@@ -10,7 +10,7 @@ jobs:
permissions:
id-token: write
steps:
- - uses: actions/download-artifact@v3
+ - uses: actions/download-artifact@v4
with:
name: artifact
path: ./dist/
diff --git a/tests/conftest.py b/tests/conftest.py
index 129f993..7e737d3 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -51,3 +51,12 @@ def setup_logging(caplog: LogCaptureFixture):
@pytest.fixture(autouse=True)
def logger():
return logging.getLogger()
+
+
+def pytest_addoption(parser):
+ parser.addoption("--show", action="store", default=False)
+
+
+@pytest.fixture(scope="session")
+def show(request):
+ return request.config.getoption("--show") == "True"
diff --git a/tests/data/BouncingBall.cases b/tests/data/BouncingBall.cases
deleted file mode 100644
index 6e2baac..0000000
--- a/tests/data/BouncingBall.cases
+++ /dev/null
@@ -1,48 +0,0 @@
-{name : 'BouncingBall',
-description : 'Case Study with the BouncingBall FMU as a simple test of the case_study modules',
-modelFile : "../data/BouncingBall/OspSystemStructure.xml",
-timeUnit : "second",
-variables : {
- x : ['bb', 'x', "Position of ball (x,z)"],
- v : ['bb', 'v', "Speed of ball (x,z)"],
- v0: ['bb', 'v0', "Speed of ball (x,z) at time 0"],
- b : ['bb', 'bounceFactor', "factor on speed when bouncing"],
- d : ['bb', 'drag', "drag decelleration factor defined as a = self.drag* v^2 with dimension 1/m"],
- e : ['bb', 'energy', "Total energy of ball in J"],
- p : ['bb', 'period', "Bouncing period of ball"],
- },
-base : {
- description : "Variable settings for the base case. All other cases are based on that",
- spec: {
- stopTime : '3',
- v0 : [1.0,1.0],
- b : 0.95,
- d : 0.0,
- }},
-case1 : {
- description : "Change the start velocity in z-direction",
- spec: {
- v0 : [1.0, 2.0],
- }},
-case2 : {
- description : "Based case1 (v0_z change), change also bouncing factor",
- parent : 'case1',
- spec : {
- b : 9.0
- }},
-case3 : {
- description : "Related directly to base case, changing bouncing factor alone",
- spec : {
- b : 9.0
- }},
-results : {
- spec : [
- x@step,
- v@1.0,
- b,
- d,
- ]}
-},
-evaluation: {
- checkSpeed: "F(bb.v = 0)" # Eventually the speed of the BouncingBall will be 0
-}
diff --git a/tests/data/BouncingBall3D/BouncingBall.cases b/tests/data/BouncingBall3D/BouncingBall.cases
new file mode 100644
index 0000000..584c74a
--- /dev/null
+++ b/tests/data/BouncingBall3D/BouncingBall.cases
@@ -0,0 +1,44 @@
+{name : 'BouncingBall',
+description : 'Simple Case Study with the 3D BouncingBall FMU (3D position and speed',
+modelFile : "OspSystemStructure.xml",
+timeUnit : "second",
+variables : {
+ g : ['bb', 'g', "Gravity acting on the ball"],
+ e : ['bb', 'e', "Coefficient of restitution"],
+ x : ['bb', 'pos', "3D Position of the ball in meters"],
+ v : ['bb', 'speed', "3D speed of ball in meters/second"],
+ x_b : ['bb', 'p_bounce', "Expected 3D Position where the next bounce will occur (in meters)"],
+ },
+base : {
+ description : "Ball dropping from height 1 m. Results should be the same as the basic BouncingBall",
+ spec: {
+ stepSize : 0.01,
+ stopTime : '3',
+ g : -9.81,
+ e : 1.0,
+ x[2] : 1.0,
+ }},
+restitution : {
+ description : "Smaller coefficient of restitution e",
+ spec: {
+ e : 0.5,
+ }},
+restitutionAndGravity : {
+ description : "Based restitution (e change), change also the gravity g",
+ parent : 'restitution',
+ spec : {
+ g : -1.5
+ }},
+gravity : {
+ description : "Gravity like on the moon",
+ spec : {
+ g : -1.5
+ }},
+results : {
+ spec : [
+ e@0.0,
+ g@0.0,
+ x@step,
+ v@step,
+ ]}
+}
diff --git a/tests/data/BouncingBall3D/BouncingBall3D.fmu b/tests/data/BouncingBall3D/BouncingBall3D.fmu
new file mode 100644
index 0000000..002b7e7
Binary files /dev/null and b/tests/data/BouncingBall3D/BouncingBall3D.fmu differ
diff --git a/tests/data/BouncingBall3D/OspSystemStructure.xml b/tests/data/BouncingBall3D/OspSystemStructure.xml
new file mode 100644
index 0000000..3bd9210
--- /dev/null
+++ b/tests/data/BouncingBall3D/OspSystemStructure.xml
@@ -0,0 +1,11 @@
+
+
+ 0.01
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/tests/data/BouncingBall3D/bouncing_ball_3d.py b/tests/data/BouncingBall3D/bouncing_ball_3d.py
new file mode 100644
index 0000000..035b168
--- /dev/null
+++ b/tests/data/BouncingBall3D/bouncing_ball_3d.py
@@ -0,0 +1,156 @@
+from math import sqrt
+
+import numpy as np
+
+from component_model.model import Model
+from component_model.variable import Variable
+
+
+class BouncingBall3D(Model):
+ """Another BouncingBall model, made in Python and using Model and Variable to construct a FMU.
+
+ Special features:
+
+ * The ball has a 3-D vector as position and speed
+ * As output variable the model estimates the next bouncing point
+ * As input variables, the restitution coefficient `e` and the ground angle at the bouncing point can be changed.
+ * Internal units are SI (m,s,rad)
+
+ Args:
+ pos (np.array)=(0,0,1): The 3-D position in of the ball at time [m]
+ speed (np.array)=(1,0,0): The 3-D speed of the ball at time [m/s]
+ g (float)=9.81: The gravitational acceleration [m/s^2]
+ e (float)=0.9: The coefficient of restitution (dimensionless): |speed after| / |speed before| collision
+ min_speed_z (float)=1e-6: The minimum speed in z-direction when bouncing stops [m/s]
+ """
+
+ def __init__(
+ self,
+ name: str = "BouncingBall3D",
+ description="Another BouncingBall model, made in Python and using Model and Variable to construct a FMU",
+ pos: tuple = (0, 0, 10),
+ speed: tuple = (1, 0, 0),
+ g: float = 9.81,
+ e: float = 0.9,
+ min_speed_z: float = 1e-6,
+ **kwargs,
+ ):
+ super().__init__(name, description, author="DNV, SEACo project", **kwargs)
+ self._pos = self._interface( 'pos', pos)
+ self._speed = self._interface( 'speed', speed)
+ self.a = np.array((0, 0, -g), float)
+ self._g = self._interface( 'g', g)
+ self._e = self._interface( 'e', e)
+ self.min_speed_z = min_speed_z
+ self.stopped = False
+ self.time = 0.0
+ self._p_bounce = self._interface( 'p_bounce', ('0m', '0m','0m')) # instantiates self.p_bounce. z always 0.
+ self.t_bounce, self.p_bounce = self.next_bounce()
+
+ def _interface(self, name:str, start:float|tuple):
+ """Define a FMU2 interface variable, using the variable interface.
+
+ Args:
+ name (str): base name of the variable
+ start (str|float|tuple): start value of the variable (optionally with units)
+
+ Returns:
+ the variable object. As a side effect the variable value is made available as self.
+ """
+ if name == 'pos':
+ return Variable(
+ self,
+ name="pos",
+ description="The 3D position of the ball [m] (height in inch as displayUnit example.",
+ causality="output",
+ variability="continuous",
+ initial="exact",
+ start=start,
+ rng=((0, "100 m"), None, (0, "10 m")),
+ )
+ elif name == 'speed':
+ return Variable(
+ self,
+ name="speed",
+ description="The 3D speed of the ball, i.e. d pos / dt [m/s]",
+ causality="output",
+ variability="continuous",
+ initial="exact",
+ start=start,
+ rng=((0, "1 m/s"), None, ("-100 m/s", "100 m/s")),
+ )
+ elif name == 'g':
+ return Variable(
+ self,
+ name="g",
+ description="The gravitational acceleration (absolute value).",
+ causality="parameter",
+ variability="fixed",
+ start=start,
+ rng=(),
+ )
+ elif name == 'e':
+ return Variable(
+ self,
+ name="e",
+ description="The coefficient of restitution, i.e. |speed after| / |speed before| bounce.",
+ causality="parameter",
+ variability="fixed",
+ start=start,
+ rng=(),
+ )
+ elif name == 'p_bounce':
+ return Variable(
+ self,
+ name="p_bounce",
+ description="The expected position of the next bounce as 3D vector",
+ causality="output",
+ variability="continuous",
+ start=start,
+ rng=(),
+ )
+
+ def do_step(self, time, dt):
+ """Perform a simulation step from `time` to `time + dt`."""
+ if not super().do_step(time, dt):
+ return False
+ self.t_bounce, self.p_bounce = self.next_bounce()
+ # print(f"Step@{time}. pos:{self.pos}, speed{self.speed}, t_bounce:{self.t_bounce}, p_bounce:{self.p_bounce}")
+ while dt > self.t_bounce: # if the time is this long
+ dt -= self.t_bounce
+ self.pos = self.p_bounce
+ self.speed -= self.a * self.t_bounce # speed before bouncing
+ self.speed[2] = -self.speed[2] # speed after bouncing if e==1.0
+ self.speed *= self.e # speed reduction due to coefficient of restitution
+ if self.speed[2] < self.min_speed_z:
+ self.stopped = True
+ self.a[2] = 0.0
+ self.speed[2] = 0.0
+ self.pos[2] = 0.0
+ self.t_bounce, self.p_bounce = self.next_bounce()
+ self.pos += self.speed * dt + 0.5 * self.a * dt**2
+ self.speed += self.a * dt
+ if self.pos[2] < 0:
+ self.pos[2] = 0
+ print(f"@{time}. pos {self.pos}, speed {self.speed}, bounce {self.t_bounce}")
+ return True
+
+ def next_bounce(self):
+ """Calculate time until next bounce and position where the ground will be hit,
+ based on current time, pos and speed.
+ """
+ if self.stopped: # stopped bouncing
+ return (1e300, np.array((1e300, 1e300, 0), float))
+ # return ( float('inf'), np.array( (float('inf'), float('inf'), 0), float))
+ else:
+ t_bounce = (self.speed[2] + sqrt(self.speed[2] ** 2 + 2 * self.g * self.pos[2])) / self.g
+ p_bounce = self.pos + self.speed * t_bounce # linear. not correct for z-direction!
+ p_bounce[2] = 0
+ return (t_bounce, p_bounce)
+
+ def setup_experiment(self, start: float):
+ """Set initial (non-interface) variables."""
+ super().setup_experiment(start)
+ self.stopped = False
+ self.a = np.array((0, 0, -self.g), float)
+
diff --git a/tests/data/BouncingBall_0.cases b/tests/data/BouncingBall_0.cases
deleted file mode 100644
index 8e9693f..0000000
--- a/tests/data/BouncingBall_0.cases
+++ /dev/null
@@ -1,45 +0,0 @@
-{name : 'BouncingBall',
-description : 'Simple Case Study with the basic BouncingBall FMU (ball dropped from h=1m',
-modelFile : "../data/BouncingBall/OspSystemStructure.xml",
-timeUnit : "second",
-variables : {
- g : ['bb', 'g', "Gravity acting on the ball"],
- e : ['bb', 'e', "Coefficient of restitution"],
- v_min : ['bb', 'v_min', "Velocity below which the ball stops bouncing"],
- h : ['bb', 'h', "Position (z) of the ball"],
- v_z : ['bb', 'der(h)', "Derivative of h (speed in z-direction"],
- v : ['bb', 'v', "Velocity of ball"],
- a_z : ['bb', 'der(v)', "Derivative of v (acceleration in z-direction)"],
- },
-base : {
- description : "Variable settings for the base case. All other cases are based on that",
- spec: {
- stepSize : 0.1,
- stopTime : '3',
- g : -9.81,
- e : 0.71,
- }},
-case1 : {
- description : "Smaller coefficient of restitution e",
- spec: {
- e : 0.35,
- }},
-case2 : {
- description : "Based case1 (e change), change also the gravity g",
- parent : 'case1',
- spec : {
- g : 1.5
- }},
-case3 : {
- description : "Related directly to base case, larger e",
- spec : {
- e : 1.4
- }},
-results : {
- spec : [
- h@step,
- v@1.0,
- e,
- g,
- ]}
-}
diff --git a/tests/test_BouncingBall.py b/tests/test_BouncingBall.py
index e57103d..51c9008 100644
--- a/tests/test_BouncingBall.py
+++ b/tests/test_BouncingBall.py
@@ -1,19 +1,28 @@
+from math import sqrt
from pathlib import Path
-from fmpy import simulate_fmu
+import pytest
+from fmpy import plot_result, simulate_fmu
-""" Test and validate the basic BouncingBall using fmpy and not using OSP or case_study."""
+def nearly_equal(res: tuple, expected: tuple, eps=1e-7):
+ assert len(res) == len(
+ expected
+ ), f"Tuples of different lengths cannot be equal. Found {len(res)} != {len(expected)}"
+ for i, (x, y) in enumerate(zip(res, expected, strict=False)):
+ assert abs(x - y) < eps, f"Element {i} not nearly equal in {x}, {y}"
-def test_run_fmpy():
+
+def test_run_fmpy(show):
+ """Test and validate the basic BouncingBall using fmpy and not using OSP or case_study."""
path = Path(Path(__file__).parent, "data/BouncingBall0/BouncingBall.fmu")
assert path.exists(), f"File {path} does not exist"
-
- _ = simulate_fmu( # type: ignore
+ stepsize = 0.01
+ result = simulate_fmu(
path,
start_time=0.0,
stop_time=3.0,
- step_size=0.1,
+ step_size=stepsize,
validate=True,
solver="Euler",
debug_logging=False,
@@ -21,6 +30,34 @@ def test_run_fmpy():
logger=print, # fmi_call_logger=print,
start_values={
"e": 0.71,
- "g": -9.82,
+ "g": -9.81,
},
)
+ if show:
+ plot_result(result)
+ nearly_equal(result[0], (0, 1, 0))
+ t_before = int(sqrt(2 / 9.81) / stepsize) * stepsize # just before bounce
+ nearly_equal(
+ result[int(t_before / stepsize)],
+ (t_before, 1.0 - 0.5 * 9.81 * t_before * t_before, -9.81 * t_before),
+ eps=0.003,
+ )
+ t_bounce = sqrt(2 / 9.81)
+ v_bounce = 9.81 * t_bounce
+ nearly_equal(
+ result[int(t_before / stepsize) + 1],
+ (
+ t_before + stepsize,
+ v_bounce * 0.71 * (t_before + stepsize - t_bounce) - 0.5 * 9.81 * (t_before + stepsize - t_bounce) ** 2,
+ v_bounce * 0.71 - 9.81 * (t_before + stepsize - t_bounce),
+ ),
+ eps=0.03,
+ )
+ nearly_equal(result[int(2.5 / stepsize)], (2.5, 0, 0), eps=0.4)
+ nearly_equal(result[int(3 / stepsize)], (3, 0, 0))
+ print("RESULT", result[int(t_before / stepsize) + 1])
+
+
+if __name__ == "__main__":
+ retcode = pytest.main(["-rA", "-v", __file__, "--show", "True"])
+ assert retcode == 0, f"Non-zero return code {retcode}"
diff --git a/tests/test_OspSystemStructure.py b/tests/test_OspSystemStructure.py
index 753b89b..d28e787 100644
--- a/tests/test_OspSystemStructure.py
+++ b/tests/test_OspSystemStructure.py
@@ -1,5 +1,6 @@
from pathlib import Path
+import pytest
from libcosimpy.CosimEnums import CosimVariableCausality, CosimVariableType, CosimVariableVariability
from libcosimpy.CosimExecution import CosimExecution # type: ignore
@@ -10,6 +11,9 @@ def test_system_structure():
sim = CosimExecution.from_osp_config_file(str(path))
assert sim.execution_status.current_time == 0
assert sim.execution_status.state == 0
+ assert len(sim.slave_infos()) == 3, "Three bouncing balls were included!"
+ assert sim.slave_infos()[0].name.decode() == "bb2", "The order of components is not maintained within OSP"
+ assert sim.slave_infos()[0].index == 0
assert len(sim.slave_infos()) == 3
variables = sim.slave_variables(0)
assert variables[0].name.decode() == "time"
@@ -19,3 +23,14 @@ def test_system_structure():
assert variables[0].variability == CosimVariableVariability.CONTINUOUS.value
for v in variables:
print(v)
+
+
+if __name__ == "__main__":
+ retcode = pytest.main(
+ [
+ "-rA",
+ "-v",
+ __file__,
+ ]
+ )
+ assert retcode == 0, f"Non-zero return code {retcode}"
diff --git a/tests/test_assertion.py b/tests/test_assertion.py
new file mode 100644
index 0000000..8b3bf73
--- /dev/null
+++ b/tests/test_assertion.py
@@ -0,0 +1,70 @@
+from math import cos, sin
+
+import matplotlib.pyplot as plt
+import pytest
+from case_study.assertion import Assertion
+from sympy import symbols
+
+_t = [0.1 * float(x) for x in range(100)]
+_x = [0.3 * sin(t) for t in _t]
+_y = [1.0 * cos(t) for t in _t]
+
+
+def show_data():
+ fig, ax = plt.subplots()
+ ax.plot(_x, _y)
+ plt.title("Data (_x, _y)", loc="left")
+ plt.show()
+
+
+def test_init():
+ Assertion.reset()
+ t, x, y = symbols("t x y")
+ ass = Assertion("t>8")
+ assert ass.symbols["t"] == t
+ assert Assertion.ns == {"t": t}
+ ass = Assertion("(t>8) & (x>0.1)")
+ assert ass.symbols == {"t": t, "x": x}
+ assert Assertion.ns == {"t": t, "x": x}
+ ass = Assertion("(y<=4) & (y>=4)")
+ assert ass.symbols == {"y": y}
+ assert Assertion.ns == {"t": t, "x": x, "y": y}
+
+
+def test_assertion():
+ t, x, y = symbols("t x y")
+ # show_data()print("Analyze", analyze( "t>8 & x>0.1"))
+ Assertion.reset()
+ ass = Assertion("t>8")
+ assert ass.assert_single([("t", 9.0)])
+ assert not ass.assert_single([("t", 7)])
+ res = ass.assert_series([("t", _t)], "bool-list")
+ assert True in res, "There is at least one point where the assertion is True"
+ assert res.index(True) == 81, f"Element {res.index(True)} is True"
+ assert all(res[i] for i in range(81, 100)), "Assertion remains True"
+ assert ass.assert_series([("t", _t)], "bool"), "There is at least one point where the assertion is True"
+ assert ass.assert_series([("t", _t)], "interval") == (81, 100), "Index-interval where the assertion is True"
+ ass = Assertion("(t>8) & (x>0.1)")
+ res = ass.assert_series([("t", _t), ("x", _x)])
+ assert res, "True at some point"
+ assert ass.assert_series([("t", _t), ("x", _x)], "interval") == (81, 91)
+ assert ass.assert_series([("t", _t), ("x", _x)], "count") == 10
+ with pytest.raises(ValueError, match="Unknown return type 'Hello'"):
+ ass.assert_series([("t", _t), ("x", _x)], "Hello")
+ # Checking equivalence. '==' does not work
+ ass = Assertion("(y<=4) & (y>=4)")
+ assert ass.symbols == {"y": y}
+ assert Assertion.ns == {"t": t, "x": x, "y": y}
+ assert ass.assert_single([("y", 4)])
+ assert not ass.assert_series([("y", _y)], ret="bool")
+ with pytest.raises(ValueError, match="'==' cannot be used to check equivalence. Use 'a-b' and check against 0"):
+ ass = Assertion("y==4")
+ ass = Assertion("y-4")
+ assert 0 == ass.assert_single([("y", 4)])
+
+
+if __name__ == "__main__":
+ # retcode = pytest.main(["-rA","-v", __file__])
+ # assert retcode == 0, f"Non-zero return code {retcode}"
+ test_init()
+ test_assertion()
diff --git a/tests/test_bouncing_ball_3d.py b/tests/test_bouncing_ball_3d.py
new file mode 100644
index 0000000..d54a9ed
--- /dev/null
+++ b/tests/test_bouncing_ball_3d.py
@@ -0,0 +1,97 @@
+from math import sqrt
+from pathlib import Path
+from shutil import copy
+
+from component_model.model import Model
+from fmpy import plot_result, simulate_fmu
+
+
+def nearly_equal(res: tuple, expected: tuple, eps=1e-7):
+ assert len(res) == len(
+ expected
+ ), f"Tuples of different lengths cannot be equal. Found {len(res)} != {len(expected)}"
+ for i, (x, y) in enumerate(zip(res, expected, strict=False)):
+ assert abs(x - y) < eps, f"Element {i} not nearly equal in {x}, {y}"
+
+
+def test_make_fmu(): # chdir):
+ fmu_path = Model.build(
+ str(Path(__file__).parent / "data" / "BouncingBall3D" / "bouncing_ball_3d.py"), dest=Path(Path.cwd())
+ )
+ copy(fmu_path, Path(__file__).parent / "data" / "BouncingBall3D")
+
+
+def test_run_fmpy(show):
+ """Test and validate the basic BouncingBall using fmpy and not using OSP or case_study."""
+ path = Path("BouncingBall3D.fmu")
+ assert path.exists(), f"File {path} does not exist"
+ dt = 0.01
+ result = simulate_fmu(
+ path,
+ start_time=0.0,
+ stop_time=3.0,
+ step_size=dt,
+ validate=True,
+ solver="Euler",
+ debug_logging=False,
+ visible=True,
+ logger=print, # fmi_call_logger=print,
+ start_values={
+ "e": 0.71,
+ "g": 9.81,
+ },
+ )
+ if show:
+ plot_result(result)
+ t_bounce = sqrt(2 * 10 * 0.0254 / 9.81)
+ v_bounce = 9.81 * t_bounce # speed in z-direction
+ x_bounce = t_bounce / 1.0 # x-position where it bounces in m
+ # Note: default values are reported at time 0!
+ nearly_equal(result[0], (0, 0, 0, 10, 1, 0, 0, sqrt(2 * 10 / 9.81), 0, 0)) # time,pos-3, speed-3, p_bounce-3
+ print(result[1])
+ """
+ arrays_equal(
+ result(bb),
+ (
+ 0.01,
+ 0.01,
+ 0,
+ (10 * 0.0254 - 0.5 * 9.81 * 0.01**2) / 0.0254,
+ 1,
+ 0,
+ -9.81 * 0.01,
+ sqrt(2 * 10 * 0.0254 / 9.81),
+ 0,
+ 0,
+ ),
+ )
+ """
+ t_before = int(sqrt(2 / 9.81) / dt) * dt # just before bounce
+ print("BEFORE", t_before, result[int(t_before / dt)])
+ nearly_equal(
+ result[int(t_before / dt)],
+ (t_before, 1 * t_before, 0, 1.0 - 0.5 * 9.81 * t_before * t_before, 1, 0, -9.81 * t_before, x_bounce, 0, 0),
+ eps=0.003,
+ )
+ nearly_equal(
+ result[int(t_before / dt) + 1],
+ (
+ t_before + dt,
+ v_bounce * 0.71 * (t_before + dt - t_bounce) - 0.5 * 9.81 * (t_before + dt - t_bounce) ** 2,
+ v_bounce * 0.71 - 9.81 * (t_before + dt - t_bounce),
+ ),
+ eps=0.03,
+ )
+ nearly_equal(result[int(2.5 / dt)], (2.5, 0, 0), eps=0.4)
+ nearly_equal(result[int(3 / dt)], (3, 0, 0))
+ print("RESULT", result[int(t_before / dt) + 1])
+
+
+if __name__ == "__main__":
+ # retcode = pytest.main(["-rA", "-v", __file__, "--show", "True"])
+ # assert retcode == 0, f"Non-zero return code {retcode}"
+ import os
+
+ os.chdir(Path(__file__).parent.absolute() / "test_working_directory")
+ test_make_fmu()
+ test_run_fmpy(show=True)
diff --git a/tests/test_case.py b/tests/test_case.py
index 80c77c7..02ab238 100644
--- a/tests/test_case.py
+++ b/tests/test_case.py
@@ -155,6 +155,12 @@ def test_case_set_get(simpletable):
for act in caseX.act_set[0.0]:
print(str_act(act))
assert caseX.special["stopTime"] == 10, f"Erroneous stopTime {caseX.special['stopTime']}"
+ # print(f"ACT_SET: {caseX.act_set[0.0][0]}") #! set_initial, therefore no tuples!
+ assert caseX.act_set[0.0][0].func.__name__ == "set_initial", "function name"
+ assert caseX.act_set[0.0][0].args[0] == 0, "model instance"
+ assert caseX.act_set[0.0][0].args[1] == 3, f"variable type {caseX.act_set[0.0][0].args[1]}"
+ assert caseX.act_set[0.0][0].args[2] == 3, f"variable ref {caseX.act_set[0.0][0].args[2]}"
+ assert caseX.act_set[0.0][0].args[3], f"variable value {caseX.act_set[0.0][0].args[3]}"
# print(caseX.act_set[0.0][0])
assert caseX.act_set[0.0][0].args[0] == 0, "model instance"
assert caseX.act_set[0.0][0].args[1] == 3, f"variable type {caseX.act_set[0.0][0].args[1]}"
diff --git a/tests/test_cases.py b/tests/test_cases.py
index 68a4c80..a2fe25f 100644
--- a/tests/test_cases.py
+++ b/tests/test_cases.py
@@ -55,6 +55,13 @@ def test_cases():
for c in cases.base.list_cases(as_name=False, flat=True):
assert cases.case_by_name(c.name).name == c.name, f"Case {c.name} not found in hierarchy"
assert cases.case_by_name("case99") is None, "Case99 was not expected to be found"
+ c_gravity = cases.case_by_name("gravity")
+ assert c_gravity is not None and c_gravity.name == "gravity", "'gravity' is expected to exist"
+ msg = "'restitution' should not exist within the sub-hierarchy of 'gravity'"
+ assert c_gravity is not None and c_gravity.case_by_name("restitution") is None, msg
+ c_r = cases.case_by_name("restitution")
+ msg = "'restitutionAndGravity' should exist within the sub-hierarchy of 'restitution'"
+ assert c_r is not None and c_r.case_by_name("restitutionAndGravity") is not None, msg
gravity_case = cases.case_by_name("gravity")
assert gravity_case is not None and gravity_case.name == "gravity", "'gravity' is expected to exist"
msg = "'case2' should not exist within the sub-hierarchy of 'gravity'"
diff --git a/tests/test_run_bouncingball0.py b/tests/test_run_bouncingball0.py
index d668a55..50d5928 100644
--- a/tests/test_run_bouncingball0.py
+++ b/tests/test_run_bouncingball0.py
@@ -2,6 +2,7 @@
from pathlib import Path
import numpy as np
+import pytest
from case_study.case import Case, Cases
from case_study.simulator_interface import SimulatorInterface
@@ -166,7 +167,7 @@ def test_run_cases():
t0 = sqrt(2 * h0 / 9.81) # half-period time with full restitution
v_max = sqrt(2 * h0 * 9.81) # speed when hitting bottom
# h_v = lambda v, g: 0.5 * v**2 / g # calculate height
- assert abs(h0 - 1.0) < 2e-3
+ assert abs(h0 - 1.0) < 1e-2
assert expect_bounce_at(res, t0, eps=0.02), f"No bounce at {sqrt(2*h0/9.81)}"
assert expect_bounce_at(res, 2 * t0, eps=0.02), f"No top point at {2*sqrt(2*h0/9.81)}"
@@ -185,3 +186,8 @@ def test_run_cases():
assert expect_bounce_at(res, sqrt(2 * h0 / 1.5), eps=0.02), f"No bounce at {sqrt(2*h0/9.81)}"
assert expect_bounce_at(res, sqrt(2 * h0 / 1.5) + 0.5 * sqrt(2 * h0 / 1.5), eps=0.4)
cases.simulator.reset()
+
+
+if __name__ == "__main__":
+ retcode = pytest.main(["-rA", "-v", __file__])
+ assert retcode == 0, f"Non-zero return code {retcode}"