Skip to content

Commit

Permalink
resolved issues raised by mypy
Browse files Browse the repository at this point in the history
  • Loading branch information
ClaasRostock committed Nov 5, 2024
1 parent 7734cff commit 1199e23
Show file tree
Hide file tree
Showing 11 changed files with 185 additions and 104 deletions.
15 changes: 9 additions & 6 deletions src/sim_explorer/case.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@

import math
import os
from collections.abc import Callable
from datetime import datetime
from functools import partial
from pathlib import Path
from typing import Any, Callable
from typing import Any

import matplotlib.pyplot as plt
import numpy as np
from libcosimpy.CosimLogging import CosimLogLevel, log_output_level

from .json5 import Json5
from .simulator_interface import SimulatorInterface, from_xml
from sim_explorer.json5 import Json5
from sim_explorer.simulator_interface import SimulatorInterface, from_xml

"""
sim_explorer module for definition and execution of simulation experiments
Expand Down Expand Up @@ -368,9 +369,10 @@ def read_spec_item(self, key: str, value: Any | None = None):
at_time_arg * self.cases.timefac,
)

def list_cases(self, as_name=True, flat=False) -> list:
def list_cases(self, as_name: bool = True, flat: bool = False) -> list[str] | list[Case]:
"""List this case and all sub-cases recursively, as name or case objects."""
lst = [self.name if as_name else self]
lst: list[str] | list[Case]
lst = [self.name] if as_name else [self] # type: ignore[list-item]
for s in self.subs:
if flat:
lst.extend(s.list_cases(as_name, flat))
Expand Down Expand Up @@ -849,7 +851,7 @@ def _init_from_existing(self, file: str | Path):
cases = Cases(Path(case))
except ValueError:
raise CaseInitError(f"Cases {Path(case)} instantiation error") from ValueError
self.case = cases.case_by_name(self.res.jspath("$.header.case", str, True))
self.case: Case | None = cases.case_by_name(name=self.res.jspath(path="$.header.case", typ=str, errorMsg=True))
assert isinstance(self.case, Case), f"Case {self.res.jspath( '$.header.case', str, True)} not found"
assert isinstance(self.case.cases, Cases), "Cases object not defined"
self._header_transform(False)
Expand All @@ -872,6 +874,7 @@ def _header_make(self):
"""Make a standard header for the results of 'case' as dict.
This function is used as starting point when a new results file is created.
"""
assert self.case is not None, "Case object not defined"
_ = self.case.cases.js.jspath("$.header.name", str, True)
results = {
"header": {
Expand Down
15 changes: 9 additions & 6 deletions src/sim_explorer/cli/sim_explorer.py
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,11 @@ def main() -> None:
logger.error(f"sim-explorer.py: File {cases_file} not found.")
return
cases = Cases(args.cases)
print("ARGS", args)
logger.info(f"ARGS: {args}")

if not isinstance(cases, Cases):
print(f"Instantiation of {args.cases} not successfull")
logger.error(f"Instantiation of {args.cases} not successfull")
return

log_msg_stub: str = f"Start sim-explorer.py with following arguments:\n" f"\t cases_file: \t{cases_file}\n"

Expand All @@ -145,16 +146,18 @@ def main() -> None:

elif args.run is not None:
case = cases.case_by_name(args.run)
if not isinstance(case, Case):
print(f"Case {args.run} not found in {args.cases}")
if case is None:
logger.error(f"Case {args.run} not found in {args.cases}")
return
logger.info(f"{log_msg_stub}\t option: run \t\t\t{args.run}\n")
# Invoke API
case.run()

elif args.Run is not None:
case = cases.case_by_name(args.Run)
if not isinstance(case, Case):
print(f"Case {args.Run} not found in {args.cases}")
if case is None:
logger.error(f"Case {args.Run} not found in {args.cases}")
return
logger.info(f"{log_msg_stub}\t --Run \t\t\t{args.Run}\n")
# Invoke API
for _case in case.iter():
Expand Down
1 change: 1 addition & 0 deletions src/sim_explorer/json5.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ def _key(self) -> str:

def _value(self):
"""Read and return a value at the current position, i.e. expect ,'...', "...",}."""
v: str | dict[str, Any] | list[Any]
q1, q2 = self._quoted()
if q2 < 0: # no quotation found. Include also [ and { in search
m = re.search(r"[\[,\{\}\]]", self.js5[self.pos :])
Expand Down
9 changes: 4 additions & 5 deletions src/sim_explorer/simulator_interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import xml.etree.ElementTree as ET # noqa: N817
from enum import Enum
from pathlib import Path
from typing import TypeAlias
from typing import TypeAlias, cast
from zipfile import BadZipFile, ZipFile, is_zipfile

from libcosimpy.CosimEnums import CosimVariableCausality, CosimVariableType, CosimVariableVariability # type: ignore
Expand Down Expand Up @@ -70,8 +70,6 @@ class SimulatorInterface:
but it can be set to TRACE, DEBUG, INFO, WARNING, ERROR or FATAL (e.g. for debugging purposes)
"""

