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}"