diff --git a/case_study/assertion.py b/case_study/assertion.py index bf4a072..56601f7 100644 --- a/case_study/assertion.py +++ b/case_study/assertion.py @@ -1,4 +1,5 @@ -from sympy import Symbol, sympify, FiniteSet +from sympy import Symbol, sympify + class Assertion: """Define Assertion objects for checking expectations with respect to simulation results. @@ -15,59 +16,59 @@ class Assertion: expr (str): The boolean expression definition as string. Any unknown symbol within the expression is defined as sympy.Symbol and is expected to match a variable. """ - ns = {} - - def __init__(self, expr:str): - self._expr = Assertion.do_sympify( expr) + + ns: dict = {} + + def __init__(self, expr: str): + self._expr = Assertion.do_sympify(expr) self._symbols = self.get_symbols() - Assertion.update_namespace( self._symbols) + Assertion.update_namespace(self._symbols) @property def expr(self): return self._expr - + @property def symbols(self): return self._symbols - - def symbol(self, name:str): + + def symbol(self, name: str): try: return self._symbols[name] - except KeyError as err: + except KeyError: return None @staticmethod - def do_sympify( _expr): + def do_sympify(_expr): """Evaluate the initial expression as sympy expression. Return the sympified expression or throw an error if sympification is not possible. """ - if '==' in _expr: + if "==" in _expr: raise ValueError("'==' cannot be used to check equivalence. Use 'a-b' and check against 0") from None try: - expr = sympify( _expr) + expr = sympify(_expr) except ValueError as err: raise Exception(f"Something wrong with expression {_expr}: {err}|. Cannot sympify.") from None return expr - + def get_symbols(self): - """Get the atom symbols used in the expression. Return the symbols as dict of `name : symbol`""" + """Get the atom symbols used in the expression. Return the symbols as dict of `name : symbol`.""" syms = self._expr.atoms(Symbol) - return { s.name : s for s in syms} - + return {s.name: s for s in syms} + @staticmethod def reset(): - """Reset the global dictionary of symbols used by all Assertions""" + """Reset the global dictionary of symbols used by all Assertions.""" Assertion.ns = {} - + @staticmethod - def update_namespace( sym: dict): - """Ensure that the symbols of this expression are registered in the global namespace `ns`""" - for n,s in sym.items(): + def update_namespace(sym: dict): + """Ensure that the symbols of this expression are registered in the global namespace `ns`.""" + for n, s in sym.items(): if n not in Assertion.ns: - Assertion.ns.update({n:s}) - + Assertion.ns.update({n: s}) - def assert_single(self, subs:list[tuple]): + def assert_single(self, subs: list[tuple]): """Perform assertion on a single data point. Args: @@ -78,11 +79,10 @@ def assert_single(self, subs:list[tuple]): Results: (bool) result of assertion """ - _subs = [ (self._symbols[s[0]], s[1]) for s in subs] - return self._expr.subs( _subs) - + _subs = [(self._symbols[s[0]], s[1]) for s in subs] + return self._expr.subs(_subs) - def assert_series(self, subs:list[tuple], ret:str='bool'): + def assert_series(self, subs: list[tuple], ret: str = "bool"): """Perform assertion on a (time) series. Args: @@ -91,7 +91,7 @@ def assert_series(self, subs:list[tuple], ret:str='bool'): All required variables for the evaluation shall be listed The variable-name provided as string is translated to its symbol before evaluation. ret (str)='bool': Determines how to return the result of the assertion: - + `bool` : True if any element of the assertion of the series is evaluated to True `bool-list` : List of True/False for each data point in the series `interval` : tuple of interval of indices for which the assertion is True @@ -100,32 +100,31 @@ def assert_series(self, subs:list[tuple], ret:str='bool'): bool, list[bool], tuple[int] or int, depending on `ret` parameter. Default: True/False on whether at least one record is found where the assertion is True. """ - _subs = [ (self._symbols[s[0]], s[1]) for s in subs] - length = len( subs[0][1]) - result = [False]* length - - for i in range( length): + _subs = [(self._symbols[s[0]], s[1]) for s in subs] + length = len(subs[0][1]) + result = [False] * length + + for i in range(length): s = [] - for k in range( len( _subs)): # number of variables in substitution - s.append( (_subs[k][0], _subs[k][1][i])) - res = self._expr.subs( s) + for k in range(len(_subs)): # number of variables in substitution + s.append((_subs[k][0], _subs[k][1][i])) + res = self._expr.subs(s) if res: result[i] = True - if ret == 'bool': + if ret == "bool": return True in result - elif ret == 'bool-list': + elif ret == "bool-list": return result - elif ret == 'interval': + elif ret == "interval": if True in result: idx0 = result.index(True) if False in result[idx0:]: - return (idx0, idx0+result[idx0:].index(False)) + return (idx0, idx0 + result[idx0:].index(False)) else: return (idx0, length) else: return None - elif ret == 'count': - return sum( x for x in result) + elif ret == "count": + return sum(x for x in result) else: raise ValueError(f"Unknown return type '{ret}'") from None - diff --git a/case_study/case.py b/case_study/case.py index a449cec..431fc1c 100644 --- a/case_study/case.py +++ b/case_study/case.py @@ -3,24 +3,17 @@ import math import os -import time +from datetime import datetime from functools import partial from pathlib import Path -from typing import Any, Callable, TypeAlias +from typing import Any, Callable import matplotlib.pyplot as plt import numpy as np -from .json5 import Json5Reader, json5_write +from .json5 import Json5 from .simulator_interface import SimulatorInterface, from_xml -# type definitions -PyVal: TypeAlias = str | float | int | bool # simple python types / Json5 atom -Json5: TypeAlias = dict[str, "Json5Val"] # Json5 object -Json5List: TypeAlias = list["Json5Val"] # Json5 list -Json5Val: TypeAlias = PyVal | Json5 | Json5List # Json5 values - - """ case_study module for definition and execution of simulation experiments * read and compile the case definitions from configuration file @@ -83,7 +76,7 @@ def __init__( name: str, description: str = "", parent: "Case" | None = None, - spec: dict | list | None = None, # Json5 | Json5List | None = None, + spec: dict | list | None = None, special: dict | None = None, ): self.cases = cases @@ -114,7 +107,6 @@ def __init__( self.special = dict(self.parent.special) self.act_get = Case._actions_copy(self.parent.act_get) self.act_set = Case._actions_copy(self.parent.act_set) - self.results: dict = {} # Json5 dict of results, added by cases.run_case() when run if self.name == "results": assert isinstance(self.spec, list), f"A list is expected as spec. Found {self.spec}" for k in self.spec: # only keys, no values @@ -134,15 +126,19 @@ def __init__( self.act_get = dict(sorted(self.act_get.items())) if self.name != "results": self.act_set = dict(sorted(self.act_set.items())) + # self.res represents the Results object and is added when collecting results or when evaluating results + + def add_results_object(self, res: Results): + self.res = res def iter(self): """Construct an iterator, allowing iteration from base case to this case through the hierarchy.""" h = [] nxt = self while True: # need first to collect the path to the base case - if nxt is None: - break h.append(nxt) + if nxt.parent is None: + break nxt = nxt.parent while len(h): yield h.pop() @@ -222,12 +218,12 @@ def _num_elements(obj) -> int: else: return 1 - def _disect_at_time(self, txt: str, value: PyVal | list[PyVal] | None = None) -> tuple[str, str, float]: + def _disect_at_time(self, txt: str, value: Any | None = None) -> tuple[str, str, float]: """Disect the @txt argument into 'at_time_type' and 'at_time_arg'. Args: txt (str): The key text after '@' and before ':' - value (PyVal, list(PyVal)): the value argument. Needed to distinguish the action type + value (Any): the value argument. Needed to distinguish the action type Returns ------- @@ -320,7 +316,7 @@ def _disect_range(self, key: str) -> tuple[str, dict, list | range]: rng = range(cvar_len) return (pre, cvar_info, rng) - def read_spec_item(self, key: str, value: PyVal | list[PyVal] | None = None): + def read_spec_item(self, key: str, value: Any | None = None): """Use the alias variable information (key) and the value to construct an action function, which is run when this variable is set/read. @@ -350,7 +346,7 @@ def read_spec_item(self, key: str, value: PyVal | list[PyVal] | None = None): Args: key (str): the key of the spec item - value (list[PyVal])=None: the values with respect to the item. For 'results' this is not used + value (Any])=None: the values with respect to the item. For 'results' this is not used """ if key in ("startTime", "stopTime", "stepSize"): self.special.update({key: value}) # just keep these as a dictionary so far @@ -392,11 +388,10 @@ def read_spec_item(self, key: str, value: PyVal | list[PyVal] | None = None): (_inst, cvar_info["type"], tuple(var_refs)), at_time_arg * self.cases.timefac, ) - else: # set actions assert value is not None, f"Variable {key}: Value needed for 'set' actions." assert at_time_type in ("set"), f"Unknown @time type {at_time_type} for case '{self.name}'" - if at_time_arg <= self.special["startTime"]: + if at_time_arg <= self.special["startTime"]: # False: #?? set_initial() does so far not work??# # SimulatorInterface.default_initial(cvar_info["causality"], cvar_info["variability"]) < 3: assert at_time_arg <= self.special["startTime"], f"Initial settings at time {at_time_arg}?" for inst in cvar_info["instances"]: # ask simulator to provide function to set variables: @@ -463,79 +458,23 @@ def get_from_config(element: str, default: float | None = None): raise CaseInitError("'stepSize' should be specified as part of the 'base' specification.") from None return special - def _results_make_header(self): - """Make a standard header for the results of 'case'. - The data is added in run_case(). - """ - assert isinstance(self.cases.spec, dict), f"Top level spec of cases: {type(self.cases.spec)}" - results = { - "Header": { - "case": self.name, - "dateTime": time.time(), - "cases": self.cases.spec.get("name", "None"), - "file": str(self.cases.file), - "casesDate": os.path.getmtime(self.cases.file), - "timeUnit": self.cases.spec.get("timeUnit", "None"), - "timeFactor": self.cases.timefac, - } - } - return results - - def _results_add(self, results: dict, time: float, comp: int, typ: int, refs: tuple[int, ...], values: tuple): - """Add the results of a get action to the results dict for the case. + def run(self, dump: str | None = ""): + """Set up case and run it. Args: - results (dict): The results dict (js5-dict) collected so far and where a new item is added. - time (float): the time of the results - component (int): The index of the component - typ (int): The data type of the variable as enumeration int - ref (list): The variable references linked to this variable definition - values (tuple): the values of the variable - print_type (str)='plain': 'plain': use indices as supplied - """ - if self.cases.results_print_type == "plain": - ref = refs[0] if len(refs) == 1 else str(refs) # comply to js5 key rules - elif self.cases.results_print_type == "names": - comp, ref = self.cases.comp_refs_to_case_var(comp, tuple(refs)) - if time in results: - if comp in results[time]: - results[time][comp].update({ref: values}) - else: - results[time].update({comp: {ref: values}}) - else: - results.update({time: {comp: {ref: values}}}) - - def _results_save(self, results: dict, jsfile: bool | str): - """Dump the results dict to a json5 file. - - Args: - results (dict): results dict as produced by Cases.run_case() - jsfile (bool,str): json file name to use for dump. If True: automatic file name generation. - """ - if not jsfile: - return - if not isinstance(jsfile, str): - jsfile = self.name + ".js5" - elif not jsfile.endswith(".js5"): - jsfile += ".js5" - json5_write(results, Path(self.cases.file.parent, jsfile)) - - def run(self, dump: bool | str = False): - """Set up case 'name' and run it. - - Args: - name (str,Case): case name as str or case object. The case to be run dump (str): Optionally save the results as json file. - False: only as string, True: json file with automatic file name, str: explicit filename.json + None: do not save, '': use default file name, str (with or without '.js5'): save with that file name """ - def do_actions(_t: float, _a, _iter, time: int, results: dict | None = None): + def do_actions(_t: float, _a, _iter, time: int, record: bool = True): while time >= _t: # issue the _a - actions if len(_a): - for a in _a: - res = a() - if results is not None: # get action. Store result - self._results_add(results, time / self.cases.timefac, a.args[0], a.args[1], a.args[2], res) + if record: + for a in _a: + self.res.add(time / self.cases.timefac, a.args[0], a.args[1], a.args[2], a()) + else: # do not record + for a in _a: + a() try: _t, _a = next(_iter) except StopIteration: @@ -555,6 +494,8 @@ def do_actions(_t: float, _a, _iter, time: int, results: dict | None = None): t_set, a_set = (float("inf"), []) # satisfy linter get_iter = self.act_get.items().__iter__() # iterator over get actions => time, action_list act_step = None + self.add_results_object(Results(self)) + while True: try: t_get, a_get = next(get_iter) @@ -564,61 +505,23 @@ def do_actions(_t: float, _a, _iter, time: int, results: dict | None = None): act_step = a_get else: break - results = self._results_make_header() - - # print(f"BEFORE LOOP. SET: {time}:{t_set}") - # for a in a_set: - # print(f" {a.func}, {a.args[2]}={a.args[3]}") - # print(f"BEFORE LOOP.GET: {time}:{t_get}") - # for a in a_get: - # print(f" {a.args[2]}={a()}") - # print(f"BEFORE LOOP.STEP: ") - # for a in act_step: - # print(f" {a}") #{a.args[2]}={a()}") - while True: - t_set, a_set = do_actions(t_set, a_set, set_iter, time) - if time <= tstart: # issue the current get actions (initial values) - self.cases.simulator.simulator.simulate_until(1) # one nano time step (ensure initialization) - t_get, a_get = do_actions(t_get, a_get, get_iter, time, results) + while True: + t_set, a_set = do_actions(t_set, a_set, set_iter, time, record=False) time += tstep if time > tstop: break self.cases.simulator.simulator.simulate_until(time) - t_get, a_get = do_actions(t_get, a_get, get_iter, time, results) # issue the current get actions + t_get, a_get = do_actions(t_get, a_get, get_iter, time) # issue the current get actions if act_step is not None: # there are step-always actions for a in act_step: - # print("STEP args", a.args) - self._results_add(results, time / self.cases.timefac, a.args[0], a.args[1], a.args[2], a()) + self.res.add(time / self.cases.timefac, a.args[0], a.args[1], a.args[2], a()) self.cases.simulator.reset() - if dump: - self._results_save(results, dump) - self.results = results - return results - - def plot_time_series(self, aliases: list[str], title=""): - """Use self.results to extract the provided alias variables and plot the data found in the same plot.""" - assert len(self.results), "The results dictionary is empty. Cannot plot results" - timefac = self.cases.timefac - for var in aliases: - times = [] - values = [] - for key in self.results: - if isinstance(key, int): # time value - if var in self.results[key]: - times.append(key / timefac) - values.append(self.results[key][var][2][0]) - plt.plot(times, values, label=var, linewidth=3) - - if len(title): - plt.title(title) - plt.xlabel("Time") - # plt.ylabel('Values') - plt.legend() - plt.show() + if dump is not None: + self.res.save(dump) @staticmethod def _actions_copy(actions: dict) -> dict: @@ -659,6 +562,7 @@ class Cases: __slots__ = ( "file", + "js", "spec", "simulator", "timefac", @@ -669,12 +573,11 @@ class Cases: "results_print_type", ) - def __init__( - self, spec: str | Path, simulator: SimulatorInterface | None = None, results_print_type: str = "names" - ): + def __init__(self, spec: str | Path, simulator: SimulatorInterface | None = None): self.file = Path(spec) # everything relative to the folder of this file! assert self.file.exists(), f"Cases spec file {spec} not found" - self.spec = Json5Reader(spec).js_py + self.js = Json5(spec) + self.spec = self.js.js_py if simulator is None: path = Path(self.file.parent, self.spec.get("modelFile", "OspSystemStructure.xml")) # type: ignore assert path.exists(), f"OSP system structure file {path} not found" @@ -693,11 +596,10 @@ def __init__( self.timefac = self._get_time_unit() * 1e9 # internally OSP uses pico-seconds as integer! # read the 'variables' section and generate dict { alias : { (instances), (variables)}}: self.variables = self.get_case_variables() - self.results_print_type = results_print_type self._comp_refs_to_case_var_cache: dict = ( dict() ) # cache of results indices translations used by comp_refs_to_case_var() - self.read_cases() # sets self.base and self.results + self.read_cases() def get_case_variables(self) -> dict[str, dict]: """Read the 'variables' main key, which defines self.variables (case variables) as a dictionary: @@ -767,6 +669,7 @@ def _get_time_unit(self) -> float: """Find system time unit from the spec and return as seconds. If the entry is not found, 1 second is assumed. """ + # _unit = assert isinstance(self.spec, dict), f"Dict expected as spec. Found {type(self.spec)}" unit = str(self.spec["timeUnit"]) if "timeUnit" in self.spec else "second" if unit.lower().startswith("sec"): @@ -807,7 +710,8 @@ def read_cases(self): "startTime": self.spec["base"]["spec"].get("startTime", 0.0), # type: ignore "stopTime": self.spec["base"]["spec"].get("stopTime", -1), # type: ignore } # type: ignore - assert special["stopTime"] > 0, "No stopTime defined in base case" # type: ignore # all case definitions are top-level objects in self.spec. 'base' and 'results' are mandatory + assert special["stopTime"] > 0, "No stopTime defined in base case" # type: ignore + # all case definitions are top-level objects in self.spec. 'base' and 'results' are mandatory self.results = Case( self, "results", @@ -900,7 +804,7 @@ def info(self, case: Case | None = None, level: int = 0) -> str: def comp_refs_to_case_var(self, comp: int, refs: tuple[int, ...]): """Get the translation of the component id `comp` + references `refs` to the variable names used in the cases file. - To speed up the process the process the cache dict _comp_refs_to_case_var_cache is used. + To speed up the process the cache dict _comp_refs_to_case_var_cache is used. """ try: component, var = self._comp_refs_to_case_var_cache[comp][refs] @@ -914,11 +818,160 @@ def comp_refs_to_case_var(self, comp: int, refs: tuple[int, ...]): self._comp_refs_to_case_var_cache[comp].update({refs: (component, var)}) return component, var - def run_case(self, name: str | Case, dump: bool | str = False): + def run_case(self, name: str | Case, dump: str | None = ""): """Initiate case run. If done from here, the case name can be chosen.""" if isinstance(name, Case): - return name.run(dump) + name.run(dump) else: c = self.case_by_name(name) - assert c is not None, f"Case {name} not found" - return c.run(dump) + assert isinstance(c, Case), f"Case {name} not found" + c.run(dump) + + +class Results: + """Manage the results of a case. + + * Collect results when a case is run + * Save case results as Json5 file + * Read results from file and work with them + + Args: + case (Case,str,Path)=None: The case object, the results relate to. + When instantiating from Case (for collecting data) this shall be explicitly provided. + When instantiating from stored results, this should refer to the cases definition, + or the default file name .cases is expected. + file (Path,str)=None: The file where results are saved (as Json5). + When instantiating from stored results (for working with data) this shall be explicitly provided. + When instantiating from Case, this file name will be used for storing results. + If "" default file name is used, if None, results are not stored. + """ + + def __init__(self, case: Case | str | Path | None = None, file: str | Path | None = None): + self.file: Path | None # None denotes that results are not automatically saved + if (case is None or isinstance(case, (str, Path))) and file is not None: + self._init_from_existing(file) # instantiating from existing results file (work with data) + elif isinstance(case, Case): # instantiating from cases file (for data collection) + self._init_new(case) + else: + raise ValueError(f"Inconsistent init arguments case:{case}, file:{file}") + + def _init_from_existing(self, file: str | Path): + self.file = Path(file) + assert self.file.exists(), f"File {file} is expected to exist." + self.res = Json5(self.file) + case = Path(self.file.parent / (self.res.jspath("$.header.cases", str, True) + ".cases")) + try: + 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)) + 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) + self.case.add_results_object(self) # make Results object known to self.case + + def _init_new(self, case: Case, file: str | Path | None = ""): + assert isinstance(case, Case), f"Case object expected as 'case' in Results. Found {type(case)}" + self.case = case + if file is not None: # use that for storing results data as Json5 + if file == "": # use default file name (can be changed through self.save(): + self.file = self.case.cases.file.parent / (self.case.name + ".js5") + else: + self.file = Path(file) + else: # do not store data + self.file = None + self.res = Json5(str(self._header_make())) # instantiate the results object + self._header_transform(tostring=False) + + 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 isinstance(self.case.cases.spec.get("name"), str), f"Spec of cases: {self.case.cases.spec.get('name')}" + results = { + "header": { + "case": self.case.name, + "dateTime": datetime.today().isoformat(), + "cases": self.case.cases.spec.get("name", "None"), + "file": Path(self.case.cases.file).as_posix(), + "casesDate": datetime.fromtimestamp(os.path.getmtime(self.case.cases.file)).isoformat(), + "timeUnit": self.case.cases.spec.get("timeUnit", "None"), + "timeFactor": self.case.cases.timefac, + } + } + return results + + def _header_transform(self, tostring: bool = True): + """Transform the header back- and forth between python types and string. + tostring=True is used when saving to file and =False is used when reading from file. + """ + res = self.res + if tostring: + res.update("$.header.dateTime", res.jspath("$.header.dateTime", datetime, True).isoformat()) + res.update("$.header.casesDate", res.jspath("$.header.casesDate", datetime, True).isoformat()) + res.update("$.header.file", res.jspath("$.header.file", Path, True).as_posix()) + else: + res.update("$.header.dateTime", datetime.fromisoformat(res.jspath("$.header.dateTime", str, True))) + res.update("$.header.casesDate", datetime.fromisoformat(res.jspath("$.header.casesDate", str, True))) + res.update("$.header.file", Path(res.jspath("$.header.file", str, True))) + + def add(self, time: float, comp: int, typ: int, refs: int | list[int], values: tuple): + """Add the results of a get action to the results dict for the case. + + Args: + time (float): the time of the results + component (int): The index of the component + typ (int): The data type of the variable as enumeration int + ref (list): The variable reference(s) linked to this variable definition + values (tuple): the values of the variable + """ + if isinstance(refs, int): + refs = [refs] + values = (values,) + compname, varname = self.case.cases.comp_refs_to_case_var(comp, tuple(refs)) # type: ignore [union-attr] + # print(f"ADD@{time}: {compname}, {varname} = {values}") + if len(values) == 1: + self.res.update("$[" + str(time) + "]" + compname, {varname: values[0]}) + else: + self.res.update("$[" + str(time) + "]" + compname, {varname: values}) + + def save(self, jsfile: str | Path = ""): + """Dump the results dict to a json5 file. + + Args: + jsfile (str|Path): Optional possibility to change the default name (self.case.name.js5) to use for dump. + """ + if self.file is None: + return + if jsfile == "": + jsfile = self.file + else: # a new file name is provided + if isinstance(jsfile, str): + if not jsfile.endswith(".js5"): + jsfile += ".js5" + jsfile = Path(self.case.cases.file.parent / jsfile) # type: ignore [union-attr] + self.file = jsfile # remember the new file name + self._header_transform(tostring=True) + self.res.write(jsfile) + + def plot_time_series(self, aliases: list[str], title=""): + """Extract the provided alias variables and plot the data found in the same plot.""" + if not len(self.res.js_py) or self.case is None: + return + timefac = self.case.cases.timefac + for var in aliases: + times: list = [] + values: list = [] + for key in self.res.js_py: + if isinstance(key, int): # time value + if var in self.res.js_py[key]: + times.append(key / timefac) + values.append(self.res.js_py[key][var][2][0]) + plt.plot(times, values, label=var, linewidth=3) + + if len(title): + plt.title(title) + plt.xlabel("Time") + # plt.ylabel('Values') + plt.legend() + plt.show() diff --git a/case_study/json5.py b/case_study/json5.py index 06606c5..754d6a1 100644 --- a/case_study/json5.py +++ b/case_study/json5.py @@ -1,15 +1,13 @@ from __future__ import annotations +# from jsonpath_ng.ext.filter import Expression#, Filter import os import re from pathlib import Path -from typing import TypeAlias +from typing import Any -# type definitions -PyVal: TypeAlias = str | float | int | bool # simple python types / Json5 atom -Json5: TypeAlias = dict[str, "Json5Val"] # Json5 object -Json5List: TypeAlias = list["Json5Val"] # Json5 list -Json5Val: TypeAlias = PyVal | Json5 | Json5List # Json5 values +from jsonpath_ng.ext import parse # type: ignore +from jsonpath_ng.jsonpath import DatumInContext # type: ignore class Json5Error(Exception): @@ -18,10 +16,13 @@ class Json5Error(Exception): pass -class Json5Reader: - """Read json5 files and return as python dict (of dicst/lists). +class Json5: + """Work with json5 files (e.g. cases specification and results). - Note that Json5 is here restricted to unique keys (within an object) and the order of key:values is preserved. + * Read Json5 code from file or string, representing the result internally as Python code (dict of dicts,lists,values) + * Searching for elements using JsonPath expressions + * Some Json manipulation methods + * Write Json5 code to file Args: js5 (Path,str): Path to json5 file or json5 string @@ -48,13 +49,17 @@ def __init__( self.pos = 0 self.comments_eol = comments_eol self.comments_ml = comments_ml - if Path(js5).exists(): - with open(Path(js5), "r") as file: # read file into string - self.js5 = file.read() - elif isinstance(js5, str): - self.js5 = js5 - else: - raise Json5Error(f"File {Path(js5)} not found") + try: + if Path(js5).exists(): + with open(Path(js5), "r") as file: # read file into string + self.js5 = file.read() + except Exception: + pass + if not hasattr(self, "js5"): # file reading not succesfull + if isinstance(js5, str): + self.js5 = js5 + else: + raise Json5Error(f"Invalid Json5 input {self.js5}") from None if self.js5[0] != "{": self.js5 = "{\n" + self.js5 if self.js5[-1] != "}": @@ -249,7 +254,7 @@ def _re(txt: str): pos += s2.end() return _js5, comments - def to_py(self) -> Json5: + def to_py(self) -> dict[str, Any]: """Translate json5 code 'self.js5' to a python dict and store as self.js_py.""" self.pos = 0 return self._object() @@ -266,12 +271,12 @@ def _strip(self, txt: str) -> str: else: len0 = len(txt) - def _object(self) -> Json5: + def _object(self) -> dict[str, Any]: """Start reading a json5 object { ... } at current position.""" # print(f"OBJECT({self.pos}): {self.js5[self.pos:]}") assert self.js5[self.pos] == "{", self._msg("object start '{' expected") self.pos += 1 - dct = None # {}: dict[str,Json5] = {} + dct = None # {}: dict[str,Any] = {} while True: r0, c0 = self._get_line_number(self.pos) k = self._key() # read until ':' @@ -291,7 +296,7 @@ def _object(self) -> Json5: else: dct.update({k: v}) - def _list(self) -> Json5List: + def _list(self) -> list: """Read and return a list object at the current position.""" # print(f"LIST({self.pos}): {self.js5[self.pos:]}") assert self.js5[self.pos] == "[", self._msg("List start '[' expected") @@ -354,7 +359,7 @@ def _key(self) -> str: self.pos += m.end() return str(self._strip(k)) - def _value(self) -> PyVal | Json5List | Json5: + def _value(self): """Read and return a value at the current position, i.e. expect ,'...', "...",}.""" q1, q2 = self._quoted() if q2 < 0: # no quotation found. Include also [ and { in search @@ -384,13 +389,14 @@ def _value(self) -> PyVal | Json5List | Json5: # print(f"VALUE. Jump:{self.js5[save_pos:self.pos]}, return:{v}") if isinstance(v, str): v = v.strip().strip("'").strip('"').strip() + if q2 < 0: # no quotation was used. Key separator not allowed. + assert ":" not in v, self._msg(f"Key separator ':' in value: {v}. Forgot ','?") # print(f"VALUE {v} @ {self.pos}:'{self.js5[self.pos:self.pos+50]}'") if isinstance(v, (dict, list)): return v elif isinstance(v, str) and not len(v): # might be empty due to trailing ',' return "" - assert ":" not in v, self._msg(f"Key separator ':' in value: {v}. Forgot ','?") try: return int(v) # type: ignore except Exception: @@ -410,65 +416,159 @@ def _value(self) -> PyVal | Json5List | Json5: else: raise Json5Error(f"This should not happen. v:{v}") from None + def jspath(self, path: str, typ: type | None = None, errorMsg: bool = False): + """Evaluate a JsonPath expression on the Json5 code and return the result. + + Syntax see `RFC9535 `_ + and `jsonpath-ng (used here) `_ + + * $: root node identifier (Section 2.2) + * @: current node identifier (Section 2.3.5) (valid only within filter selectors) + * []: child segment (Section 2.5.1): selects zero or more children of a node + * .name: shorthand for ['name'] + * .*: shorthand for [*] + * ..⁠[]: descendant segment (Section 2.5.2): selects zero or more descendants of a node + * ..name: shorthand for ..['name'] + * ..*: shorthand for ..[*] + * 'name': name selector (Section 2.3.1): selects a named child of an object + * *: wildcard selector (Section 2.3.2): selects all children of a node + * i: (int) index selector (Section 2.3.3): selects an indexed child of an array (from 0) + * 0:100:5: array slice selector (Section 2.3.4): start:end:step for arrays + * ?: filter selector (Section 2.3.5): selects particular children using a logical expression + * length(@.foo): function extension (Section 2.4): invokes a function in a filter expression -def json5_write( - js5: dict[str, PyVal | Json5List | Json5], file: str | os.PathLike[str] | None = None, pretty_print: bool = True -): - """Write a Json(5) tree to string or file. + Args: + path (str): path expression as string. + """ + compiled = parse(path) + data = compiled.find(self.js_py) + # print("DATA", data) + val = None + if not len(data): # not found + if errorMsg: + raise KeyError(f"No match for {path}") from None + else: + return None + elif len(data) == 1: # found a single element + val = data[0].value + else: # multiple elements + if isinstance(data[0], DatumInContext): + val = [x.value for x in data] + + if typ is None or isinstance(val, typ): + return val + else: + if errorMsg: + raise ValueError(f"{path} matches, but type {typ} does not match {type(val)}.") + else: + return None - Args: - js5 (Json5): The Json(5) dict which shall be written to file or string - file (str, Path)=None: The file name (as string or Path object) or None. If None, a string is returned. - pretty_print (bool)=True: Denote whether the string/file should be pretty printed (LF,indents). + @staticmethod + def _spath_to_keys(spath): + """Extract the keys from path. + So far this is a minimum implementation for adding data. Probably this could be done using jysonpath-ng. + """ + keys = [] + spath = spath.lstrip("$.") + if spath.startswith("$["): + spath = spath[1:] + c = re.compile(r"\[(.*?)\]") + while True: + m = c.search(spath) + if m is not None: + if m.start() > 0: + keys.extend(spath[: m.start()].split(".")) + keys.append(spath[m.start() + 1 : m.end() - 1]) + spath = spath[m.end() :] + elif not len(spath.strip()): + break + else: + keys.extend(spath.split(".")) + break + return keys - Returns: The serialized Json(5) object as string. This string is optionally written to file. - """ + def update(self, spath: str, data: Any): + """Append data to the js_py dict at the path pointed to by keys. + So far this is a minimum implementation for adding data. Probably this could be done using jysonpath-ng. + """ - def remove_comma(txt: str) -> str: - for i in range(len(txt) - 1, -1, -1): - if txt[i] == ",": - return txt[:i] - return "" + keys = Json5._spath_to_keys(spath) + path = self.js_py + parent = path + for i, k in enumerate(keys): + if k not in path: + for j in range(len(keys) - 1, i - 1, -1): + data = {keys[j]: data} + break + else: + parent = path + path = path[k] # type: ignore [assignment] + # print(f"UPDATE path:{path}, parent:{parent}, k:{k}: {data}") + if isinstance(path, list): + path.append(data) + elif isinstance(path, dict): + path.update(data) + elif isinstance(parent, dict): # update the parent dict (replace a value) + parent.update({k: data}) + else: + raise ValueError(f"Unknown type of path: {path}") - def print_js5(sub: PyVal | Json5List | Json5, level: int = 0, pretty: bool = True) -> str: - """Print the Json5 object recursively. Return the formated string. + def write(self, file: str | os.PathLike[str] | None = None, pretty_print: bool = True): + """Write a Json(5) tree to string or file. Args: - sub (Json5): the Json5 object to print - level (int)=0: level in recursive printing. Used for indentation. - pretty (bool)=True: Pretty print (LF and indentation). + file (str, Path)=None: The file name (as string or Path object) or None. If None, a string is returned. + pretty_print (bool)=True: Denote whether the string/file should be pretty printed (LF,indents). + + Returns: The serialized Json(5) object as string. This string is optionally written to file. """ - if isinstance(sub, dict): - res = "{" - for k, v in sub.items(): # print the keys and values of dicts + + def remove_comma(txt: str) -> str: + for i in range(len(txt) - 1, -1, -1): + if txt[i] == ",": + return txt[:i] + return "" + + def print_js5(sub: Any, level: int = 0, pretty: bool = True) -> str: + """Print the Json5 object recursively. Return the formated string. + + Args: + sub (dict): the Json5 object to print + level (int)=0: level in recursive printing. Used for indentation. + pretty (bool)=True: Pretty print (LF and indentation). + """ + if isinstance(sub, dict): + res = "{" + for k, v in sub.items(): # print the keys and values of dicts + res += "\n" + " " * level if pretty else "" + res += " " * level if pretty else "" + res += str(k) + res += " : " if pretty else ":" + res += print_js5(v, level + 1, pretty) res += "\n" + " " * level if pretty else "" - res += " " * level if pretty else "" - res += str(k) - res += " : " if pretty else ":" - res += print_js5(v, level + 1, pretty) - res += "\n" + " " * level if pretty else "" - res = remove_comma(res) - res += "}," if level > 0 else "}" - res += "\n" if pretty else "" - return res - elif isinstance(sub, list): - res = "[" - for v in sub: - sub_res = print_js5(v, level=level, pretty=pretty) - res += "" if sub_res is None else sub_res - res = remove_comma(res) - res += "]," - res += "\n" if pretty else "" + res = remove_comma(res) + res += "}," if level > 0 else "}" + res += "\n" if pretty else "" + elif isinstance(sub, list): + res = "[" + for v in sub: + sub_res = print_js5(v, level=level, pretty=pretty) + res += "" if sub_res is None else sub_res + res = remove_comma(res) + res += "]," + res += "\n" if pretty else "" + elif sub == "": + res = "," + elif isinstance(sub, str): + res = "'" + str(sub) + "'," + elif isinstance(sub, (int, float, bool)): + res = str(sub) + "," + else: # try still to make a string + res = str(sub) + "," return res - elif sub == "": - return "," - elif isinstance(sub, str): - return "'" + str(sub) + "'," - elif isinstance(sub, (int, float, bool)): - return str(sub) + "," - - txt = print_js5(js5, level=0, pretty=pretty_print) - if file: - with open(file, "w") as fp: - fp.write(txt) - return txt + + js5 = print_js5(self.js_py, level=0, pretty=pretty_print) + if file: + with open(file, "w") as fp: + fp.write(js5) + return js5 diff --git a/case_study/simulator_interface.py b/case_study/simulator_interface.py index 568eb0c..8897e87 100644 --- a/case_study/simulator_interface.py +++ b/case_study/simulator_interface.py @@ -3,7 +3,7 @@ import xml.etree.ElementTree as ET # noqa: N817 from enum import Enum from pathlib import Path -from typing import Callable, TypeAlias +from typing import TypeAlias from zipfile import BadZipFile, ZipFile, is_zipfile from libcosimpy.CosimEnums import CosimVariableCausality, CosimVariableType, CosimVariableVariability # type: ignore @@ -82,6 +82,8 @@ def __init__( 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) else: self.simulator = simulator @@ -99,11 +101,21 @@ def __init__( def path(self): return self.sysconfig.resolve().parent if self.sysconfig is not None else None + def _check_system_structure(self, file: Path): + """Check the OspSystemStructure file. Used in cases where the simulatorInterface is instantiated from Cases.""" + el = from_xml(file) + assert isinstance(el, ET.Element), f"ElementTree element expected. Found {el}" + ns = el.tag.split("{")[1].split("}")[0] + msg = "" + for s in el.findall(".//{*}Simulator"): + if not Path(Path(file).parent / s.get("source", "??")).exists(): + msg += f"Component {s.get('name')}, source {s.get('source','??')} not found. NS:{ns}" + return (not len(msg), msg) + def reset(self): # , cases:Cases): """Reset the simulator interface, so that a new simulation can be run.""" assert isinstance(self.sysconfig, Path), "Simulator resetting does not work with explicitly supplied simulator." assert self.sysconfig.exists(), "Simulator resetting does not work with explicitly supplied simulator." - print("PATH", self.sysconfig.name, self.sysconfig.parent, self.sysconfig.is_dir()) assert isinstance(self.manipulator, CosimManipulator) assert isinstance(self.observer, CosimObserver) # self.simulator = self._simulator_from_config(self.sysconfig) @@ -153,9 +165,6 @@ def get_components(self, model: int = -1) -> dict: else: comp_infos = self.simulator.slave_infos() - print( - "COMP_INFOS", comp_infos, comp_infos[0].name, comp_infos[0].index, len(comp_infos) - ) # , comp_infos.name, comp_infos.index) for comp in comp_infos: for r, same in self.same_model(comp.index, set(comps.values())): if same: @@ -401,7 +410,7 @@ def set_initial(self, instance: int, typ: int, var_ref: int, var_val: PyVal): # msg = f"Initial setting of ref:{var_refs}, type {typ} to val:{var_vals} failed. Status: {res}" # assert all(x for x in res), msg - def set_variable_value(self, instance: int, typ: int, var_refs: tuple[int], var_vals: tuple[PyVal]) -> Callable: + def set_variable_value(self, instance: int, typ: int, var_refs: tuple[int], var_vals: tuple[PyVal]) -> bool: """Provide a manipulator function which sets the 'variable' (of the given 'instance' model) to 'value'. Args: @@ -421,7 +430,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]) -> Callable: + 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: @@ -596,19 +605,20 @@ def from_xml(file: Path, sub: str | None = None, xpath: str | None = None) -> ET if is_zipfile(file) and sub is not None: # expect a zipped archive containing xml file 'sub' with ZipFile(file) as zp: try: - xml = zp.read(sub) + xml = zp.read(sub).decode("utf-8") except BadZipFile as err: raise CaseInitError(f"File '{sub}' not found in {file}: {err}") from err - else: - return ET.fromstring(xml) elif not is_zipfile(file) and file.exists() and sub is None: # expect an xml file - try: - et = ET.parse(file).getroot() - except ET.ParseError as err: - raise CaseInitError(f"File '{file}' does not seem to be a proper xml file") from err + with open(file, encoding="utf-8") as f: + xml = f.read() else: raise CaseInitError(f"It was not possible to read an XML from file {file}, sub {sub}") + try: + et = ET.fromstring(xml) + except ET.ParseError as err: + raise CaseInitError(f"File '{file}' does not seem to be a proper xml file") from err + if xpath is None: return et else: diff --git a/docs/source/modules.rst b/docs/source/modules.rst index 04e9d97..e4e3607 100644 --- a/docs/source/modules.rst +++ b/docs/source/modules.rst @@ -5,14 +5,12 @@ This section documents the contents of the case_study package. json5 ----- -Python module for reading and writing json5 files. +Python module for working with json5 files. -.. autoclass:: case_study.json5.Json5Reader +.. autoclass:: case_study.json5.Json5 :members: :show-inheritance: -.. autofunction:: case_study.json5.json5_write - Simulator Interface ------------------- @@ -35,3 +33,6 @@ Python module to manage cases with respect to reading *.cases files, running cas :members: :show-inheritance: +.. autoclass:: case_study.case.Results + :members: + :show-inheritance: diff --git a/pyproject.toml b/pyproject.toml index 685ccdf..9801328 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,8 @@ build-backend = "setuptools.build_meta" "__init__.py" = ["I001"] "./tests/*" = ["D"] -[tool.pyright] + +[tool.mypy] exclude = [ ".git", ".venv", @@ -104,4 +105,4 @@ build-backend = "setuptools.build_meta" "**/__pycache__", "./doc/source/conf.py", "./tests/data", - ] + ] \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py index 47c2cf2..7e737d3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -52,10 +52,11 @@ def setup_logging(caplog: LogCaptureFixture): 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/BouncingBall3D/BouncingBall.cases b/tests/data/BouncingBall3D/BouncingBall3D.cases similarity index 85% rename from tests/data/BouncingBall3D/BouncingBall.cases rename to tests/data/BouncingBall3D/BouncingBall3D.cases index 584c74a..d9e082d 100644 --- a/tests/data/BouncingBall3D/BouncingBall.cases +++ b/tests/data/BouncingBall3D/BouncingBall3D.cases @@ -1,4 +1,4 @@ -{name : 'BouncingBall', +{name : 'BouncingBall3D', description : 'Simple Case Study with the 3D BouncingBall FMU (3D position and speed', modelFile : "OspSystemStructure.xml", timeUnit : "second", @@ -13,10 +13,10 @@ 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, + stopTime : 3, + g : 9.81, e : 1.0, - x[2] : 1.0, + x[2] : 39.37007874015748, # this is in inch => 1m! }}, restitution : { description : "Smaller coefficient of restitution e", @@ -27,18 +27,20 @@ restitutionAndGravity : { description : "Based restitution (e change), change also the gravity g", parent : 'restitution', spec : { - g : -1.5 + g : 1.5 }}, gravity : { description : "Gravity like on the moon", spec : { - g : -1.5 + g : 1.5 }}, results : { spec : [ e@0.0, g@0.0, + x@0.0, x@step, v@step, + x_b@step, ]} } diff --git a/tests/data/BouncingBall3D/BouncingBall3D.fmu b/tests/data/BouncingBall3D/BouncingBall3D.fmu index 002b7e7..f083741 100644 Binary files a/tests/data/BouncingBall3D/BouncingBall3D.fmu and b/tests/data/BouncingBall3D/BouncingBall3D.fmu differ diff --git a/tests/data/BouncingBall3D/OspSystemStructure.xml b/tests/data/BouncingBall3D/OspSystemStructure.xml index 3bd9210..1a6645a 100644 --- a/tests/data/BouncingBall3D/OspSystemStructure.xml +++ b/tests/data/BouncingBall3D/OspSystemStructure.xml @@ -3,7 +3,7 @@ version="0.1"> 0.01 - + diff --git a/tests/data/BouncingBall3D/bouncing_ball_3d.py b/tests/data/BouncingBall3D/bouncing_ball_3d.py deleted file mode 100644 index 035b168..0000000 --- a/tests/data/BouncingBall3D/bouncing_ball_3d.py +++ /dev/null @@ -1,156 +0,0 @@ -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/BouncingBall3D/test_case.js5 b/tests/data/BouncingBall3D/test_case.js5 new file mode 100644 index 0000000..b3a0d2f --- /dev/null +++ b/tests/data/BouncingBall3D/test_case.js5 @@ -0,0 +1,2411 @@ +{ +header : { + case : 'restitutionAndGravity', + dateTime : '2024-10-29T22:44:03.688511', + cases : 'BouncingBall3D', + file : 'C:/Users/eis/Documents/Projects/Simulation_Model_Assurance/case_study/tests/data/BouncingBall3D/BouncingBall3D.cases', + casesDate : '2024-10-29T17:09:44.161850', + timeUnit : 'second', + timeFactor : 1000000000.0}, + +0.01 : { + bb : { + e : 0.5, + g : 1.5, + x : [0.01,0.0,39.36712598425197], + + v : [1.0,0.0,-0.015], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.02 : { + bb : { + x : [0.02,0.0,39.35826771653543], + + v : [1.0,0.0,-0.03], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.03 : { + bb : { + x : [0.03,0.0,39.343503937007874], + + v : [1.0,0.0,-0.045], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.04 : { + bb : { + x : [0.04,0.0,39.32283464566929], + + v : [1.0,0.0,-0.06], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.05 : { + bb : { + x : [0.05,0.0,39.29625984251968], + + v : [1.0,0.0,-0.075], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.06 : { + bb : { + x : [0.06,0.0,39.26377952755906], + + v : [1.0,0.0,-0.09], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.07 : { + bb : { + x : [0.07,0.0,39.22539370078741], + + v : [1.0,0.0,-0.10500000000000001], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.08 : { + bb : { + x : [0.08,0.0,39.181102362204726], + + v : [1.0,0.0,-0.12], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.09 : { + bb : { + x : [0.09,0.0,39.13090551181102], + + v : [1.0,0.0,-0.13499999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.1 : { + bb : { + x : [0.1,0.0,39.074803149606296], + + v : [1.0,0.0,-0.15], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.11 : { + bb : { + x : [0.11,0.0,39.01279527559055], + + v : [1.0,0.0,-0.16499999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.12 : { + bb : { + x : [0.12,0.0,38.94488188976378], + + v : [1.0,0.0,-0.17999999999999997], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.13 : { + bb : { + x : [0.13,0.0,38.871062992125985], + + v : [1.0,0.0,-0.19499999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.14 : { + bb : { + x : [0.14,0.0,38.79133858267716], + + v : [1.0,0.0,-0.21], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.15 : { + bb : { + x : [0.15,0.0,38.705708661417326], + + v : [1.0,0.0,-0.22499999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.16 : { + bb : { + x : [0.16,0.0,38.61417322834645], + + v : [1.0,0.0,-0.24], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.17 : { + bb : { + x : [0.17,0.0,38.51673228346456], + + v : [1.0,0.0,-0.255], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.18 : { + bb : { + x : [0.18,0.0,38.41338582677165], + + v : [1.0,0.0,-0.26999999999999996], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.19 : { + bb : { + x : [0.19,0.0,38.30413385826772], + + v : [1.0,0.0,-0.285], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.2 : { + bb : { + x : [0.2,0.0,38.188976377952756], + + v : [1.0,0.0,-0.3], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.21 : { + bb : { + x : [0.21,0.0,38.06791338582678], + + v : [1.0,0.0,-0.31499999999999995], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.22 : { + bb : { + x : [0.22,0.0,37.94094488188977], + + v : [1.0,0.0,-0.32999999999999996], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.23 : { + bb : { + x : [0.23,0.0,37.80807086614173], + + v : [1.0,0.0,-0.345], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.24 : { + bb : { + x : [0.24,0.0,37.66929133858268], + + v : [1.0,0.0,-0.35999999999999993], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.25 : { + bb : { + x : [0.25,0.0,37.5246062992126], + + v : [1.0,0.0,-0.37499999999999994], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.26 : { + bb : { + x : [0.26,0.0,37.3740157480315], + + v : [1.0,0.0,-0.38999999999999996], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.27 : { + bb : { + x : [0.27,0.0,37.21751968503938], + + v : [1.0,0.0,-0.40499999999999997], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.28 : { + bb : { + x : [0.28,0.0,37.05511811023622], + + v : [1.0,0.0,-0.42], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.29 : { + bb : { + x : [0.29,0.0,36.88681102362205], + + v : [1.0,0.0,-0.43499999999999994], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.3 : { + bb : { + x : [0.3,0.0,36.71259842519685], + + v : [1.0,0.0,-0.44999999999999996], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.31 : { + bb : { + x : [0.31,0.0,36.53248031496063], + + v : [1.0,0.0,-0.46499999999999997], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.32 : { + bb : { + x : [0.32,0.0,36.34645669291339], + + v : [1.0,0.0,-0.48], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.33 : { + bb : { + x : [0.33,0.0,36.154527559055126], + + v : [1.0,0.0,-0.495], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.34 : { + bb : { + x : [0.34,0.0,35.95669291338583], + + v : [1.0,0.0,-0.51], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.35 : { + bb : { + x : [0.35,0.0,35.75295275590552], + + v : [1.0,0.0,-0.5249999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.36 : { + bb : { + x : [0.36,0.0,35.54330708661418], + + v : [1.0,0.0,-0.5399999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.37 : { + bb : { + x : [0.37,0.0,35.327755905511815], + + v : [1.0,0.0,-0.5549999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.38 : { + bb : { + x : [0.38,0.0,35.10629921259843], + + v : [1.0,0.0,-0.57], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.39 : { + bb : { + x : [0.39,0.0,34.87893700787402], + + v : [1.0,0.0,-0.585], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.4 : { + bb : { + x : [0.4,0.0,34.64566929133859], + + v : [1.0,0.0,-0.6], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.41 : { + bb : { + x : [0.41,0.0,34.40649606299213], + + v : [1.0,0.0,-0.6149999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.42 : { + bb : { + x : [0.42,0.0,34.16141732283465], + + v : [1.0,0.0,-0.6299999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.43 : { + bb : { + x : [0.43,0.0,33.91043307086615], + + v : [1.0,0.0,-0.6449999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.44 : { + bb : { + x : [0.44,0.0,33.65354330708662], + + v : [1.0,0.0,-0.6599999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.45 : { + bb : { + x : [0.45,0.0,33.390748031496074], + + v : [1.0,0.0,-0.6749999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.46 : { + bb : { + x : [0.46,0.0,33.122047244094496], + + v : [1.0,0.0,-0.69], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.47 : { + bb : { + x : [0.47,0.0,32.8474409448819], + + v : [1.0,0.0,-0.7049999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.48 : { + bb : { + x : [0.48,0.0,32.56692913385828], + + v : [1.0,0.0,-0.7199999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.49 : { + bb : { + x : [0.49,0.0,32.28051181102363], + + v : [1.0,0.0,-0.7349999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.5 : { + bb : { + x : [0.5,0.0,31.988188976377963], + + v : [1.0,0.0,-0.7499999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.51 : { + bb : { + x : [0.51,0.0,31.68996062992127], + + v : [1.0,0.0,-0.7649999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.52 : { + bb : { + x : [0.52,0.0,31.38582677165355], + + v : [1.0,0.0,-0.7799999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.53 : { + bb : { + x : [0.53,0.0,31.075787401574814], + + v : [1.0,0.0,-0.7949999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.54 : { + bb : { + x : [0.54,0.0,30.759842519685048], + + v : [1.0,0.0,-0.8099999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.55 : { + bb : { + x : [0.55,0.0,30.43799212598426], + + v : [1.0,0.0,-0.825], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.56 : { + bb : { + x : [0.56,0.0,30.110236220472448], + + v : [1.0,0.0,-0.84], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.57 : { + bb : { + x : [0.57,0.0,29.776574803149614], + + v : [1.0,0.0,-0.8549999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.58 : { + bb : { + x : [0.58,0.0,29.43700787401576], + + v : [1.0,0.0,-0.8699999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.59 : { + bb : { + x : [0.59,0.0,29.091535433070877], + + v : [1.0,0.0,-0.8849999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.6 : { + bb : { + x : [0.6,0.0,28.740157480314974], + + v : [1.0,0.0,-0.8999999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.61 : { + bb : { + x : [0.61,0.0,28.38287401574804], + + v : [1.0,0.0,-0.9149999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.62 : { + bb : { + x : [0.62,0.0,28.01968503937009], + + v : [1.0,0.0,-0.9299999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.63 : { + bb : { + x : [0.63,0.0,27.65059055118111], + + v : [1.0,0.0,-0.9449999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.64 : { + bb : { + x : [0.64,0.0,27.27559055118111], + + v : [1.0,0.0,-0.9599999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.65 : { + bb : { + x : [0.65,0.0,26.89468503937009], + + v : [1.0,0.0,-0.9749999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.66 : { + bb : { + x : [0.66,0.0,26.50787401574804], + + v : [1.0,0.0,-0.9899999999999999], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.67 : { + bb : { + x : [0.67,0.0,26.115157480314974], + + v : [1.0,0.0,-1.005], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.68 : { + bb : { + x : [0.68,0.0,25.716535433070874], + + v : [1.0,0.0,-1.02], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.69 : { + bb : { + x : [0.69,0.0,25.31200787401576], + + v : [1.0,0.0,-1.035], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.7 : { + bb : { + x : [0.7,0.0,24.901574803149618], + + v : [1.0,0.0,-1.0499999999999998], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.71 : { + bb : { + x : [0.71,0.0,24.48523622047245], + + v : [1.0,0.0,-1.065], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.72 : { + bb : { + x : [0.72,0.0,24.062992125984266], + + v : [1.0,0.0,-1.08], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.73 : { + bb : { + x : [0.73,0.0,23.634842519685055], + + v : [1.0,0.0,-1.0950000000000002], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.74 : { + bb : { + x : [0.74,0.0,23.200787401574814], + + v : [1.0,0.0,-1.1100000000000003], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.75 : { + bb : { + x : [0.75,0.0,22.76082677165355], + + v : [1.0,0.0,-1.1250000000000004], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.76 : { + bb : { + x : [0.76,0.0,22.31496062992127], + + v : [1.0,0.0,-1.1400000000000006], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.77 : { + bb : { + x : [0.77,0.0,21.863188976377963], + + v : [1.0,0.0,-1.1550000000000007], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.78 : { + bb : { + x : [0.78,0.0,21.40551181102363], + + v : [1.0,0.0,-1.1700000000000008], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.79 : { + bb : { + x : [0.79,0.0,20.941929133858277], + + v : [1.0,0.0,-1.185000000000001], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.8 : { + bb : { + x : [0.8,0.0,20.472440944881896], + + v : [1.0,0.0,-1.200000000000001], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.81 : { + bb : { + x : [0.81,0.0,19.997047244094492], + + v : [1.0,0.0,-1.2150000000000012], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.82 : { + bb : { + x : [0.82,0.0,19.515748031496074], + + v : [1.0,0.0,-1.230000000000001], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.83 : { + bb : { + x : [0.83,0.0,19.028543307086622], + + v : [1.0,0.0,-1.245000000000001], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.84 : { + bb : { + x : [0.84,0.0,18.53543307086615], + + v : [1.0,0.0,-1.2600000000000011], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.85 : { + bb : { + x : [0.85,0.0,18.03641732283465], + + v : [1.0,0.0,-1.2750000000000012], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.86 : { + bb : { + x : [0.86,0.0,17.53149606299213], + + v : [1.0,0.0,-1.2900000000000014], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.87 : { + bb : { + x : [0.87,0.0,17.02066929133859], + + v : [1.0,0.0,-1.3050000000000015], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.88 : { + bb : { + x : [0.88,0.0,16.50393700787402], + + v : [1.0,0.0,-1.3200000000000016], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.89 : { + bb : { + x : [0.89,0.0,15.981299212598426], + + v : [1.0,0.0,-1.3350000000000017], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.9 : { + bb : { + x : [0.9,0.0,15.452755905511811], + + v : [1.0,0.0,-1.3500000000000019], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.91 : { + bb : { + x : [0.91,0.0,14.918307086614174], + + v : [1.0,0.0,-1.365000000000002], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.92 : { + bb : { + x : [0.92,0.0,14.377952755905511], + + v : [1.0,0.0,-1.3800000000000021], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.93 : { + bb : { + x : [0.93,0.0,13.831692913385824], + + v : [1.0,0.0,-1.3950000000000022], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.94 : { + bb : { + x : [0.94,0.0,13.27952755905512], + + v : [1.0,0.0,-1.4100000000000021], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.95 : { + bb : { + x : [0.95,0.0,12.721456692913385], + + v : [1.0,0.0,-1.425000000000002], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.96 : { + bb : { + x : [0.96,0.0,12.15748031496063], + + v : [1.0,0.0,-1.4400000000000022], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.97 : { + bb : { + x : [0.97,0.0,11.587598425196848], + + v : [1.0,0.0,-1.4550000000000023], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.98 : { + bb : { + x : [0.98,0.0,11.011811023622043], + + v : [1.0,0.0,-1.4700000000000024], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +0.99 : { + bb : { + x : [0.99,0.0,10.430118110236215], + + v : [1.0,0.0,-1.4850000000000025], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.0 : { + bb : { + x : [1.0,0.0,9.842519685039363], + + v : [1.0,0.0,-1.5000000000000027], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.01 : { + bb : { + x : [1.01,0.0,9.249015748031487], + + v : [1.0,0.0,-1.5150000000000028], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.02 : { + bb : { + x : [1.02,0.0,8.64960629921259], + + v : [1.0,0.0,-1.530000000000003], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.03 : { + bb : { + x : [1.03,0.0,8.044291338582665], + + v : [1.0,0.0,-1.545000000000003], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.04 : { + bb : { + x : [1.04,0.0,7.433070866141719], + + v : [1.0,0.0,-1.5600000000000032], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.05 : { + bb : { + x : [1.05,0.0,6.815944881889749], + + v : [1.0,0.0,-1.5750000000000033], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.06 : { + bb : { + x : [1.06,0.0,6.192913385826755], + + v : [1.0,0.0,-1.5900000000000034], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.07 : { + bb : { + x : [1.07,0.0,5.563976377952738], + + v : [1.0,0.0,-1.6050000000000035], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.08 : { + bb : { + x : [1.08,0.0,4.929133858267696], + + v : [1.0,0.0,-1.6200000000000037], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.09 : { + bb : { + x : [1.09,0.0,4.2883858267716315], + + v : [1.0,0.0,-1.6350000000000038], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.1 : { + bb : { + x : [1.1,0.0,3.6417322834645427], + + v : [1.0,0.0,-1.650000000000004], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.11 : { + bb : { + x : [1.11,0.0,2.9891732283464307], + + v : [1.0,0.0,-1.665000000000004], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.12 : { + bb : { + x : [1.12,0.0,2.330708661417295], + + v : [1.0,0.0,-1.6800000000000042], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.13 : { + bb : { + x : [1.13,0.0,1.6663385826771504], + + v : [1.0,0.0,-1.6950000000000038], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.14 : { + bb : { + x : [1.14,0.0,0.9960629921259674], + + v : [1.0,0.0,-1.710000000000004], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.15 : { + bb : { + x : [1.15,0.0,0.31988188976376064], + + v : [1.0,0.0,-1.725000000000004], + + x_b : [1.1547005383792517,0.0,0.0]}}, + +1.16 : { + bb : { + x : [1.1573502691896258,0.0,0.17985847125379134], + + v : [0.5,0.0,0.8580762113533185], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.17 : { + bb : { + x : [1.1623502691896257,0.0,0.5147309954086413], + + v : [0.5,0.0,0.8430762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.18 : { + bb : { + x : [1.1673502691896256,0.0,0.8436980077524677], + + v : [0.5,0.0,0.8280762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.19 : { + bb : { + x : [1.1723502691896255,0.0,1.1667595082852704], + + v : [0.5,0.0,0.8130762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.2 : { + bb : { + x : [1.1773502691896254,0.0,1.4839154970070494], + + v : [0.5,0.0,0.7980762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.21 : { + bb : { + x : [1.1823502691896253,0.0,1.7951659739178052], + + v : [0.5,0.0,0.7830762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.22 : { + bb : { + x : [1.1873502691896252,0.0,2.100510939017537], + + v : [0.5,0.0,0.7680762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.23 : { + bb : { + x : [1.192350269189625,0.0,2.3999503923062453], + + v : [0.5,0.0,0.7530762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.24 : { + bb : { + x : [1.197350269189625,0.0,2.6934843337839296], + + v : [0.5,0.0,0.7380762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.25 : { + bb : { + x : [1.2023502691896248,0.0,2.9811127634505907], + + v : [0.5,0.0,0.7230762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.26 : { + bb : { + x : [1.2073502691896247,0.0,3.2628356813062283], + + v : [0.5,0.0,0.7080762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.27 : { + bb : { + x : [1.2123502691896246,0.0,3.538653087350842], + + v : [0.5,0.0,0.6930762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.28 : { + bb : { + x : [1.2173502691896245,0.0,3.808564981584432], + + v : [0.5,0.0,0.6780762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.29 : { + bb : { + x : [1.2223502691896244,0.0,4.072571364006999], + + v : [0.5,0.0,0.6630762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.3 : { + bb : { + x : [1.2273502691896243,0.0,4.330672234618541], + + v : [0.5,0.0,0.6480762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.31 : { + bb : { + x : [1.2323502691896242,0.0,4.582867593419061], + + v : [0.5,0.0,0.6330762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.32 : { + bb : { + x : [1.237350269189624,0.0,4.829157440408556], + + v : [0.5,0.0,0.6180762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.33 : { + bb : { + x : [1.242350269189624,0.0,5.0695417755870285], + + v : [0.5,0.0,0.6030762113533182], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.34 : { + bb : { + x : [1.2473502691896239,0.0,5.304020598954477], + + v : [0.5,0.0,0.5880762113533182], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.35 : { + bb : { + x : [1.2523502691896238,0.0,5.532593910510901], + + v : [0.5,0.0,0.5730762113533182], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.36 : { + bb : { + x : [1.2573502691896237,0.0,5.755261710256303], + + v : [0.5,0.0,0.5580762113533182], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.37 : { + bb : { + x : [1.2623502691896236,0.0,5.9720239981906795], + + v : [0.5,0.0,0.5430762113533182], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.38 : { + bb : { + x : [1.2673502691896235,0.0,6.182880774314028], + + v : [0.5,0.0,0.5280762113533185], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.39 : { + bb : { + x : [1.2723502691896234,0.0,6.3878320386263585], + + v : [0.5,0.0,0.5130762113533185], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.4 : { + bb : { + x : [1.2773502691896232,0.0,6.586877791127665], + + v : [0.5,0.0,0.4980762113533185], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.41 : { + bb : { + x : [1.2823502691896231,0.0,6.780018031817948], + + v : [0.5,0.0,0.48307621135331846], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.42 : { + bb : { + x : [1.287350269189623,0.0,6.967252760697208], + + v : [0.5,0.0,0.46807621135331845], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.43 : { + bb : { + x : [1.292350269189623,0.0,7.148581977765444], + + v : [0.5,0.0,0.45307621135331844], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.44 : { + bb : { + x : [1.2973502691896228,0.0,7.324005683022656], + + v : [0.5,0.0,0.4380762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.45 : { + bb : { + x : [1.3023502691896227,0.0,7.493523876468845], + + v : [0.5,0.0,0.4230762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.46 : { + bb : { + x : [1.3073502691896226,0.0,7.65713655810401], + + v : [0.5,0.0,0.4080762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.47 : { + bb : { + x : [1.3123502691896225,0.0,7.81484372792815], + + v : [0.5,0.0,0.3930762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.48 : { + bb : { + x : [1.3173502691896224,0.0,7.966645385941268], + + v : [0.5,0.0,0.37807621135331837], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.49 : { + bb : { + x : [1.3223502691896223,0.0,8.112541532143362], + + v : [0.5,0.0,0.36307621135331836], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.5 : { + bb : { + x : [1.3273502691896222,0.0,8.252532166534433], + + v : [0.5,0.0,0.34807621135331834], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.51 : { + bb : { + x : [1.332350269189622,0.0,8.38661728911448], + + v : [0.5,0.0,0.33307621135331833], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.52 : { + bb : { + x : [1.337350269189622,0.0,8.514796899883503], + + v : [0.5,0.0,0.3180762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.53 : { + bb : { + x : [1.3423502691896219,0.0,8.637070998841502], + + v : [0.5,0.0,0.3030762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.54 : { + bb : { + x : [1.3473502691896218,0.0,8.753439585988477], + + v : [0.5,0.0,0.2880762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.55 : { + bb : { + x : [1.3523502691896216,0.0,8.86390266132443], + + v : [0.5,0.0,0.2730762113533183], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.56 : { + bb : { + x : [1.3573502691896215,0.0,8.968460224849359], + + v : [0.5,0.0,0.25807621135331826], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.57 : { + bb : { + x : [1.3623502691896214,0.0,9.067112276563263], + + v : [0.5,0.0,0.24307621135331825], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.58 : { + bb : { + x : [1.3673502691896213,0.0,9.159858816466144], + + v : [0.5,0.0,0.22807621135331824], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.59 : { + bb : { + x : [1.3723502691896212,0.0,9.246699844558002], + + v : [0.5,0.0,0.21307621135331822], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.6 : { + bb : { + x : [1.3773502691896211,0.0,9.327635360838835], + + v : [0.5,0.0,0.1980762113533182], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.61 : { + bb : { + x : [1.382350269189621,0.0,9.402665365308646], + + v : [0.5,0.0,0.1830762113533182], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.62 : { + bb : { + x : [1.387350269189621,0.0,9.471789857967433], + + v : [0.5,0.0,0.16807621135331818], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.63 : { + bb : { + x : [1.3923502691896208,0.0,9.535008838815195], + + v : [0.5,0.0,0.1530762113533185], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.64 : { + bb : { + x : [1.3973502691896207,0.0,9.592322307851934], + + v : [0.5,0.0,0.1380762113533185], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.65 : { + bb : { + x : [1.4023502691896206,0.0,9.64373026507765], + + v : [0.5,0.0,0.12307621135331848], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.66 : { + bb : { + x : [1.4073502691896205,0.0,9.689232710492343], + + v : [0.5,0.0,0.10807621135331846], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.67 : { + bb : { + x : [1.4123502691896204,0.0,9.728829644096011], + + v : [0.5,0.0,0.09307621135331845], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.68 : { + bb : { + x : [1.4173502691896203,0.0,9.762521065888656], + + v : [0.5,0.0,0.07807621135331844], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.69 : { + bb : { + x : [1.4223502691896202,0.0,9.790306975870278], + + v : [0.5,0.0,0.06307621135331842], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.7 : { + bb : { + x : [1.42735026918962,0.0,9.812187374040876], + + v : [0.5,0.0,0.04807621135331841], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.71 : { + bb : { + x : [1.43235026918962,0.0,9.82816226040045], + + v : [0.5,0.0,0.0330762113533184], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.72 : { + bb : { + x : [1.4373502691896198,0.0,9.838231634949], + + v : [0.5,0.0,0.018076211353318383], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.73 : { + bb : { + x : [1.4423502691896197,0.0,9.842395497686528], + + v : [0.5,0.0,0.00307621135331837], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.74 : { + bb : { + x : [1.4473502691896196,0.0,9.84065384861303], + + v : [0.5,0.0,-0.011923788646681643], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.75 : { + bb : { + x : [1.4523502691896195,0.0,9.83300668772851], + + v : [0.5,0.0,-0.026923788646681657], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.76 : { + bb : { + x : [1.4573502691896194,0.0,9.819454015032965], + + v : [0.5,0.0,-0.04192378864668167], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.77 : { + bb : { + x : [1.4623502691896193,0.0,9.799995830526399], + + v : [0.5,0.0,-0.05692378864668168], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.78 : { + bb : { + x : [1.4673502691896192,0.0,9.774632134208806], + + v : [0.5,0.0,-0.0719237886466817], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.79 : { + bb : { + x : [1.472350269189619,0.0,9.743362926080191], + + v : [0.5,0.0,-0.08692378864668171], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.8 : { + bb : { + x : [1.477350269189619,0.0,9.706188206140553], + + v : [0.5,0.0,-0.10192378864668172], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.81 : { + bb : { + x : [1.4823502691896189,0.0,9.663107974389892], + + v : [0.5,0.0,-0.11692378864668174], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.82 : { + bb : { + x : [1.4873502691896188,0.0,9.614122230828205], + + v : [0.5,0.0,-0.13192378864668175], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.83 : { + bb : { + x : [1.4923502691896187,0.0,9.559230975455495], + + v : [0.5,0.0,-0.14692378864668176], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.84 : { + bb : { + x : [1.4973502691896186,0.0,9.498434208271764], + + v : [0.5,0.0,-0.16192378864668178], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.85 : { + bb : { + x : [1.5023502691896184,0.0,9.431731929277007], + + v : [0.5,0.0,-0.1769237886466818], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.86 : { + bb : { + x : [1.5073502691896183,0.0,9.359124138471225], + + v : [0.5,0.0,-0.1919237886466818], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.87 : { + bb : { + x : [1.5123502691896182,0.0,9.280610835854421], + + v : [0.5,0.0,-0.20692378864668182], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.88 : { + bb : { + x : [1.5173502691896181,0.0,9.196192021426596], + + v : [0.5,0.0,-0.2219237886466815], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.89 : { + bb : { + x : [1.522350269189618,0.0,9.105867695187744], + + v : [0.5,0.0,-0.2369237886466815], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.9 : { + bb : { + x : [1.527350269189618,0.0,9.00963785713787], + + v : [0.5,0.0,-0.2519237886466815], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.91 : { + bb : { + x : [1.5323502691896178,0.0,8.907502507276972], + + v : [0.5,0.0,-0.26692378864668154], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.92 : { + bb : { + x : [1.5373502691896177,0.0,8.79946164560505], + + v : [0.5,0.0,-0.28192378864668155], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.93 : { + bb : { + x : [1.5423502691896176,0.0,8.685515272122105], + + v : [0.5,0.0,-0.29692378864668156], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.94 : { + bb : { + x : [1.5473502691896175,0.0,8.565663386828135], + + v : [0.5,0.0,-0.3119237886466816], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.95 : { + bb : { + x : [1.5523502691896174,0.0,8.439905989723142], + + v : [0.5,0.0,-0.3269237886466816], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.96 : { + bb : { + x : [1.5573502691896173,0.0,8.308243080807125], + + v : [0.5,0.0,-0.3419237886466816], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.97 : { + bb : { + x : [1.5623502691896172,0.0,8.170674660080087], + + v : [0.5,0.0,-0.3569237886466816], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.98 : { + bb : { + x : [1.567350269189617,0.0,8.027200727542022], + + v : [0.5,0.0,-0.37192378864668163], + + x_b : [1.732050807568879,0.0,0.0]}}, + +1.99 : { + bb : { + x : [1.572350269189617,0.0,7.877821283192935], + + v : [0.5,0.0,-0.38692378864668164], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.0 : { + bb : { + x : [1.5773502691896168,0.0,7.722536327032824], + + v : [0.5,0.0,-0.40192378864668166], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.01 : { + bb : { + x : [1.5823502691896167,0.0,7.561345859061692], + + v : [0.5,0.0,-0.41692378864668134], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.02 : { + bb : { + x : [1.5873502691896169,0.0,7.39424987927953], + + v : [0.5,0.0,-0.4319237886466817], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.03 : { + bb : { + x : [1.5923502691896168,0.0,7.2212483876863525], + + v : [0.5,0.0,-0.44692378864668136], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.04 : { + bb : { + x : [1.5973502691896169,0.0,7.0423413842821425], + + v : [0.5,0.0,-0.4619237886466817], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.05 : { + bb : { + x : [1.6023502691896168,0.0,6.8575288690669165], + + v : [0.5,0.0,-0.4769237886466814], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.06 : { + bb : { + x : [1.6073502691896169,0.0,6.666810842040659], + + v : [0.5,0.0,-0.49192378864668174], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.07 : { + bb : { + x : [1.6123502691896168,0.0,6.470187303203388], + + v : [0.5,0.0,-0.5069237886466814], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.08 : { + bb : { + x : [1.6173502691896169,0.0,6.2676582525550835], + + v : [0.5,0.0,-0.5219237886466818], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.09 : { + bb : { + x : [1.6223502691896168,0.0,6.059223690095764], + + v : [0.5,0.0,-0.5369237886466814], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.1 : { + bb : { + x : [1.627350269189617,0.0,5.844883615825412], + + v : [0.5,0.0,-0.5519237886466818], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.11 : { + bb : { + x : [1.6323502691896168,0.0,5.6246380297440455], + + v : [0.5,0.0,-0.5669237886466815], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.12 : { + bb : { + x : [1.637350269189617,0.0,5.398486931851647], + + v : [0.5,0.0,-0.5819237886466818], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.13 : { + bb : { + x : [1.6423502691896168,0.0,5.166430322148233], + + v : [0.5,0.0,-0.5969237886466815], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.14 : { + bb : { + x : [1.647350269189617,0.0,4.928468200633786], + + v : [0.5,0.0,-0.6119237886466818], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.15 : { + bb : { + x : [1.6523502691896168,0.0,4.684600567308325], + + v : [0.5,0.0,-0.6269237886466815], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.16 : { + bb : { + x : [1.657350269189617,0.0,4.434827422171831], + + v : [0.5,0.0,-0.6419237886466819], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.17 : { + bb : { + x : [1.6623502691896168,0.0,4.179148765224324], + + v : [0.5,0.0,-0.6569237886466816], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.18 : { + bb : { + x : [1.667350269189617,0.0,3.917564596465782], + + v : [0.5,0.0,-0.6719237886466819], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.19 : { + bb : { + x : [1.6723502691896168,0.0,3.6500749158962282], + + v : [0.5,0.0,-0.6869237886466816], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.2 : { + bb : { + x : [1.677350269189617,0.0,3.3766797235156387], + + v : [0.5,0.0,-0.7019237886466819], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.21 : { + bb : { + x : [1.6823502691896168,0.0,3.0973790193240376], + + v : [0.5,0.0,-0.7169237886466816], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.22 : { + bb : { + x : [1.687350269189617,0.0,2.8121728033214004], + + v : [0.5,0.0,-0.731923788646682], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.23 : { + bb : { + x : [1.6923502691896168,0.0,2.5210610755077525], + + v : [0.5,0.0,-0.7469237886466816], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.24 : { + bb : { + x : [1.697350269189617,0.0,2.2240438358830676], + + v : [0.5,0.0,-0.761923788646682], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.25 : { + bb : { + x : [1.7023502691896168,0.0,1.9211210844473727], + + v : [0.5,0.0,-0.7769237886466817], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.26 : { + bb : { + x : [1.7073502691896167,0.0,1.6122928212006544], + + v : [0.5,0.0,-0.7919237886466813], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.27 : { + bb : { + x : [1.7123502691896169,0.0,1.2975590461428985], + + v : [0.5,0.0,-0.8069237886466817], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.28 : { + bb : { + x : [1.7173502691896168,0.0,0.9769197592741332], + + v : [0.5,0.0,-0.8219237886466814], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.29 : { + bb : { + x : [1.7223502691896169,0.0,0.6503749605943299], + + v : [0.5,0.0,-0.8369237886466817], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.3 : { + bb : { + x : [1.7273502691896168,0.0,0.31792465010351745], + + v : [0.5,0.0,-0.8519237886466814], + + x_b : [1.732050807568879,0.0,0.0]}}, + +2.31 : { + bb : { + x : [1.7322005383792525,0.0,0.010199698395215402], + + v : [0.25,0.0,0.4321143170299793], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.32 : { + bb : { + x : [1.7347005383792524,0.0,0.17737068935189665], + + v : [0.25,0.0,0.41711431702997964], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.33 : { + bb : { + x : [1.7372005383792524,0.0,0.3386361684975615], + + v : [0.25,0.0,0.4021143170299793], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.34 : { + bb : { + x : [1.7397005383792523,0.0,0.4939961358321958], + + v : [0.25,0.0,0.3871143170299796], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.35 : { + bb : { + x : [1.7422005383792523,0.0,0.6434505913558131], + + v : [0.25,0.0,0.37211431702997927], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.36 : { + bb : { + x : [1.7447005383792522,0.0,0.7869995350684004], + + v : [0.25,0.0,0.3571143170299796], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.37 : { + bb : { + x : [1.7472005383792522,0.0,0.9246429669699702], + + v : [0.25,0.0,0.34211431702997924], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.38 : { + bb : { + x : [1.749700538379252,0.0,1.0563808870605105], + + v : [0.25,0.0,0.32711431702997956], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.39 : { + bb : { + x : [1.752200538379252,0.0,1.1822132953400326], + + v : [0.25,0.0,0.3121143170299792], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.4 : { + bb : { + x : [1.754700538379252,0.0,1.3021401918085258], + + v : [0.25,0.0,0.29711431702997954], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.41 : { + bb : { + x : [1.757200538379252,0.0,1.4161615764660007], + + v : [0.25,0.0,0.2821143170299792], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.42 : { + bb : { + x : [1.7597005383792519,0.0,1.524277449312447], + + v : [0.25,0.0,0.2671143170299795], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.43 : { + bb : { + x : [1.7622005383792518,0.0,1.6264878103478742], + + v : [0.25,0.0,0.25211431702997916], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.44 : { + bb : { + x : [1.7647005383792518,0.0,1.7227926595722731], + + v : [0.25,0.0,0.23711431702997948], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.45 : { + bb : { + x : [1.7672005383792517,0.0,1.813191996985653], + + v : [0.25,0.0,0.22211431702997914], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.46 : { + bb : { + x : [1.7697005383792517,0.0,1.8976858225880051], + + v : [0.25,0.0,0.20711431702997946], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.47 : { + bb : { + x : [1.7722005383792516,0.0,1.9762741363793372], + + v : [0.25,0.0,0.1921143170299791], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.48 : { + bb : { + x : [1.7747005383792516,0.0,2.0489569383596424], + + v : [0.25,0.0,0.17711431702997943], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.49 : { + bb : { + x : [1.7772005383792515,0.0,2.1157342285289276], + + v : [0.25,0.0,0.16211431702997908], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.5 : { + bb : { + x : [1.7797005383792515,0.0,2.1766060068871855], + + v : [0.25,0.0,0.1471143170299794], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.51 : { + bb : { + x : [1.7822005383792514,0.0,2.2315722734344203], + + v : [0.25,0.0,0.13211431702997972], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.52 : { + bb : { + x : [1.7847005383792514,0.0,2.2806330281706337], + + v : [0.25,0.0,0.11711431702997938], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.53 : { + bb : { + x : [1.7872005383792513,0.0,2.3237882710958218], + + v : [0.25,0.0,0.1021143170299797], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.54 : { + bb : { + x : [1.7897005383792512,0.0,2.361038002209988], + + v : [0.25,0.0,0.08711431702997935], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.55 : { + bb : { + x : [1.7922005383792512,0.0,2.3923822215131287], + + v : [0.25,0.0,0.07211431702997967], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.56 : { + bb : { + x : [1.7947005383792511,0.0,2.4178209290052473], + + v : [0.25,0.0,0.05711431702997932], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.57 : { + bb : { + x : [1.797200538379251,0.0,2.4373541246863413], + + v : [0.25,0.0,0.04211431702997964], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.58 : { + bb : { + x : [1.799700538379251,0.0,2.450981808556412], + + v : [0.25,0.0,0.027114317029979296], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.59 : { + bb : { + x : [1.802200538379251,0.0,2.4587039806154594], + + v : [0.25,0.0,0.012114317029979615], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.6 : { + bb : { + x : [1.804700538379251,0.0,2.4605206408634825], + + v : [0.25,0.0,-0.002885682970020731], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.61 : { + bb : { + x : [1.8072005383792509,0.0,2.4564317893004826], + + v : [0.25,0.0,-0.01788568297002041], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.62 : { + bb : { + x : [1.8097005383792508,0.0,2.446437425926458], + + v : [0.25,0.0,-0.03288568297002076], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.63 : { + bb : { + x : [1.8122005383792508,0.0,2.430537550741411], + + v : [0.25,0.0,-0.04788568297002044], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.64 : { + bb : { + x : [1.8147005383792507,0.0,2.4087321637453396], + + v : [0.25,0.0,-0.06288568297002078], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.65 : { + bb : { + x : [1.8172005383792507,0.0,2.381021264938245], + + v : [0.25,0.0,-0.07788568297002046], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.66 : { + bb : { + x : [1.8197005383792506,0.0,2.3474048543201262], + + v : [0.25,0.0,-0.09288568297002081], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.67 : { + bb : { + x : [1.8222005383792506,0.0,2.307882931890985], + + v : [0.25,0.0,-0.10788568297002049], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.68 : { + bb : { + x : [1.8247005383792505,0.0,2.2624554976508184], + + v : [0.25,0.0,-0.12288568297002084], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.69 : { + bb : { + x : [1.8272005383792504,0.0,2.21112255159963], + + v : [0.25,0.0,-0.13788568297002052], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.7 : { + bb : { + x : [1.8297005383792504,0.0,2.1538840937374157], + + v : [0.25,0.0,-0.15288568297002086], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.71 : { + bb : { + x : [1.8322005383792503,0.0,2.090740124064181], + + v : [0.25,0.0,-0.16788568297002054], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.72 : { + bb : { + x : [1.8347005383792503,0.0,2.021690642579919], + + v : [0.25,0.0,-0.1828856829700209], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.73 : { + bb : { + x : [1.8372005383792502,0.0,1.946735649284637], + + v : [0.25,0.0,-0.19788568297002057], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.74 : { + bb : { + x : [1.8397005383792502,0.0,1.865875144178328], + + v : [0.25,0.0,-0.21288568297002092], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.75 : { + bb : { + x : [1.8422005383792501,0.0,1.7791091272609987], + + v : [0.25,0.0,-0.2278856829700206], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.76 : { + bb : { + x : [1.84470053837925,0.0,1.686437598532646], + + v : [0.25,0.0,-0.24288568297002028], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.77 : { + bb : { + x : [1.84720053837925,0.0,1.5878605579932659], + + v : [0.25,0.0,-0.2578856829700206], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.78 : { + bb : { + x : [1.84970053837925,0.0,1.4833780056428665], + + v : [0.25,0.0,-0.2728856829700203], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.79 : { + bb : { + x : [1.85220053837925,0.0,1.3729899414814386], + + v : [0.25,0.0,-0.28788568297002065], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.8 : { + bb : { + x : [1.8547005383792499,0.0,1.2566963655089922], + + v : [0.25,0.0,-0.30288568297002033], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.81 : { + bb : { + x : [1.8572005383792498,0.0,1.134497277725517], + + v : [0.25,0.0,-0.3178856829700207], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.82 : { + bb : { + x : [1.8597005383792498,0.0,1.0063926781310233], + + v : [0.25,0.0,-0.33288568297002036], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.83 : { + bb : { + x : [1.8622005383792497,0.0,0.8723825667255003], + + v : [0.25,0.0,-0.3478856829700207], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.84 : { + bb : { + x : [1.8647005383792497,0.0,0.7324669435089598], + + v : [0.25,0.0,-0.3628856829700204], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.85 : { + bb : { + x : [1.8672005383792496,0.0,0.5866458084813894], + + v : [0.25,0.0,-0.37788568297002073], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.86 : { + bb : { + x : [1.8697005383792495,0.0,0.4349191616428018], + + v : [0.25,0.0,-0.3928856829700204], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.87 : { + bb : { + x : [1.8722005383792495,0.0,0.27728700299318376], + + v : [0.25,0.0,-0.40788568297002076], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.88 : { + bb : { + x : [1.8747005383792494,0.0,0.11374933253254924], + + v : [0.25,0.0,-0.42288568297002044], + + x_b : [1.8763883748662857,0.0,0.0]}}, + +2.89 : { + bb : { + x : [1.8767944566227692,0.0,0.02737948501553115], + + v : [0.125,0.0,0.21163336986830958], + + x_b : [1.9124727666906374,0.0,0.0]}}, + +2.9 : { + bb : { + x : [1.8780444566227692,0.0,0.10774695346761987], + + v : [0.125,0.0,0.1966333698683099], + + x_b : [1.9124727666906374,0.0,0.0]}}, + +2.91 : { + bb : { + x : [1.8792944566227692,0.0,0.1822089101086884], + + v : [0.125,0.0,0.18163336986830955], + + x_b : [1.9124727666906374,0.0,0.0]}}, + +2.92 : { + bb : { + x : [1.8805444566227691,0.0,0.2507653549387301], + + v : [0.125,0.0,0.16663336986830987], + + x_b : [1.9124727666906374,0.0,0.0]}}, + +2.93 : { + bb : { + x : [1.8817944566227691,0.0,0.3134162879577511], + + v : [0.125,0.0,0.15163336986830953], + + x_b : [1.9124727666906374,0.0,0.0]}}, + +2.94 : { + bb : { + x : [1.883044456622769,0.0,0.37016170916574587], + + v : [0.125,0.0,0.13663336986830985], + + x_b : [1.9124727666906374,0.0,0.0]}}, + +2.95 : { + bb : { + x : [1.884294456622769,0.0,0.42100161856271934], + + v : [0.125,0.0,0.1216333698683095], + + x_b : [1.9124727666906374,0.0,0.0]}}, + +2.96 : { + bb : { + x : [1.885544456622769,0.0,0.4659360161486671], + + v : [0.125,0.0,0.10663336986830982], + + x_b : [1.9124727666906374,0.0,0.0]}}, + +2.97 : { + bb : { + x : [1.886794456622769,0.0,0.504964901923593], + + v : [0.125,0.0,0.09163336986830947], + + x_b : [1.9124727666906374,0.0,0.0]}}, + +2.98 : { + bb : { + x : [1.888044456622769,0.0,0.5380882758874937], + + v : [0.125,0.0,0.07663336986830979], + + x_b : [1.9124727666906374,0.0,0.0]}}, + +2.99 : { + bb : { + x : [1.889294456622769,0.0,0.5653061380403721], + + v : [0.125,0.0,0.061633369868309446], + + x_b : [1.9124727666906374,0.0,0.0]}}, + +3.0 : { + bb : { + x : [1.890544456622769,0.0,0.5866184883822259], + + v : [0.125,0.0,0.046633369868309765], + + x_b : [1.9124727666906374,0.0,0.0]}}} diff --git a/tests/test_BouncingBall.py b/tests/test_BouncingBall.py index 9a647d7..51c9008 100644 --- a/tests/test_BouncingBall.py +++ b/tests/test_BouncingBall.py @@ -1,5 +1,6 @@ from math import sqrt from pathlib import Path + import pytest from fmpy import plot_result, simulate_fmu @@ -56,6 +57,7 @@ def test_run_fmpy(show): 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_assertion.py b/tests/test_assertion.py index 144aa51..5e85f12 100644 --- a/tests/test_assertion.py +++ b/tests/test_assertion.py @@ -1,12 +1,14 @@ -from case_study.assertion import Assertion -from math import sin, cos +from math import cos, sin + import matplotlib.pyplot as plt import pytest -from sympy import symbols, sympify +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] -_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() @@ -14,54 +16,58 @@ def show_data(): 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} + 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} - + 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") + 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') + 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" + 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)]) + 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 + 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'") as err: - ass.assert_series([('t',_t), ('x',_x)], '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") as err: + 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" + ) as err: ass = Assertion("y==4") - ass = Assertion( "y-4") - assert 0==ass.assert_single( [('y', 4)]) - + print(err) + 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}" + # retcode = pytest.main(["-rA","-v", __file__]) + # assert retcode == 0, f"Non-zero return code {retcode}" test_init() - test_assertion() \ No newline at end of file + test_assertion() diff --git a/tests/test_bouncing_ball_3d.py b/tests/test_bouncing_ball_3d.py index eceb489..ffb581e 100644 --- a/tests/test_bouncing_ball_3d.py +++ b/tests/test_bouncing_ball_3d.py @@ -2,28 +2,43 @@ from pathlib import Path import pytest -from component_model.example_models.bouncing_ball_3d import BouncingBall3D +from case_study.case import Case, Cases + +# from component_model.tests.examples.bouncing_ball_3d import BouncingBall3D from component_model.model import Model from fmpy import plot_result, simulate_fmu -from shutil import copy -def nearly_equal(res: tuple, expected: tuple, eps=1e-7): + +def arrays_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): + +@pytest.fixture(scope="session") +def ensure_fmu(): + return _ensure_fmu() + + +def _ensure_fmu(): + """Make sure that the FMU is available in test_working_directory""" + import component_model.model + + build_path = Path(__file__).parent / "data" / "BouncingBall3D" + build_path.mkdir(exist_ok=True) 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") + str(Path(component_model.model.__file__).parent.parent / "tests" / "examples" / "bouncing_ball_3d.py"), + project_files=[], + dest=build_path, + ) + return fmu_path def test_run_fmpy(show): """Test and validate the basic BouncingBall using fmpy and not using OSP or case_study.""" - path = Path("BouncingBall3D.fmu") + path = Path(__file__).parent / "data" / "BouncingBall3D" / "BouncingBall3D.fmu" assert path.exists(), f"File {path} does not exist" dt = 0.01 result = simulate_fmu( @@ -37,46 +52,121 @@ def test_run_fmpy(show): visible=True, logger=print, # fmi_call_logger=print, start_values={ - "e": 0.71, + "pos[2]": 10.0 * 0.0254, + "speed[0]": 1.0, + "e": 0.9, "g": 9.81, }, ) + assert len(result) 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, + # no more testing than that. This is done in component-model tests + + +def check_case( + cases: Cases, + casename: str, + stepSize: float = 0.01, + stopTime: float = 3, + g: float = 9.81, + e: float = 1.0, + x_z: float = 1 / 0.0254, # this is in inch => 1m! + hf: float = 0.0254, # transformation m->inch +): + """Run case 'name' and check results with respect to key issues.""" + case = cases.case_by_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("test_case") # 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 + # 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.spec.items(): + if k in ("stepSize", "stopTime"): + pass + elif k == "g": + g = val + elif k == "e": + e = val + elif k == "x[2]": + x[2] = val + else: + raise KeyError(f"Unknown key {k}") + # check correct reporting of start values: ! seems unfortunately not possible! + # expected time and position of first bounce + t_bounce = sqrt(2 * x[2] * hf / g) + 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.01'].bb.e") == e, "Not possible to check at time 0!" + assert results.res.jspath("$['0.01'].bb.g") == g, "Not possible to check at time 0!" + 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") + arrays_equal(x_b, [x_bounce, 0, 0]) + # 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]) + arrays_equal(results.res.jspath(f"$['{t_before}'].bb.x_b"), [x_bounce, 0, 0]) + # 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], ) - 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]) + arrays_equal(results.res.jspath(f"$['{t_before+dt}'].bb.v"), [e * v[0], 0, (v_bounce * e - g * ddt)]) + arrays_equal(results.res.jspath(f"$['{t_before+dt}'].bb.x_b"), [x_bounce2, 0, 0]) + # 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}") + for n in range(2, 100): # from bounce to bounce + print(f"Case {casename}. Bounce {n}") + v_x = v_x * e # adjusted speeds + v_z = v_z * e + delta_t = 2 * v_z / g # time for one bounce (parabola): v(t) = v0 - g*t/2 => 2*v0/g = t + 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: + 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] + 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" + assert _vx * e == vx_, f"Bounce {n}@{t_b}. Reduced speed in x-direction. {_vx}*{e}!={vx_}" + + +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) 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) \ No newline at end of file + 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") + # fmu = _ensure_fmu() + # test_run_fmpy( show=True) + # test_run_cases() diff --git a/tests/test_case.py b/tests/test_case.py index 02ab238..46ccceb 100644 --- a/tests/test_case.py +++ b/tests/test_case.py @@ -3,18 +3,21 @@ import pytest from case_study.case import Case, Cases -from case_study.json5 import json5_write +from case_study.json5 import Json5 from case_study.simulator_interface import SimulatorInterface @pytest.fixture def simpletable(scope="module", autouse=True): + return _simpletable() + + +def _simpletable(): path = Path(Path(__file__).parent, "data/SimpleTable/test.cases") assert path.exists(), "SimpleTable cases file not found" return Cases(path) -@pytest.mark.skip(reason="Causes an error when run with the other tests!") def test_fixture(simpletable): assert isinstance(simpletable, Cases), f"Cases object expected. Found:{simpletable}" @@ -57,7 +60,8 @@ def _make_cases(): "caseX": {"description": "Based case1 longer simulation", "parent": "case1", "spec": {"stopTime": 10}}, "results": {"spec": ["x@step", "x[0]@1.0", "i"]}, } - json5_write(json5, "data/test.cases") + js = Json5(json5) + js.write("data/test.cases") _ = SimulatorInterface("data/OspSystemStructure.xml", "testSystem") _ = Cases("data/test.cases") @@ -111,7 +115,7 @@ def do_case_range(txt: str, casename: str, expected: tuple | str, simpletable): if isinstance(expected, str): # error case with pytest.raises(Exception) as err: case._disect_range(txt) - print(f"ERROR:{err.value}") + # print(f"ERROR:{err.value}") assert str(err.value).startswith(expected) else: assert case._disect_range(txt) == expected @@ -188,3 +192,9 @@ def test_case_set_get(simpletable): 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.absolute() / "test_working_directory") +# test_fixture(_simpletable()) +# test_case_at_time(_simpletable()) +# test_case_range(_simpletable()) +# test_case_set_get(_simpletable()) diff --git a/tests/test_cases.py b/tests/test_cases.py index 1b5c53a..e00a16a 100644 --- a/tests/test_cases.py +++ b/tests/test_cases.py @@ -1,5 +1,6 @@ from pathlib import Path +import pytest from case_study.case import Cases from case_study.simulator_interface import SimulatorInterface @@ -29,7 +30,6 @@ def _file(file: str = "BouncingBall.cases"): def test_cases_management(): cases = Cases(_file("data/SimpleTable/test.cases")) - assert cases.results.results == {} assert cases.case_var_by_ref(0, 1) == ( "x", (1,), @@ -79,3 +79,8 @@ def test_cases(): assert cases.variables["h"]["variability"] == 4 vs = dict((k, v) for k, v in cases.variables.items() if k.startswith("v")) assert all(x in vs for x in ("v_min", "v_z", "v")) + + +if __name__ == "__main__": + retcode = pytest.main(["-rA", "-v", __file__]) + assert retcode == 0, f"Non-zero return code {retcode}" diff --git a/tests/test_json5.py b/tests/test_json5.py index 82d0b70..36cd4c1 100644 --- a/tests/test_json5.py +++ b/tests/test_json5.py @@ -1,72 +1,227 @@ +import time from pathlib import Path import pytest -from case_study.json5 import Json5Reader, json5_write +from case_study.json5 import Json5 + + +@pytest.fixture(scope="session") +def ex(scope="session"): + return _rfc9535_example() + + +def _rfc9535_example(): + js5 = """{ store: { + book: [ + { category: "reference", + author: "Nigel Rees", + title: "Sayings of the Century", + price: 8.95 + }, + { category: "fiction", + author: "Evelyn Waugh", + title: "Sword of Honour", + price: 12.99 + }, + { category: "fiction", + author: "Herman Melville", + title: "Moby Dick", + isbn: "0-553-21311-3", + price: 8.99 + }, + { category: "fiction", + author: "J. R. R. Tolkien", + title: "The Lord of the Rings", + isbn: "0-395-19395-8", + price: 22.99 + } + ], + bicycle: { + color: "red", + price: 399 + } + }}""" + js = Json5(js5) + return js + + +def test_jpath(ex): + assert isinstance(ex.js_py, dict), f"Expect dict. Found {ex.js_py}" + found = ex.jspath("$.store.book[*].author", list) + assert found == [ + "Nigel Rees", + "Evelyn Waugh", + "Herman Melville", + "J. R. R. Tolkien", + ], f"The authors of all books in the store: {found}" + found = ex.jspath("$..author", list) + assert found == ["Nigel Rees", "Evelyn Waugh", "Herman Melville", "J. R. R. Tolkien"], f"All authors: {found}" + found = ex.jspath("$.store.*", list) + assert isinstance(found[0], list) and isinstance(found[1], dict), "Everything ins store: books and a bike" + assert ex.jspath("$.store..price", list) == [8.95, 12.99, 8.99, 22.99, 399], "Price of all articles in the store" + found = ex.jspath("$..book[2]", typ=dict) + expected = { + "category": "fiction", + "author": "Herman Melville", + "title": "Moby Dick", + "isbn": "0-553-21311-3", + "price": 8.99, + } + assert found == expected, "The third book" + assert ex.jspath("$..book[2].author", str) == "Herman Melville", "The third book's author" + assert ( + ex.jspath("$..book[2].publisher", str) is None + ), "empty result: the third book does not have a 'publisher' member" + found = ex.jspath("$..book[-1]", dict) + expected = { + "category": "fiction", + "author": "J. R. R. Tolkien", + "title": "The Lord of the Rings", + "isbn": "0-395-19395-8", + "price": 22.99, + } + assert found == expected, f"The last book in order: {found}" + found = ex.jspath("$..book[:2]", list) + assert len(found) == 2, f"The first two books: {found}" + # found = ex.jspath( '$..book[0,1]', list) #!! does not seem to work, but [:2] works + # assert len(found)==2, f"The first two books: {found}" + assert 2 == len(ex.jspath("$..book[?@.isbn]", list)), "All books with an ISBN number" + assert 2 == len(ex.jspath("$..book[?@.price<10]", None)), "All books cheaper than 10" + assert 23 == len(ex.jspath("$..*", list)), "All member values and array elements contained in the input value" + # working with expected match and expected type, raising an error message, or not + assert ex.jspath("$..book[2].authors", typ=str, errorMsg=False) is None, "Fail silently" + with pytest.raises(KeyError) as err: + found = ex.jspath("$..book[2].authors", typ=str, errorMsg=True) + assert str(err.value) == "'No match for $..book[2].authors'", f"ERR:{err.value}" + + assert ex.jspath("$..book[2].author", typ=float, errorMsg=False) is None, "Fail silently" + with pytest.raises(ValueError) as err: + found = ex.jspath("$..book[2].author", typ=float, errorMsg=True) + assert ( + str(err.value) == "$..book[2].author matches, but type does not match ." + ), f"ERR:{err.value}" + + # some selected jsonpath extensions: + # not yet tested (not found out how it is done): + # where: jsonpath1 where jsonpath2 : Any nodes matching jsonpath1 with a child matching jsonpath2 + # wherenot: jsonpath1 wherenot jsonpath2 : Any nodes matching jsonpath1 with a child not matching jsonpath2 + # |: works but strange result: found = ex.jspath( '$..book[?@.price<10] | $..book[?@.price>10]', None) + + js = Json5("{header : { case : 'Test', timeFactor : 1.0}, 0.0 : { bb: { h : [0,0,1], v : 2.3}}}") + assert js.jspath("$['0.0']", dict) == {"bb": {"h": [0, 0, 1], "v": 2.3}}, "Use [] notation when path includes '.'" + # print("FOUND", type(found), 0 if found is None else len(found), found) + + +def test_update(ex): + assert Json5._spath_to_keys("$.Hei[ho]Hi[ha]he.hu") == ["Hei", "ho", "Hi", "ha", "he", "hu"] + assert Json5._spath_to_keys("$.Hei[0.0]Hi[1.0]he.hu") == ["Hei", "0.0", "Hi", "1.0", "he", "hu"] + + # ex.js_py['store']['book'].append({'category':'crime','author':'noname'}) + # print(ex.js_py) + # print("FOUND", type(found), 0 if found is None else len(found), found) + + ex.update("$.store.book", {"category": "crime", "author": "noname"}) + assert ex.jspath("$..book[-1]", typ=dict) == {"category": "crime", "author": "noname"}, "Book added" + + # start with header and add the first data + js = Json5("{header : { case : 'Test', timeFactor : 1.0}, ") + js.update("$[0.0]bb", {"f": 9.9}) + expected = {"header": {"case": "Test", "timeFactor": 1.0}, "0.0": {"bb": {"f": 9.9}}} + assert js.js_py == expected + + js = Json5("{header : { case : 'Test', timeFactor : 1.0}, 0.0 : { bb: { h : [0,0,1], v : 2.3}}}") + js.update("$[0.0]bb", {"f": 9.9}) + expected = {"header": {"case": "Test", "timeFactor": 1.0}, "0.0": {"bb": {"h": [0, 0, 1], "v": 2.3, "f": 9.9}}} + assert js.js_py == expected + + js = Json5("{header : { case : 'Test', timeFactor : 1.0}, 0.0 : { bb: { h : [0,0,1], v : 2.3}}}") + js.update("$[0.0]", {"bc": {"f": 9.9}}) + expected = { + "header": {"case": "Test", "timeFactor": 1.0}, + "0.0": {"bb": {"h": [0, 0, 1], "v": 2.3}, "bc": {"f": 9.9}}, + } + assert js.js_py == expected, f"\n{js.js_py}\n !=\n{expected}" + + js = Json5("{header : { case : 'Test', timeFactor : 1.0}, 0.0 : { bb: { h : [0,0,1], v : 2.3}}}") + js.update("$.", {"1.0": {"bb": {"h": [0, 0, 0.98]}}}) + expected = { + "header": {"case": "Test", "timeFactor": 1.0}, + "0.0": {"bb": {"h": [0, 0, 1], "v": 2.3}}, + "1.0": {"bb": {"h": [0, 0, 0.98]}}, + } + assert js.js_py == expected + + js = Json5("{header : { case : 'Test', timeFactor : 1.0}, 0.0 : { bb: { h : [0,0,1], v : 2.3}}}") + js.update("$.header.case", "Operation") + assert js.jspath("$.header.case") == "Operation", "Changed a dict value" def test_json5_syntax(): - assert Json5Reader("Hello World", False).js5 == "{ Hello World }", "Automatic addition of '{}' did not work" - js = Json5Reader("Hello\nWorld\rHei\n\rHo\r\nHi", auto=False) + 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]}" assert js.js5 == "{ Hello World Hei Ho Hi }", "Automatic replacement of newlines did not work" assert js.line(-1)[-1] == "}", "Ending '}' expected" assert js.line(3) == "Hei", "Quote of line 3 wrong" assert js.line(5) == "Hi", "Quote of line 5 wrong" - js = Json5Reader("Hello 'W\norld'", 0).js5 - assert Json5Reader("Hello 'W\norld'", 0).js5[10] == "\n", "newline within quotations should not be replaced" - assert Json5Reader("He'llo 'Wo'rld'", 0).js5 == "{ He'llo 'Wo'rld' }", "Handling of single quotes not correct" + js = Json5("Hello 'W\norld'", 0).js5 + assert Json5("Hello 'W\norld'", 0).js5[10] == "\n", "newline within quotations should not be replaced" + assert Json5("He'llo 'Wo'rld'", 0).js5 == "{ He'llo 'Wo'rld' }", "Handling of single quotes not correct" assert ( - len(Json5Reader("Hello World //added a EOL comment", 0).js5) == len("Hello World //added a EOL comment") + 4 + len(Json5("Hello World //added a EOL comment", 0).js5) == len("Hello World //added a EOL comment") + 4 ), "Length of string not conserved when replacing comment" - assert Json5Reader("Hello//EOL comment", 0).js5 == "{ Hello }", "Comment not properly replaced" - assert Json5Reader("Hello#EOL comment", 0).js5 == "{ Hello }", "Comment not properly replaced" + assert Json5("Hello//EOL comment", 0).js5 == "{ Hello }", "Comment not properly replaced" + assert Json5("Hello#EOL comment", 0).js5 == "{ Hello }", "Comment not properly replaced" raw = """{spec: { dp:1.5, #'com1' dr@0.9 : 10, # "com2" }}""" - js = Json5Reader(raw) + js = Json5(raw) assert js.comments == {28: "#'com1'", 61: '# "com2"'}, "Comments not extracted as expected" assert js.js_py["spec"]["dp"] == 1.5, "Comments not properly removed" - js = Json5Reader("Hello /*Line1\nLine2\n..*/..", 0) + js = Json5("Hello /*Line1\nLine2\n..*/..", 0) assert js.js5 == "{ Hello .. }", "Incorrect multi-line comment" - assert Json5Reader("{'Hi':1, Ho:2}").js_py == {"Hi": 1.0, "Ho": 2.0}, "Simple dict expected. Second key without '" - assert Json5Reader("{'Hello:@#%&/=?World':1}").to_py() == { + assert Json5("{'Hi':1, Ho:2}").js_py == {"Hi": 1.0, "Ho": 2.0}, "Simple dict expected. Second key without '" + assert Json5("{'Hello:@#%&/=?World':1}").to_py() == { "Hello:@#%&/=?World": 1 }, "Literal string keys should handle any character, including':' and comments" - js = Json5Reader("{Start: {\n 'H':1,\n 99:{'e':11,'l':12}},\nLast:999}") + js = Json5("{Start: {\n 'H':1,\n 99:{'e':11,'l':12}},\nLast:999}") assert js.to_py() == {"Start": {"H": 1, "99": {"e": 11, "l": 12}}, "Last": 999}, "Dict of dict dict expected" - assert Json5Reader("{'H':1, 99:['e','l','l','o']}").js_py == { + assert Json5("{'H':1, 99:['e','l','l','o']}").js_py == { "H": 1, "99": ["e", "l", "l", "o"], }, "List as value expected" - js = Json5Reader("{'H':1, 99:['e','l','l','o'], 'W':999}") + js = Json5("{'H':1, 99:['e','l','l','o'], 'W':999}") assert list(js.js_py.keys()) == ["H", "99", "W"], "Additional or missing main object elements" with pytest.raises(AssertionError) as err: - Json5Reader("{ H : 1,2}") + Json5("{ H : 1,2}") assert str(err.value).startswith("Json5 read error at 1(10): No proper key:value:") - js = Json5Reader( + js = Json5( "{ spec: {\n stopTime : '3',\n bb.h : '10.0',\n bb.v : '[0.0, 1.0, 3.0]',\n bb.e : '0.7',\n }}" ) # print(js.js5) with pytest.raises(AssertionError) as err: - js = Json5Reader( + js = Json5( "{ spec: {\n stopTime : 3\n bb.h : '10.0',\n bb.v : '[0.0, 1.0, 3.0]',\n bb.e : '0.7',\n }}" ) assert str(err.value).startswith("Json5 read error at 3(19): Key separator ':' in value") with pytest.raises(AssertionError) as err: - js = Json5Reader("{spec: {\n da_dt : [0,0,0,0], dp_dt : 0 db_dt : 0 v : [0,0,0,0],}}") + js = Json5("{spec: {\n da_dt : [0,0,0,0], dp_dt : 0 db_dt : 0 v : [0,0,0,0],}}") assert str(err.value).startswith("Json5 read error at 2(28): Found ':'") + expected = {"Path": "C:/Users/eis/Documents/Projects/Simulation_Model_Assurance/case_study/tests/test_json5.py"} + assert Json5("{Path:'" + str(Path(__file__).as_posix()) + "'}").js_py == expected, str(expected) -def test_json5_write(): +def test_write(): js1 = {"key1": 1.0, "key2": "a string", "key3": ["a", "list", "including", "numbers", 9.9, 1]} expected = "{key1:1.0,key2:'a string',key3:['a','list','including','numbers',9.9,1]}" - assert json5_write(js1, pretty_print=False) == expected, "Simple JSON5 dict" + js = Json5(str(js1)) + assert js.write(pretty_print=False) == expected, "Simple JSON5 dict" js2 = { "key1": 1.0, @@ -83,14 +238,54 @@ def test_json5_write(): ], } expected = "{key1:1.0,key2:'a string',key3:['a','list','including','numbers',9.9,1,'+ object',{hello:1,World:2,dict:{hi:2.1,ho:2.2}}]}" - assert json5_write(js2, pretty_print=False) == expected, "Json5 with object within list" + js = Json5(str(js2)) + assert js.write(pretty_print=False) == expected, "Json5 with object within list" - txt = json5_write(js2, pretty_print=True) + txt = js.write(pretty_print=True) assert len(txt) == 189, "Length of pretty-printed JSON5" print(txt) +def test_results_header(): + js_txt = ( + """{Header : { + case : 'base', + dateTime : '""" + + time.asctime(time.localtime(12345)) + + """', + cases : 'BouncingBall', + file : '""" + + Path(__file__).as_posix() + + """', + casesDate : '""" + + time.asctime(time.localtime(123456)) + + """', + timeUnit : 'second', + timeFactor : 1000000000.0}, }""" + ) + # print(js_txt) + header = Json5(js_txt).js_py + assert header["Header"]["case"] == "base" + assert header["Header"]["dateTime"] == "Thu Jan 1 04:25:45 1970" + assert ( + header["Header"]["file"] + == "C:/Users/eis/Documents/Projects/Simulation_Model_Assurance/case_study/tests/test_json5.py" + ) + + def test_read_cases(): bb_cases = Path(__file__).parent.joinpath("data/BouncingBall0/BouncingBall.cases") - js = Json5Reader(bb_cases) + js = Json5(bb_cases) assert js.js_py["name"] == "BouncingBall" + + +if __name__ == "__main__": + retcode = pytest.main(["-rA", "-v", __file__]) + assert retcode == 0, f"Non-zero return code {retcode}" + # test_json5_syntax() + # test_results_header() + # rfc = _rfc9535_example() + # test_jpath(rfc) + # test_update(rfc) + # test_results_header() + # test_write() diff --git a/tests/test_jsonpath-ng.py b/tests/test_jsonpath-ng.py new file mode 100644 index 0000000..d2f6f6c --- /dev/null +++ b/tests/test_jsonpath-ng.py @@ -0,0 +1,81 @@ +import pytest +from jsonpath_ng.ext import parse +from jsonpath_ng.ext.filter import Expression, Filter +from jsonpath_ng.jsonpath import Child, Descendants, Fields, Index, Root, Slice, This + + +@pytest.mark.parametrize( + "string, parsed", + [ + # The authors of all books in the store + ( + "$.store.book[*].author", + Child( + Child(Child(Child(Root(), Fields("store")), Fields("book")), Slice()), + Fields("author"), + ), + ), + # + # All authors + ("$..author", Descendants(Root(), Fields("author"))), + # + # All things in the store + ("$.store.*", Child(Child(Root(), Fields("store")), Fields("*"))), + # + # The price of everything in the store + ( + "$.store..price", + Descendants(Child(Root(), Fields("store")), Fields("price")), + ), + # + # The third book + ("$..book[2]", Child(Descendants(Root(), Fields("book")), Index(2))), + # + # The last book in order + # "$..book[(@.length-1)]" # Not implemented + ("$..book[-1:]", Child(Descendants(Root(), Fields("book")), Slice(start=-1))), + # + # The first two books + # "$..book[0,1]" # Not implemented + ("$..book[:2]", Child(Descendants(Root(), Fields("book")), Slice(end=2))), + # + # Filter all books with an ISBN + ( + "$..book[?(@.isbn)]", + Child( + Descendants(Root(), Fields("book")), + Filter([Expression(Child(This(), Fields("isbn")), None, None)]), + ), + ), + # + # Filter all books cheaper than 10 + ( + "$..book[?(@.price<10)]", + Child( + Descendants(Root(), Fields("book")), + Filter([Expression(Child(This(), Fields("price")), "<", 10)]), + ), + ), + # + # All members of JSON structure + ("$..*", Descendants(Root(), Fields("*"))), + ], +) +def test_goessner_examples(string, parsed): + """ + Test Stefan Goessner's `examples`_ + + .. _examples: https://goessner.net/articles/JsonPath/index.html#e3 + """ + assert parse(string, debug=True) == parsed + + +def test_attribute_and_dict_syntax(): + """Verify that attribute and dict syntax result in identical parse trees.""" + + assert parse("$.store.book[0].title") == parse("$['store']['book'][0]['title']") + + +if __name__ == "__main__": + retcode = pytest.main(["-rA", "-v", __file__]) + assert retcode == 0, f"Non-zero return code {retcode}" diff --git a/tests/test_results.py b/tests/test_results.py new file mode 100644 index 0000000..862d2c6 --- /dev/null +++ b/tests/test_results.py @@ -0,0 +1,40 @@ +from datetime import datetime +from pathlib import Path + +import pytest +from case_study.case import Cases, Results + + +def test_init(): + # init through existing file + file = Path.cwd().parent / "data" / "BouncingBall3D" / "test_results.js5" + print("FILE", file) + res = Results(file=file) + assert res.res.jspath("$.header.file", Path, True).exists() + assert res.res.jspath("$.header.dateTime", datetime, True).isoformat() == "1924-01-14T00:00:00" + assert res.res.jspath("$.header.casesDate", datetime, True).isoformat() == "1924-01-13T00:00:00" + # init making a new file + cases = Cases(Path.cwd().parent / "data" / "BouncingBall3D" / "BouncingBall3D.cases") + case = cases.case_by_name("base") + res = Results(case=case) + assert res.res.jspath("$.header.file", Path, True).exists() + assert isinstance(res.res.jspath("$.header.dateTime", datetime, True).isoformat(), str) + assert isinstance(res.res.jspath("$.header.casesDate", datetime, True).isoformat(), str) + + +def test_add(): + cases = Cases(Path.cwd().parent / "data" / "BouncingBall3D" / "BouncingBall3D.cases") + case = cases.case_by_name("base") + res = Results(case=case) + res._header_transform(tostring=True) + res.add(0.0, 0, 0, (6,), (9.81,)) + # print( res.res.write( pretty_print=True)) + assert res.res.jspath("$['0.0'].bb.g") == 9.81 + + +if __name__ == "__main__": + retcode = pytest.main(["-rA", "-v", __file__]) + assert retcode == 0, f"Non-zero return code {retcode}" + # os.chdir(Path(__file__).parent.absolute() / "test_working_directory") + # test_init() + # test_add() diff --git a/tests/test_run_bouncingball0.py b/tests/test_run_bouncingball0.py index 3236501..a95fa3d 100644 --- a/tests/test_run_bouncingball0.py +++ b/tests/test_run_bouncingball0.py @@ -1,8 +1,8 @@ from math import sqrt from pathlib import Path -import pytest import numpy as np +import pytest from case_study.case import Case, Cases from case_study.simulator_interface import SimulatorInterface @@ -161,7 +161,8 @@ def test_run_cases(): print( "Run base", ) - res = cases.run_case("base", "results_base") + cases.run_case("base", "results_base") + res = cases.case_by_name("base").results.res # key results data for base case h0 = res[0.01]["bb"]["h"][0] t0 = sqrt(2 * h0 / 9.81) # half-period time with full restitution @@ -173,7 +174,8 @@ def test_run_cases(): cases.simulator.reset() print("Run restitution") - res = cases.run_case("restitution", "results_restitution") + cases.run_case("restitution", "results_restitution") + res = cases.case_by_name("restitution").results.res assert expect_bounce_at(res, sqrt(2 * h0 / 9.81), eps=0.02), f"No bounce at {sqrt(2*h0/9.81)}" assert expect_bounce_at( res, sqrt(2 * h0 / 9.81) + 0.5 * v_max / 9.81, eps=0.02 @@ -186,8 +188,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}" - diff --git a/tests/test_run_mobilecrane.py b/tests/test_run_mobilecrane.py index d575a9f..b06dea0 100644 --- a/tests/test_run_mobilecrane.py +++ b/tests/test_run_mobilecrane.py @@ -3,7 +3,7 @@ import pytest from case_study.case import Cases -from case_study.json5 import Json5Reader +from case_study.json5 import Json5 from case_study.simulator_interface import SimulatorInterface from libcosimpy.CosimEnums import CosimExecutionState from libcosimpy.CosimExecution import CosimExecution @@ -30,7 +30,7 @@ def is_nearly_equal(x: float | list, expected: float | list, eps: float = 1e-10) def test_read_cases(): path = Path(Path(__file__).parent, "data/MobileCrane/MobileCrane.cases") assert path.exists(), "System structure file not found" - json5 = Json5Reader(path) + json5 = Json5(path) assert "# lift 1m / 0.1sec" in list(json5.comments.values()) # for e in json5.js_py: # print(f"{e}: {json5.js_py[e]}") @@ -124,7 +124,7 @@ def initial_settings(): path = Path(Path(__file__).parent, "data/MobileCrane/MobileCrane.cases") assert path.exists(), "Cases file not found" - spec = Json5Reader(path).js_py + spec = Json5(path).js_py # print("SPEC", json5_write( spec, None, True)) expected_spec = {"spec": ["T@step", "x_pedestal@step", "x_boom@step", "x_load@step"]} @@ -229,7 +229,7 @@ def test_run_cases(): path = Path(Path(__file__).parent, "data/MobileCrane/MobileCrane.cases") # system_structure = Path(Path(__file__).parent, "data/MobileCrane/OspSystemStructure.xml") assert path.exists(), "MobileCrane cases file not found" - cases = Cases(path, results_print_type="names") + cases = Cases(path) # for v, info in cases.variables.items(): # print(v, info) static = cases.case_by_name("static") @@ -240,14 +240,16 @@ def test_run_cases(): assert static.act_get[-1][3].args == (0, 0, (53, 54, 55)) print("Running case 'base'...") - res = cases.run_case("base", dump="results_base") + cases.run_case("base", dump="results_base") + res = cases.case_by_name("base").results.res # ToDo: expected Torque? assert is_nearly_equal(res[1.0]["mobileCrane"]["x_pedestal"], [0.0, 0.0, 3.0]) # assert is_nearly_equal(res[1.0]["mobileCrane"]["x_boom"], [8, 0.0, 3], 1e-5) # assert is_nearly_equal(res[1.0]["mobileCrane"]["x_load"], [8, 0, 3.0 - 1e-6], 1e-5) - cases = Cases(path, results_print_type="names") - res = cases.run_case("static", dump="results_static") + cases = Cases(path) + cases.run_case("static", dump="results_static") + res = cases.case_by_name("static").results.res print("RES(1.0)", res[1.0]["mobileCrane"]) assert is_nearly_equal(res[1.0]["mobileCrane"]["x_pedestal"], [0.0, 0.0, 3.0]) print(f"x_load: {res[1.0]['mobileCrane']['x_load']} <-> {[0, 8/sqrt(2),0]}") diff --git a/tests/test_run_simpletable.py b/tests/test_run_simpletable.py index 0b41e1c..abe648b 100644 --- a/tests/test_run_simpletable.py +++ b/tests/test_run_simpletable.py @@ -1,5 +1,6 @@ from pathlib import Path +import pytest from case_study.case import Cases @@ -11,3 +12,8 @@ def test_run_casex(): _ = cases.case_by_name("case1") _ = cases.case_by_name("caseX") print("RESULTS", cases.run_case("caseX", "results")) + + +if __name__ == "__main__": + retcode = pytest.main(["-rA", "-v", __file__]) + assert retcode == 0, f"Return code {retcode}" diff --git a/tests/test_simulator_interface.py b/tests/test_simulator_interface.py index b8a7c48..95a758d 100644 --- a/tests/test_simulator_interface.py +++ b/tests/test_simulator_interface.py @@ -121,3 +121,8 @@ def test_simulator_instantiated(): assert simulator.get_variables(0) == simulator.get_variables("bb"), "Two ways of accessing variables" assert simulator.get_variables(0, "h"), {"h": {"reference": 1, "type": 0, "causality": 2, "variability": 4}} assert simulator.get_variables(0, 1), {"h": {"reference": 1, "type": 0, "causality": 2, "variability": 4}} + + +if __name__ == "__main__": + retcode = pytest.main(["-rA", "-v", __file__]) + assert retcode == 0, f"Return code {retcode}"