simulator: CosimExecution

def __init__(
self,
system: Path | str = "",
Expand All @@ -83,12 +81,13 @@ def __init__(
self.name = name # overwrite if the system includes that
self.description = description # overwrite if the system includes that
self.sysconfig: Path | None = None
self.simulator: CosimExecution
if simulator is None: # instantiate the simulator through the system config file
self.sysconfig = Path(system)
assert self.sysconfig.exists(), f"File {self.sysconfig.name} not found"
ck, msg = self._check_system_structure(self.sysconfig)
assert ck, msg
self.simulator = self._simulator_from_config(self.sysconfig)
self.simulator = cast(CosimExecution, self._simulator_from_config(self.sysconfig))
else:
self.simulator = simulator
log_output_level(log_level)
Expand Down Expand Up @@ -413,7 +412,7 @@ def set_variable_value(self, instance: int, typ: int, var_refs: tuple[int], var_
else:
raise CaseUseError(f"Unknown type {typ}") from None

def get_variable_value(self, instance: int, typ: int, var_refs: tuple[int]):
def get_variable_value(self, instance: int, typ: int, var_refs: tuple[int, ...]):
"""Provide an observer function which gets the 'variable' value (of the given 'instance' model) at the time when called.
Args:
Expand Down
107 changes: 79 additions & 28 deletions tests/test_bouncing_ball_3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,19 +53,19 @@ def check_case(
hf: float = 0.0254, # transformation m->inch
):
"""Run case 'name' and check results with respect to key issues."""
case = cases.case_by_name(casename)
case = cases.case_by_name(name=casename)
assert isinstance(case, Case), f"Case {case} does not seem to be a proper case object"
dt = case.special["stepSize"]
tfac = int(1 / dt)
print(f"Run case {case.name}. g={g}, e={e}, x_z={x_z}, dt={dt}")
case.run() # run the case and return results as results object
results = case.res # the results object
assert results.res.jspath("$.header.case", str, True) == case.name
assert results.res.jspath(path="$.header.case", typ=str, errorMsg=True) == case.name
# default initial settings, superceded by base case values
x = [0, 0, x_z] # z-value is in inch =! 1m!
v = [1.0, 0, 0]
# adjust to case settings:
for k, val in case.js.jspath("$.spec").items():
for k, val in case.js.jspath(path="$.spec").items():
if k in ("stepSize", "stopTime"):
pass
elif k == "g":
Expand All @@ -84,31 +84,46 @@ def check_case(
v_bounce = g * t_bounce # speed in z-direction
x_bounce = v[0] * t_bounce # x-position where it bounces
# check outputs after first step:
assert results.res.jspath("$['0'].bb.e") == e, "??Initial value of e"
assert results.res.jspath("$['0'].bb.g") == g, "??Initial value of g"
assert results.res.jspath("$['0'].bb.['x[2]']") == x[2], "??Initial value of x[2]"
arrays_equal(results.res.jspath("$['0.01'].bb.x"), [dt, 0, x[2] - 0.5 * g * dt**2 / hf])
arrays_equal(results.res.jspath("$['0.01'].bb.v"), [v[0], 0, -g * dt])
x_b = results.res.jspath("$.['0.01'].bb.['x_b[0]']")
assert results.res.jspath(path="$['0'].bb.e") == e, "??Initial value of e"
assert results.res.jspath(path="$['0'].bb.g") == g, "??Initial value of g"
assert results.res.jspath(path="$['0'].bb.['x[2]']") == x[2], "??Initial value of x[2]"
arrays_equal(
res=results.res.jspath(path="$['0.01'].bb.x"),
expected=(dt, 0, x[2] - 0.5 * g * dt**2 / hf),
)
arrays_equal(
res=results.res.jspath(path="$['0.01'].bb.v"),
expected=(v[0], 0, -g * dt),
)
x_b = results.res.jspath(path="$.['0.01'].bb.['x_b[0]']")
assert abs(x_b - x_bounce) < 1e-9
# just before bounce
t_before = int(t_bounce * tfac) / tfac # * dt # just before bounce
if t_before == t_bounce: # at the interval border
t_before -= dt
x_b = results.res.jspath(f"$['{t_before}'].bb.x")
arrays_equal(results.res.jspath(f"$['{t_before}'].bb.x"), [v[0] * t_before, 0, x[2] - 0.5 * g * t_before**2 / hf])
arrays_equal(results.res.jspath(f"$['{t_before}'].bb.v"), [v[0], 0, -g * t_before])
x_b = results.res.jspath(path=f"$['{t_before}'].bb.x")
arrays_equal(
res=results.res.jspath(path=f"$['{t_before}'].bb.x"),
expected=(v[0] * t_before, 0, x[2] - 0.5 * g * t_before**2 / hf),
)
arrays_equal(
res=results.res.jspath(path=f"$['{t_before}'].bb.v"),
expected=(v[0], 0, -g * t_before),
)
assert abs(results.res.jspath(f"$['{t_before}'].bb.['x_b[0]']") - x_bounce) < 1e-9
# just after bounce
ddt = t_before + dt - t_bounce # time from bounce to end of step
x_bounce2 = x_bounce + 2 * v_bounce * e * 1.0 * e / g
arrays_equal(
results.res.jspath(f"$['{t_before+dt}'].bb.x"),
[t_bounce * v[0] + v[0] * e * ddt, 0, (v_bounce * e * ddt - 0.5 * g * ddt**2) / hf],
res=results.res.jspath(path=f"$['{t_before+dt}'].bb.x"),
expected=(t_bounce * v[0] + v[0] * e * ddt, 0, (v_bounce * e * ddt - 0.5 * g * ddt**2) / hf),
)

arrays_equal(results.res.jspath(f"$['{t_before+dt}'].bb.v"), [e * v[0], 0, (v_bounce * e - g * ddt)])
assert abs(results.res.jspath(f"$['{t_before+dt}'].bb.['x_b[0]']") - x_bounce2) < 1e-9
arrays_equal(
res=results.res.jspath(path=f"$['{t_before+dt}'].bb.v"),
expected=(e * v[0], 0, (v_bounce * e - g * ddt)),
)
assert abs(results.res.jspath(path=f"$['{t_before+dt}'].bb.['x_b[0]']") - x_bounce2) < 1e-9
# from bounce to bounce
v_x, v_z, t_b, x_b = v[0], v_bounce, t_bounce, x_bounce # set start values (first bounce)
# print(f"1.bounce time: {t_bounce} v_x:{v_x}, v_z:{v_z}, t_b:{t_b}, x_b:{x_b}")
Expand All @@ -120,14 +135,14 @@ def check_case(
t_b += delta_t
x_b += v_x * delta_t
_tb = int(t_b * tfac) / tfac
if results.res.jspath(f"$['{_tb+dt}']") is None:
if results.res.jspath(path=f"$['{_tb+dt}']") is None:
break
_z = results.res.jspath(f"$['{_tb}'].bb.x")[2]
# z_ = results.res.jspath(f"$['{_tb+dt}'].bb.x")[2]
_vz = results.res.jspath(f"$['{_tb}'].bb.v")[2]
vz_ = results.res.jspath(f"$['{_tb+dt}'].bb.v")[2]
_vx = results.res.jspath(f"$['{_tb}'].bb.v")[0]
vx_ = results.res.jspath(f"$['{_tb+dt}'].bb.v")[0]
_z = results.res.jspath(path=f"$['{_tb}'].bb.x")[2]
# z_ = results.res.jspath(path=f"$['{_tb+dt}'].bb.x")[2]
_vz = results.res.jspath(path=f"$['{_tb}'].bb.v")[2]
vz_ = results.res.jspath(path=f"$['{_tb+dt}'].bb.v")[2]
_vx = results.res.jspath(path=f"$['{_tb}'].bb.v")[0]
vx_ = results.res.jspath(path=f"$['{_tb+dt}'].bb.v")[0]
assert abs(_z) < x[2] * 5e-2, f"Bounce {n}@{t_b}. z-position {_z} should be close to 0 ({x[2]*5e-2})"
if delta_t > 2 * dt:
assert _vz < 0 and vz_ > 0, f"Bounce {n}@{t_b}. Expected speed sign change {_vz}-{vz_}when bouncing"
Expand All @@ -137,11 +152,47 @@ def check_case(
def test_run_cases():
path = Path(Path(__file__).parent, "data/BouncingBall3D/BouncingBall3D.cases")
assert path.exists(), "BouncingBall3D cases file not found"
cases = Cases(path)
check_case(cases, "base", stepSize=0.01, stopTime=3, g=9.81, e=1.0, x_z=1 / 0.0254, hf=0.0254)
check_case(cases, "restitution", stepSize=0.01, stopTime=3, g=9.81, e=0.5, x_z=1 / 0.0254, hf=0.0254)
check_case(cases, "gravity", stepSize=0.01, stopTime=3, g=1.5, e=1.0, x_z=1 / 0.0254, hf=0.0254)
check_case(cases, "restitutionAndGravity", stepSize=0.01, stopTime=3, g=1.5, e=0.5, x_z=1 / 0.0254, hf=0.0254)
cases = Cases(spec=path)
check_case(
cases=cases,
casename="base",
stepSize=0.01,
stopTime=3,
g=9.81,
e=1.0,
x_z=1 / 0.0254,
hf=0.0254,
)
check_case(
cases=cases,
casename="restitution",
stepSize=0.01,
stopTime=3,
g=9.81,
e=0.5,
x_z=1 / 0.0254,
hf=0.0254,
)
check_case(
cases=cases,
casename="gravity",
stepSize=0.01,
stopTime=3,
g=1.5,
e=1.0,
x_z=1 / 0.0254,
hf=0.0254,
)
check_case(
cases=cases,
casename="restitutionAndGravity",
stepSize=0.01,
stopTime=3,
g=1.5,
e=0.5,
x_z=1 / 0.0254,
hf=0.0254,
)


if __name__ == "__main__":
Expand Down
8 changes: 6 additions & 2 deletions tests/test_cases.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

import pytest

from sim_explorer.case import Cases
from sim_explorer.case import Case, Cases
from sim_explorer.simulator_interface import SimulatorInterface

# def test_tuple_iter():
Expand Down Expand Up @@ -37,6 +37,7 @@ def test_cases():
sim = SimulatorInterface(str(Path(__file__).parent / "data" / "BouncingBall0" / "OspSystemStructure.xml"))
cases = Cases(Path(__file__).parent / "data" / "BouncingBall0" / "BouncingBall.cases", sim)

c: str | Case
print(cases.info())
# cases.spec
assert cases.js.jspath("$.header.name", str, True) == "BouncingBall", "BouncingBall expected as cases name"
Expand All @@ -50,7 +51,10 @@ def test_cases():
assert cases.js.jspath("$.header.variables.g[2]") == "Gravity acting on the ball"
# find_by_name
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 isinstance(c, Case)
case_by_name = cases.case_by_name(c.name)
assert case_by_name is not None
assert case_by_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"
Expand Down
1 change: 1 addition & 0 deletions tests/test_json5.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ def test_update(ex):


def test_json5_syntax():
js: Json5 | str
assert Json5("Hello World", False).js5 == "{ Hello World }", "Automatic addition of '{}' did not work"
js = Json5("Hello\nWorld\rHei\n\rHo\r\nHi", auto=False)
assert js.lines[6] == 24, f"Line 6 should start at {js.lines[6]}"
Expand Down
4 changes: 2 additions & 2 deletions tests/test_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ def test_add():
case = cases.case_by_name("base")
res = Results(case=case)
res._header_transform(tostring=True)
res.add(0.0, 0, 0, (6,), (9.81,))
res.add(time=0.0, comp=0, typ=0, refs=[6], values=(9.81,))
# print( res.res.write( pretty_print=True))
assert res.res.jspath("$['0.0'].bb.g") == 9.81

Expand All @@ -39,7 +39,7 @@ def test_plot_time_series(show):
assert file.exists(), f"File {file} not found"
res = Results(file=file)
if show:
res.plot_time_series(("bb.x[2]", "bb.v[2]"), "Test plot")
res.plot_time_series(variables=["bb.x[2]", "bb.v[2]"], title="Test plot")


def test_inspect():
Expand Down
Loading

0 comments on commit 1199e23

Please sign in to comment.