diff --git a/.github/workflows/_code_quality.yml b/.github/workflows/_code_quality.yml index a036e77..8f21bdb 100644 --- a/.github/workflows/_code_quality.yml +++ b/.github/workflows/_code_quality.yml @@ -78,6 +78,6 @@ jobs: python-version: "3.12.8" python-version-file: "pyproject.toml" - name: Install the project - run: uv sync --upgrade + run: uv sync --upgrade --extra tests - name: Run mypy run: uv run mypy diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index b4e4612..9e2efc8 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -13,9 +13,9 @@ jobs: - runner: windows-latest # - runner: macos-latest python: - - version: '3.10' - version: '3.11' - version: '3.12' + - version: '3.13' steps: - uses: actions/checkout@v4 - name: Install uv @@ -28,7 +28,7 @@ jobs: with: python-version: ${{ matrix.python.version }} - name: Install the project - run: uv sync --upgrade -p ${{ matrix.python.version }} --no-dev --extra modelTest + run: uv sync --upgrade -p ${{ matrix.python.version }} --no-dev --extra tests - name: Install pytest run: | uv pip install pytest diff --git a/.github/workflows/_test_future.yml b/.github/workflows/_test_future.yml index cbc6c0c..134d28d 100644 --- a/.github/workflows/_test_future.yml +++ b/.github/workflows/_test_future.yml @@ -1,10 +1,10 @@ -name: Unit Tests (py313) -# Test also with Python 3.13 (experimental; workflow will not fail on error.) +name: Unit Tests (py314) +# Test also with Python 3.14 (experimental; workflow will not fail on error.) on: workflow_call jobs: - test313: + test314: name: Test on ${{matrix.python.version}}-${{matrix.platform.runner}} (experimental) continue-on-error: true runs-on: ${{ matrix.platform.runner }} @@ -14,8 +14,8 @@ jobs: - runner: ubuntu-latest - runner: windows-latest python: - - version: '3.13.0-alpha - 3.13.0' - uvpy: '3.13' + - version: '3.14.0-alpha - 3.14.0' + uvpy: '3.14' steps: - uses: actions/checkout@v4 - name: Install uv @@ -28,7 +28,7 @@ jobs: with: python-version: ${{ matrix.python.version }} - name: Install the project - run: uv sync --upgrade -p ${{ matrix.python.uvpy }} --no-dev --extra modelTest + run: uv sync --upgrade -p ${{ matrix.python.uvpy }} --no-dev --extra tests - name: Install pytest run: | uv pip install pytest diff --git a/docs/_ext/get_from_code.py b/docs/_ext/get_from_code.py index 37ac467..182d8f5 100644 --- a/docs/_ext/get_from_code.py +++ b/docs/_ext/get_from_code.py @@ -51,7 +51,7 @@ def run(self): par = nodes.paragraph(text=text) if self.typ is None: # need to parse the extracted text parNode = nodes.paragraph(text="") # use an empty paragraph node as parent - self.state.nested_parse(par, 0, parNode) # here the content of the retrieved text is parsed + self.state.nested_parse(par, 0, parNode) # type: ignore #here the content of the retrieved text is parsed else: # use the text as is parNode = par return [parNode] diff --git a/docs/source/component-development-process.rst b/docs/source/component-development-process.rst new file mode 100644 index 0000000..3826f74 --- /dev/null +++ b/docs/source/component-development-process.rst @@ -0,0 +1,82 @@ +******************************************************* +Guideline on how to develop a FMU using component-model +******************************************************* + +The development process follows the steps + +#. Develop a functional model using Python (>=3.10.) as a Python class. Called *basic model* here. +#. Thoroughly test the basic model. +#. Define the FMU interface using component-model functions +#. Build the FMU calling `Model.build()`, optionally overwriting optional argument of the model class. +#. Test the FMU standalone, e.g. using the `FMPy` package or in conjunction with other FMUs using the `sim-explorer` package. + +Develop a basic model +===================== +Define a functional model as a Python class. We refer to this model as the *basic model*. +At this stage the emerging model class does not need to refer to the `component-model` package. +In fact it is **not recommended** to derive the basic model from `Model`class of component-model. +The basic model might import any Python package (e.g. numpy), as needed to satisfy the functionality. + +Testing the basic model +======================= +The basic model should be thoroughly tested. +This cannot be emphasised too much, as test possibilities and feadback is limited in the FMU domain, +while Python offers proper test and debugging facilities. + + +Defining the FMU interface +========================== +A FMU interface must be added to the model prior to package the model as FMU. This concerns basically + +* component model parameters, i.e. settings which can be changed prior to a simulation run, but are typically constant during the run +* component model input variables, i.e. variables which can be changed based on the output from other component models +* component model output variables, i.e. results which are provided to any connected component model, or the system. + +Defining the interface is done like + +.. code-block:: Python + + class _FMU(Model, ): + def __init__(self, .__init__() + + +Virtual derivatives +------------------- +Running component models in scenarios it is often necessary to change variables during the simulation run. +As in the reality it is often not a good idea to step values in huge steps, as this resembles a 'hammer stroke', +which the system might not tolerate. The simulation dynamics does often not handle such a situation properly either. +It is therefore often necessary to ramp up or down values to the desired final values, +i.e. changing the derivative of the input variable to a non-zero value +until the desired value of the parent variable is reached and then setting the derivative back to zero. + +It is cumbersome to introduce derivative variables for every parent variable which might be changed during the simulation. +Therefore. component-model introduces the concept of *virtual derivatives*, +which are derivative interface variables which are not linked to variables in the basic model. +When defining such variables and setting them to non-zero values, +the parent variable is changed with the given slope at every time step, +i.e. + +` += d/dt * ` + +where `d/dt` is the non-zero derivative value (slope). + +In practical terms, virtual derivatives are defined using FMI structured variables syntax: + +.. code-block:: Python + + Variable( name='der()', causality='input', variability='continuous', ...) + +Explicit specification of the arguments `owner` and `local_name` should be avoided. +Specification of `local_name` changes the virtual derivative into a non-virtual derivative, +i.e. the variable is expected to exist in the basic model. +The `on_step` argument is automatically set to change the parent variable at every time step if the derivative is non-zero. +Explicitly overwriting the automatic `on_step` function is allowed at one's own expense. + + +Building the FMU +================ + +Testing the FMU +=============== \ No newline at end of file diff --git a/docs/source/component-model.pptx b/docs/source/component-model.pptx index bda9710..cca11cf 100644 Binary files a/docs/source/component-model.pptx and b/docs/source/component-model.pptx differ diff --git a/examples/BouncingBallStructure.xml b/examples/BouncingBallStructure.xml new file mode 100644 index 0000000..eb1b995 --- /dev/null +++ b/examples/BouncingBallStructure.xml @@ -0,0 +1,9 @@ + + 0.0 + 0.1 + + + + + + \ No newline at end of file diff --git a/examples/DrivingForce.fmu b/examples/DrivingForce.fmu new file mode 100644 index 0000000..35768c2 Binary files /dev/null and b/examples/DrivingForce.fmu differ diff --git a/examples/ForcedOscillator.xml b/examples/ForcedOscillator.xml index 79b6700..47a1fa3 100644 --- a/examples/ForcedOscillator.xml +++ b/examples/ForcedOscillator.xml @@ -1,15 +1,31 @@ 0.0 0.01 + fixedStep - - - - + + + + + + + + + 0.99 + 0.0001 + 0.00001 + 0.01 + 0.2 + 1.5 + 0.2 + 0.15 + 1e-6 + 1e-6 + \ No newline at end of file diff --git a/examples/ForcedOscillator6D.xml b/examples/ForcedOscillator6D.xml new file mode 100644 index 0000000..f730641 --- /dev/null +++ b/examples/ForcedOscillator6D.xml @@ -0,0 +1,31 @@ + + 0.0 + 0.01 + fixedStep + + + + + + + + + + + + + + + + 0.99 + 0.0001 + 0.00001 + 0.01 + 0.2 + 1.5 + 0.2 + 0.15 + 1e-6 + 1e-6 + + \ No newline at end of file diff --git a/examples/HarmonicOscillator.fmu b/examples/HarmonicOscillator.fmu new file mode 100644 index 0000000..2bf2cbb Binary files /dev/null and b/examples/HarmonicOscillator.fmu differ diff --git a/examples/HarmonicOscillator6D.fmu b/examples/HarmonicOscillator6D.fmu new file mode 100644 index 0000000..f2f67bc Binary files /dev/null and b/examples/HarmonicOscillator6D.fmu differ diff --git a/examples/TimeTableStructure.xml b/examples/TimeTableStructure.xml index b1617ef..32e8cfd 100644 --- a/examples/TimeTableStructure.xml +++ b/examples/TimeTableStructure.xml @@ -1,6 +1,7 @@ 0.0 0.1 + fixedStep diff --git a/examples/bouncing_ball_3d.py b/examples/bouncing_ball_3d.py index 29593af..2e05770 100644 --- a/examples/bouncing_ball_3d.py +++ b/examples/bouncing_ball_3d.py @@ -130,7 +130,7 @@ def _interface(self, name: str, start: str | float | tuple) -> Variable: variability="continuous", initial="exact", start=start, - rng=((0, "100 m"), None, (0, "10 m")), + rng=((0, "100 m"), None, (0, "39.371 inch")), ) elif name == "speed": return Variable( diff --git a/examples/driving_force_fmu.py b/examples/driving_force_fmu.py index 41a9c19..d8b4b7f 100644 --- a/examples/driving_force_fmu.py +++ b/examples/driving_force_fmu.py @@ -1,8 +1,6 @@ import logging -from collections.abc import Callable from functools import partial -from math import pi, sin -from typing import Any +from typing import Any, Callable import numpy as np @@ -15,27 +13,40 @@ # Note: PythonFMU (which component-model is built on) works on files and thus only one Model class allowed per file -def func(time: float, ampl: float = 1.0, omega: float = 0.1): - return np.array((0, 0, ampl * sin(omega * time)), float) +def func(time: float, ampl: np.ndarray, omega: np.ndarray, d_omega: np.ndarray): + """Generate a harmonic oscillating force function. + Optionally it is possible to linearly change the angular frequency as omega + d_omega*time. + The function is intended to be initialized through partial, so that only 'time' is left as variable. + """ + if all(_do == 0.0 for _do in d_omega): + return ampl * np.sin(omega * time) + else: + return ampl * np.sin((omega + d_omega * time) * time) class DrivingForce(Model): """A driving force in 3 dimensions which produces an ouput per time and can be connected to the oscillator. + Note1: the FMU model is made directly (without a basic python class model), which is not recommended! + Note2: the speed of the connected oscillator is added as additional connector. + Since the driving is forced, the input speed is ignored, but it is needed for the ECCO algorithm (power bonds). + Note: This completely replaces DrivingForce (do_step and other functions are not re-used). Args: func (callable)=func: The driving force function f(t). - Note: The func can currently not really be handled as parameter and must be hard-coded here (see above). - Soon to come: Model.build() function which honors parameters, such that function can be supplied from - outside and the FMU can be re-build without changing the class. + ampl (float|tuple) = 1.0: the amplitude of the (sinusoidal) driving force. Same for all D if float. + Optional with units. + freq (float|tuple) = 1.0: the frequency of the (sinusoidal) driving force. Same for all D if float. + Optional with units. + d_freq (float) = 0.0: Optional frequency change per time unit (for frequency sweep experiments). """ def __init__( self, - func: Callable[..., Any] = func, - ampl: float = 1.0, - freq: float = 1.0, + ampl: float | tuple[float] | tuple[str] = 1.0, + freq: float | tuple[float] | tuple[str] = 1.0, + d_freq: float | tuple[float] | tuple[str] = 0.0, **kwargs: Any, ): super().__init__( @@ -44,23 +55,54 @@ def __init__( "Siegfried Eisinger", **kwargs, ) - # interface Variables - self._ampl = Variable(self, "ampl", "The amplitude of the force in N", start=ampl) - self._freq = Variable(self, "freq", "The frequency of the force in 1/s", start=freq) + # interface Variables. We define first their values, to help pyright, since the basic model is missing + _ampl = ampl if isinstance(ampl, tuple) else (ampl,) + self.dim = len(_ampl) + _freq = freq if isinstance(freq, tuple) else (freq,) + assert len(_freq) == self.dim, f"ampl and freq are expected of same length. Found {ampl}, {freq}" + _d_freq = d_freq if isinstance(d_freq, tuple) else (d_freq,) * self.dim + assert len(_d_freq) == self.dim, f"d_freq expected as float or has same length as ampl:{ampl}. Found {d_freq}" + self.ampl = np.array((1.0,) * self.dim, float) + self.freq = np.array((1.0,) * self.dim, float) + self.d_freq = np.array((0.0,) * self.dim, float) + self.function = func + self.func: Callable + self.f = np.array((0.0,) * self.dim, float) + self.v_osc = (0.0,) * self.dim + self._ampl = Variable(self, "ampl", "The amplitude of the force in N", start=_ampl) + self._freq = Variable(self, "freq", "The frequency of the force in 1/s", start=_freq) + self._d_freq = Variable(self, "d_freq", "Change of frequency of the force in 1/s**2", start=_d_freq) self._f = Variable( self, "f", "Output connector for the driving force f(t) in N", causality="output", variability="continuous", - start=np.array((0, 0, 0), float), + start=(0.0,) * self.dim, + ) + self._v_osc = Variable( + self, + "v_osc", + "Input connector for the speed of the connected element in m/s", + causality="input", + variability="continuous", + start=(0.0,) * self.dim, ) def do_step(self, current_time: float, step_size: float): - self.f = self.func(current_time) + self.f = self.func(current_time + step_size) return True # very important! def exit_initialization_mode(self): """Set internal state after initial variables are set.""" - self.func = partial(func, ampl=self.ampl, omega=2 * pi * self.freq) # type: ignore[reportAttributeAccessIssue] - logger.info(f"Initial settings: ampl={self.ampl}, freq={self.freq}") # type: ignore[reportAttributeAccessIssue] + assert isinstance(self.ampl, np.ndarray) + assert isinstance(self.freq, np.ndarray) + assert isinstance(self.d_freq, np.ndarray) + + self.func = partial( + self.function, + ampl=np.array(self.ampl, float), + omega=np.array(2 * np.pi * self.freq, float), # type: ignore # it is an ndarray! + d_omega=np.array(2 * np.pi * self.d_freq, float), # type: ignore # it is an ndarray! + ) + logger.info(f"Initial settings: ampl={self.ampl}, freq={self.freq}, d_freq={self.d_freq}") diff --git a/examples/new_pythonfmu_features.py b/examples/new_pythonfmu_features.py deleted file mode 100644 index b554982..0000000 --- a/examples/new_pythonfmu_features.py +++ /dev/null @@ -1,62 +0,0 @@ -from pythonfmu.enums import Fmi2Status - -from component_model import Model, Variable - - -class NewFeatures(Model): - """Dummy model to test new features of component-model and pythonfmu. - - * logger messages to the user - + handle __init__ parameters - + translate assert statements to logger messages - + translate Exceptions to logger messages - + allow class as argument to .build, instead of the source code file - """ - - def __init__(self, i: int = 1, f: float = 9.9, s: str = "Hello", **kwargs): - super().__init__( - "NewFeatures", - "Dummy model for testing new features in PythonFMU", - "Siegfried Eisinger", - default_experiment={"startTime": 0, "stopTime": 9, "stepSize": 1}, - **kwargs, - ) - print("NAME", self, self.name) - self._i = Variable( - self, - "i", - "My integer", - typ=int, - causality="output", - variability="discrete", - initial="exact", - start=i, - rng=(0, 10), - ) - - self._f = Variable(self, "f", "My float", causality="input", variability="continuous", start=f) - - self._s = Variable(self, "s", "My string", typ=str, causality="parameter", variability="fixed", start=s) - - self.log("This is a __init__ debug message", debug=True) - # self.log("This is a FATAL __init__ message", status=Fmi2Status.fatal, category="logStatusFatal", debug=False) - - def do_step(self, time: int | float, dt: int | float): - super().do_step(time, dt) - self.i += 1 - self.f = time - assert self.f == time # assert without message, but comment - # assert self.i < 8, "The range check would detect that with the next get message" - # send log messages of all types. OSP makes them visible according to log_output_level setting - self.log(f"do_step@{time}. logAll", status=Fmi2Status.ok, category="logAll", debug=True) - self.log(f"do_step@{time}. logStatusWarning", Fmi2Status.warning, "logStatusWarning", True) - self.log(f"do_step@{time}. logStatusDiscard", Fmi2Status.discard, "logStatusDiscard", True) - self.log(f"do_step@{time}. logStatusError", Fmi2Status.error, "logStatusError", True) - # self.log(f"do_step@{time}. logStatusFatal", Fmi2Status.fatal, "logStatusFatal", True) - if time > 8: - self.log(f"@{time}. Trying to terminate simulation", Fmi2Status.error, "logStatusError", True) - return False - return True - - def exit_initialization_mode(self): - print(f"My initial variables: i:{self.i}, f:{self.f}, s:{self.s}") diff --git a/examples/new_pythonfmu_features2.py b/examples/new_pythonfmu_features2.py deleted file mode 100644 index b49ad30..0000000 --- a/examples/new_pythonfmu_features2.py +++ /dev/null @@ -1,61 +0,0 @@ -from pythonfmu.enums import Fmi2Status - -from component_model import Model, Variable - - -class NewFeatures(Model): - """Dummy model to test new features of component-model and pythonfmu. - - * logger messages to the user - + handle __init__ parameters - + translate assert statements to logger messages - + translate Exceptions to logger messages - + allow class as argument to .build, instead of the source code file - """ - - def __init__(self, i: int = 1, f: float = 9.9, s: str = "Hello", **kwargs): - super().__init__( - "NewFeatures", - "Dummy model for testing new features in PythonFMU", - "Siegfried Eisinger", - default_experiment={"startTime": 0, "stopTime": 9, "stepSize": 1}, - **kwargs, - ) - self._i = Variable( - self, - "i", - "My integer", - typ=int, - causality="output", - variability="discrete", - initial="exact", - start=i, - rng=(0, 10), - ) - - self._f = Variable(self, "f", "My float", causality="input", variability="continuous", start=f) - - self._s = Variable(self, "s", "My string", typ=str, causality="parameter", variability="fixed", start=s) - - self.log("This is a __init__ debug message", debug=True) - # self.log("This is a FATAL __init__ message", status=Fmi2Status.fatal, category="logStatusFatal", debug=False) - - def do_step(self, time, dt): - super().do_step(time, dt) - self.i += 1 - self.f = time - assert self.f == time # assert without message, but comment - # assert self.i < 8, "The range check would detect that with the next get message" - # send log messages of all types. OSP makes them visible according to log_output_level setting - self.log(f"do_step@{time}. logAll", status=Fmi2Status.ok, category="logAll", debug=True) - self.log(f"do_step@{time}. logStatusWarning", Fmi2Status.warning, "logStatusWarning", True) - self.log(f"do_step@{time}. logStatusDiscard", Fmi2Status.discard, "logStatusDiscard", True) - self.log(f"do_step@{time}. logStatusError", Fmi2Status.error, "logStatusError", True) - self.log(f"do_step@{time}. logStatusFatal", Fmi2Status.fatal, "logStatusFatal", True) - if time > 8: - self.log(f"@{time}. Trying to terminate simulation", Fmi2Status.error, "logStatusError", True) - return False - return True - - def exit_initialization_mode(self): - print(f"My initial variables: i:{self.i}, f:{self.f}, s:{self.s}") diff --git a/examples/new_pythonfmu_features3.py b/examples/new_pythonfmu_features3.py deleted file mode 100644 index 6b7e254..0000000 --- a/examples/new_pythonfmu_features3.py +++ /dev/null @@ -1,65 +0,0 @@ -from pythonfmu.enums import Fmi2Status - -from component_model import Model, Variable - - -class NewFeatures(Model): - """Dummy model to test new features of component-model and pythonfmu. - - * logger messages to the user - + handle __init__ parameters - + translate assert statements to logger messages - + translate Exceptions to logger messages - + allow class as argument to .build, instead of the source code file - """ - - def __init__(self, i: int = 1, f: float = 9.9, s: str = "Hello", **kwargs): - super().__init__( - "NewFeatures", - "Dummy model for testing new features in PythonFMU", - "Siegfried Eisinger", - default_experiment={"startTime": 0, "stopTime": 9, "stepSize": 1}, - ) - self._i = Variable( - self, - "i", - "My integer", - typ=int, - causality="output", - variability="discrete", - initial="exact", - start=i, - rng=(0, 10), - ) - - self._f = Variable(self, "f", "My float", causality="input", variability="continuous", start=f) - - self._s = Variable(self, "s", "My string", typ=str, causality="parameter", variability="fixed", start=s) - - self.log("This is a __init__ debug message", debug=True) - # self.log("This is a FATAL __init__ message", status=Fmi2Status.fatal, category="logStatusFatal", debug=False) - - def do_step(self, time, dt): - super().do_step(time, dt) - self.i += 1 - self.f = time - assert self.f == time # assert without message, but comment - # assert self.i < 8, "The range check would detect that with the next get message" - # send log messages of all types. OSP makes them visible according to log_output_level setting - self.log(f"do_step@{time}. logAll", status=Fmi2Status.ok, category="logAll", debug=True) - self.log(f"do_step@{time}. logStatusWarning", Fmi2Status.warning, "logStatusWarning", True) - self.log(f"do_step@{time}. logStatusDiscard", Fmi2Status.discard, "logStatusDiscard", True) - self.log(f"do_step@{time}. logStatusError", Fmi2Status.error, "logStatusError", True) - self.log(f"do_step@{time}. logStatusFatal", Fmi2Status.fatal, "logStatusFatal", True) - if time > 8: - self.log(f"@{time}. Trying to terminate simulation", Fmi2Status.error, "logStatusError", True) - return False - return True - - def exit_initialization_mode(self): - print(f"My initial variables: i:{self.i}, f:{self.f}, s:{self.s}") - - -class NewFeatures2(Model): - def do_step(self, time, dt): - return True diff --git a/examples/new_pythonfmu_features4.py b/examples/new_pythonfmu_features4.py deleted file mode 100644 index 68a57b1..0000000 --- a/examples/new_pythonfmu_features4.py +++ /dev/null @@ -1,2 +0,0 @@ -class NewFeatures(object): - pass diff --git a/examples/oscillator.py b/examples/oscillator.py index 6e9fd6c..6fbf3b1 100644 --- a/examples/oscillator.py +++ b/examples/oscillator.py @@ -23,31 +23,48 @@ class Oscillator: Args: k (tuple)=(1.0, 1.0, 1.0): spring constant in N/m. May vary in 3D c (tuple)=(0.0, 0.0, 0.0): Viscous damping coefficient in N.s/m. May vary in 3D - m (float)=1.0: Mass of the spring load (spring mass negligible) in kg + m (float | tuple)=1.0: Mass of the spring load (spring mass negligible) in kg. Same in all dim. if float tolerance (float)=1e-5: Optional tolerance in m, i.e. maximum uncertainty in displacement x. """ def __init__( self, - k: tuple[float, float, float] | tuple[str, str, str] = (1.0, 1.0, 1.0), # type str for FMU option - c: tuple[float, float, float] | tuple[str, str, str] = (0.0, 0.0, 0.0), - m: float = 1.0, + k: tuple[float, ...] | tuple[str, ...] = (1.0, 1.0, 1.0), # type str for FMU option + c: tuple[float, ...] | tuple[str, ...] = (0.0, 0.0, 0.0), + m: float | tuple[float, ...] = 1.0, tolerance: float = 1e-5, + f_func: Callable | None = None, ): + self.dim = len(k) self.k = np.array(k, float) self.c = np.array(c, float) - self.m = m + if isinstance(m, float): + self.m = np.array((m,) * self.dim, float) + else: + self.m = np.array(m, float) self.tolerance = tolerance - self.x = np.array((0, 0, 0), float) - self.v = np.array((0, 0, 0), float) - self.f = np.array((0, 0, 0), float) + self.x = np.array((0,) * self.dim, float) + self.v = np.array((0,) * self.dim, float) + self.f = np.array((0,) * self.dim, float) # standard ODE matrix (homogeneous system): - self.ode = [np.array(((-self.c[i] / self.m, -self.k[i] / self.m), (1, 0)), float) for i in range(3)] + self.ode = [ + np.array(((-self.c[i] / self.m[i], -self.k[i] / self.m[i]), (1, 0)), float) for i in range(self.dim) + ] + self.f_func = f_func - def ode_func(self, t: float, y: np.ndarray, i: int, f: float) -> np.ndarray: + def ode_func( + self, + t: float, # scalar time + y: np.ndarray, # combined array of position and speed for component i + i: int, # dimension + f: float, + ) -> np.ndarray: # force for component i res = self.ode[i].dot(y) - if f != 0: - res += np.array((f, 0), float) + if self.f_func is None: + if f != 0: + res += np.array((f, 0), float) + else: + res += np.array((self.f_func(t)[i], 0), float) return res def do_step(self, current_time: float, step_size: int | float) -> bool: @@ -55,14 +72,14 @@ def do_step(self, current_time: float, step_size: int | float) -> bool: We implement a very simplistic algoritm based on difference calculus. """ - for i in range(3): # this is a 3D oscillator - if self.x[i] != 0 or self.v[i] != 0 or self.f[i] != 0: + for i in range(self.dim): # this is a xD oscillator + if self.x[i] != 0 or self.v[i] != 0 or self.f[i] != 0 or self.f_func is not None: y0 = np.array([self.v[i], self.x[i]], float) sol = integrate.solve_ivp( fun=self.ode_func, t_span=[current_time, current_time + step_size], y0=y0, - args=(i, self.f[i]), # dimension and force as extra arguments to fun + args=(i, self.f[i]), # axis and force as extra arguments to fun atol=self.tolerance, ) self.x[i] = sol.y[1][-1] @@ -71,10 +88,10 @@ def do_step(self, current_time: float, step_size: int | float) -> bool: @property def period(self): - """Calculate the natural period of the oscillator (without damping an).""" + """Calculate the natural period of the oscillator (without damping).""" w2 = [] - for i in range(3): - w2i = self.k[i] / self.m - (self.c[i] / 2 / self.m) ** 2 + for i in range(self.dim): + w2i = self.k[i] / self.m[i] - (self.c[i] / 2 / self.m[i]) ** 2 if w2i > 0: w2.append(2 * np.pi / np.sqrt(w2i)) else: diff --git a/examples/oscillator_6d.py b/examples/oscillator_6d.py new file mode 100644 index 0000000..6fbf3b1 --- /dev/null +++ b/examples/oscillator_6d.py @@ -0,0 +1,114 @@ +from typing import Callable + +import numpy as np +from scipy import integrate + + +class Oscillator: + """Construct a simple model of a general harmonic oscillator, potentially driven by a force. + + The system obeys the equation F(t) - k*x - c*dx/dt = m*d^2x/dt^2 + See also `Wikipedia `_ + + where x shall be a 3D vector with an initial position. F(t)=0 as long as there is not external driving force. + NOTE: This is the basic oscillator model. + + * Unrelated with Model and Variable + * FMU cannot be built from that! See oscillator_fmu where this is extended to an FMU package. + * The HarmonicOscillator can be used for model testing (highly recomended, see test_oscillator.py) + * It can of cause be used stand-alone + + We use the scipy.integrate.solve_ivp algorithm for the integration (see do_step) + + Args: + k (tuple)=(1.0, 1.0, 1.0): spring constant in N/m. May vary in 3D + c (tuple)=(0.0, 0.0, 0.0): Viscous damping coefficient in N.s/m. May vary in 3D + m (float | tuple)=1.0: Mass of the spring load (spring mass negligible) in kg. Same in all dim. if float + tolerance (float)=1e-5: Optional tolerance in m, i.e. maximum uncertainty in displacement x. + """ + + def __init__( + self, + k: tuple[float, ...] | tuple[str, ...] = (1.0, 1.0, 1.0), # type str for FMU option + c: tuple[float, ...] | tuple[str, ...] = (0.0, 0.0, 0.0), + m: float | tuple[float, ...] = 1.0, + tolerance: float = 1e-5, + f_func: Callable | None = None, + ): + self.dim = len(k) + self.k = np.array(k, float) + self.c = np.array(c, float) + if isinstance(m, float): + self.m = np.array((m,) * self.dim, float) + else: + self.m = np.array(m, float) + self.tolerance = tolerance + self.x = np.array((0,) * self.dim, float) + self.v = np.array((0,) * self.dim, float) + self.f = np.array((0,) * self.dim, float) + # standard ODE matrix (homogeneous system): + self.ode = [ + np.array(((-self.c[i] / self.m[i], -self.k[i] / self.m[i]), (1, 0)), float) for i in range(self.dim) + ] + self.f_func = f_func + + def ode_func( + self, + t: float, # scalar time + y: np.ndarray, # combined array of position and speed for component i + i: int, # dimension + f: float, + ) -> np.ndarray: # force for component i + res = self.ode[i].dot(y) + if self.f_func is None: + if f != 0: + res += np.array((f, 0), float) + else: + res += np.array((self.f_func(t)[i], 0), float) + return res + + def do_step(self, current_time: float, step_size: int | float) -> bool: + """Do one simulation step of size dt. + + We implement a very simplistic algoritm based on difference calculus. + """ + for i in range(self.dim): # this is a xD oscillator + if self.x[i] != 0 or self.v[i] != 0 or self.f[i] != 0 or self.f_func is not None: + y0 = np.array([self.v[i], self.x[i]], float) + sol = integrate.solve_ivp( + fun=self.ode_func, + t_span=[current_time, current_time + step_size], + y0=y0, + args=(i, self.f[i]), # axis and force as extra arguments to fun + atol=self.tolerance, + ) + self.x[i] = sol.y[1][-1] + self.v[i] = sol.y[0][-1] + return True # to keep the signature when moving to FMU + + @property + def period(self): + """Calculate the natural period of the oscillator (without damping).""" + w2 = [] + for i in range(self.dim): + w2i = self.k[i] / self.m[i] - (self.c[i] / 2 / self.m[i]) ** 2 + if w2i > 0: + w2.append(2 * np.pi / np.sqrt(w2i)) + else: + w2.append(float("nan")) # critically or over-damped. There is no period + return w2 + + +class Force: + """A driving force in 3 dimensions which produces an ouput per time and can be connected to the oscillator. + + Args: + func (callable)=lambda t:np.array( (0,0,0), float): A function of t, producing a 3D vector + """ + + def __init__(self, func: Callable): + self.func = func + self.out = np.array((0, 0, 0), float) + + def do_step(self, current_time: float, step_size: float): + self.out = self.func(current_time) diff --git a/examples/oscillator_6dof_fmu.py b/examples/oscillator_6dof_fmu.py new file mode 100644 index 0000000..1f0b304 --- /dev/null +++ b/examples/oscillator_6dof_fmu.py @@ -0,0 +1,112 @@ +import logging +from typing import Any + +import numpy as np + +from component_model.model import Model +from component_model.variable import Variable +from examples.oscillator import Oscillator + +logger = logging.getLogger(__name__) + + +class HarmonicOscillator6D(Model, Oscillator): # refer to Model first! + """General harmonic oscillator with 6 DoF, extended for FMU packaging. + + The system obeys the equations + F(t) - k*x - c*dx/dt = m*d^2x/dt^2 (first 3 equations, one per linear dimension) + with F: external force, x: displacement and other symbols and units as defined below. + See also `Harmonic_oscillator `_ + and + T(t) - k*a - c*da/dt = I* d^a2/dt^2 + with T: external torque, a: deflection angle and other symbols and units as defined below. + See also `Torsion_spring `_ + Torque, the angles and all parameters are in cartesian pseudo vectors, + i.e. a torque in z-direction implies a right-handed turn in the x-y plane. + All parameters may vary along the 3 spatial dimensions. + + See also test_oscillator_fmu() where build process and further testing is found. + For details see oscillator.py and test_oscillator.py, where the basic model is defined and tested. + + Args: + k (tuple[float] | tuple[str])=(1.0,)*6: spring/torsion constants in N/m or N.m/rad + c (tuple[float] | tuple[str])=(0.0,)*6: Viscous damping coefficients in N.s/m or N.m.s/rad + m (float)=1.0: Mass of the spring load (spring mass negligible) in kg + mi (tuple[float] | tuple[str])=(1.0,)*3: Moments of inertia in kg.m^2 + tolerance (float)=1e-5: Optional tolerance in m, i.e. maximum uncertainty in displacement x. + x0 (tuple) = (0.0)*6: Start position in m or rad + v0 (tuple) = (0.0)*6: Start speed in m/s or rad/s + """ + + def __init__( + self, + k: tuple[float, ...] | tuple[str, ...] = (1.0,) * 6, + c: tuple[float, ...] | tuple[str, ...] = (0.1,) * 6, + m: float | str = 1.0, + mi: tuple[float, ...] | tuple[str, ...] = (1.0,) * 3, + tolerance: float = 1e-5, + x0: tuple[float, ...] | tuple[str, ...] = (0.0,) * 6, + v0: tuple[float, ...] | tuple[str, ...] = (0.0,) * 6, + **kwargs: Any, + ): + Model.__init__( + self, # here we define a few standard entries for FMU + name="HarmonicOscillator", + description="3D harmonic oscillator prepared for FMU packaging. 3D driving force can be connected", + author="Siegfried Eisinger", + version="0.2", + default_experiment={"startTime": 0.0, "stopTime": 10.0, "stepSize": 0.01, "tolerance": 1e-5}, + **kwargs, + ) + # include arguments to get the dimension right! + Oscillator.__init__(self, (1.0,) * 6, (0.1,) * 6, (1.0,) * 6, tolerance=1e-3, f_func=None) + # interface Variables. + # Note that the Variable object is accessible as self._, while the value is self. + self._k = Variable(self, "k", "The 6D spring constant in N/m or N.m/rad", start=k) + self._c = Variable(self, "c", "The 6D spring damping in in N.s/m or N.m.s/rad", start=c) + self._m = Variable(self, "m", "The mass at end of spring in kg", start=(m, m, m, mi[0], mi[1], mi[2])) + self._x = Variable( + self, + "x", + "The time-dependent 6D generalized position of the mass in m or rad", + causality="output", + variability="continuous", + initial="exact", + start=x0, + ) + self._v = Variable( + self, + "v", + "The time-dependent 6D generalized speed of the mass in m/s or rad/s", + causality="output", + variability="continuous", + initial="exact", + start=v0, + ) + self._f = Variable( + self, + "f", + "Input connector for the 6D external force acting on the mass in N or N.m", + causality="input", + variability="continuous", + start=np.array((0,) * 6, float), + ) + + def do_step(self, current_time: float, step_size: float): + if not Model.do_step(self, current_time, step_size): # some housekeeping functions (not really needed here) + return False + Oscillator.do_step(self, current_time, step_size) # this does the integration itself + return True # very important for the FMU mechanism + + def exit_initialization_mode(self): + """Set internal state after initial variables are set. + + Note: need to update the ODE matrix to reflect changes in c, k or m! + """ + self.ode = [ + np.array(((-self.c[i] / self.m[i], -self.k[i] / self.m[i]), (1, 0)), float) for i in range(self.dim) + ] + logger.info(f"Initial settings: k={self.k}, c={self.c}, m={self.m}, x={self.x}, v={self.v}, f={self.f}") + + # Note: The other FMU functions like .setup_experiment and .exit_initialization_mode + # do not need special attention here and can be left out diff --git a/examples/oscillator_fmu.py b/examples/oscillator_fmu.py index 5771034..144397f 100644 --- a/examples/oscillator_fmu.py +++ b/examples/oscillator_fmu.py @@ -30,12 +30,12 @@ class HarmonicOscillator(Model, Oscillator): # refer to Model first! def __init__( self, - k: tuple[float, float, float] | tuple[str, str, str] = (1.0, 1.0, 1.0), - c: tuple[float, float, float] | tuple[str, str, str] = (0.0, 0.0, 0.0), + k: tuple[float, ...] | tuple[str, ...] = (1.0, 1.0, 1.0), + c: tuple[float, ...] | tuple[str, ...] = (0.0, 0.0, 0.0), m: float | str = 1.0, tolerance: float = 1e-5, - x0: tuple[float, float, float] | tuple[str, str, str] = (1.0, 1.0, 1.0), - v0: tuple[float, float, float] | tuple[str, str, str] = (0.0, 0.0, 0.0), + x0: tuple[float, ...] | tuple[str, ...] = (1.0, 1.0, 1.0), + v0: tuple[float, ...] | tuple[str, ...] = (0.0, 0.0, 0.0), **kwargs: Any, ): Model.__init__( diff --git a/examples/oscillator_xd.py b/examples/oscillator_xd.py new file mode 100644 index 0000000..5a1d2f6 --- /dev/null +++ b/examples/oscillator_xd.py @@ -0,0 +1,145 @@ +from typing import Any, Callable + +import numpy as np +from scipy import integrate + + +class OscillatorXD: + """Construct a simple model of a general harmonic oscillator, potentially driven by a force. + + The system obeys the equation F(t) - k*x - c*dx/dt = m*d^2x/dt^2 + See also `Wikipedia `_ + + where x shall be an xD vector with an initial position and velocity. Common dimentsions are + + * 1: a one dimensional oscillator (e.g. a pendulum) + * 3: a three-dimensional oscillator in 3D space + * 6: a six-dimensional oscillator, represented e.g. by a rigid body mounted on springs. + The generalized position is in this case the 3D cartesian position + 3D angular position + + F(t)=0 as long as there is not external driving force. + NOTE: This is a basic oscillator model. + + * Unrelated with Model and Variable + * FMU cannot be built from that! See oscillator_fmu where this is extended to an FMU package. + * The HarmonicOscillator can be used for model testing (highly recomended, see test_oscillator_xv.py) + * It can of cause be used stand-alone + + We use the scipy.integrate.solve_ivp algorithm for the integration (see do_step) + + Args: + dim (int)=6: dimension of the oscillator + k (float|tuple)=1.0: spring constant in N/m (if x position). May vary in the dimensions if tuple + c (float|tuple)=0.0: Viscous damping coefficient in N.s/m (if x position). May vary in the dimensions if tuple + m (float | tuple)=1.0: Mass of the spring load (spring mass negligible) in kg if x position. + Same in all dim if float. + tolerance (float)=1e-5: Optional tolerance in m, i.e. maximum uncertainty in displacement x. + force (obj): Force object + """ + + def __init__( + self, + dim: int = 6, + k: float | tuple[float, ...] | tuple[str, ...] = 1.0, + c: float | tuple[float, ...] | tuple[str, ...] = 0.0, + m: float | tuple[float, ...] = 1.0, + tolerance: float = 1e-5, + force: Any | None = None, + ): + self.dim = dim + self._m = np.array((m,) * self.dim, float) if isinstance(m, float) else np.array(m, float) + assert len(self._m) == self.dim, f"Expect dimension {self.dim} for mass. Found: {self._m}" + + if isinstance(k, float): + self.w2 = np.array((k,) * self.dim, float) / self._m + elif isinstance(k, tuple): + assert len(k) == self.dim, f"Expect dimension {self.dim} for k. Found: {k}" + self.w2 = np.array(k, float) / self._m + else: + raise ValueError(f"Unhandled type for 'k': {k}") + + if isinstance(c, float): + self.gam = np.array((c / 2.0,) * self.dim, float) / self._m + elif isinstance(c, tuple): + assert len(c) == self.dim, f"Expect dimension {self.dim} for k. Found: {k}" + self.gam = np.array(c, float) / self._m / 2.0 + else: + raise ValueError(f"Unhandled type of 'c': {c}") from None + + self.tolerance = tolerance + self.x = np.array((0,) * 2 * self.dim, float) # generalized position + generalized first derivatives + self.v = self.x[self.dim :] # link to generalized derivatives (for convenience) + self.force = force # the function object + + def ode_func( + self, + t: float, # scalar time + y: np.ndarray, # combined array of positions and speeds + ) -> np.ndarray: # derivative of positions and speeds + if self.force is None: + return np.append(y[self.dim :], -2.0 * self.gam * y[self.dim :] - self.w2 * y[: self.dim]) + else: # explicit force function is defined + f = self.force(t=t, x=y[: self.dim], v=y[self.dim :]) + d_dt = -2.0 * self.gam * y[self.dim :] - self.w2 * y[: self.dim] + f + # print(f"ODE({self.dim})@{t}. f:{f[2]}, z:{y[2]}, v:{y[8]} => d_dt:{d_dt[2]}.") + return np.append(y[self.dim :], d_dt) + + def do_step(self, current_time: float, step_size: int | float) -> bool: + """Do one simulation step of size dt. + + We implement a very simplistic algoritm based on difference calculus. + """ + sol = integrate.solve_ivp( + fun=self.ode_func, + t_span=[current_time, current_time + step_size], + y0=self.x, # use the combined position-velocity array + atol=self.tolerance, + ) + self.x = sol.y[:, -1] + self.v = self.x[self.dim :] # re-link + return True # to keep the signature when moving to FMU + + @property + def period(self): + """Calculate the natural period of the oscillator (without damping).""" + w2 = [] + for i in range(self.dim): + w2i = self.w2[i] - self.gam[i] ** 2 + if w2i > 0: + w2.append(2 * np.pi / np.sqrt(w2i)) + else: + w2.append(float("nan")) # critically or over-damped. There is no period + return w2 + + +class Force: + """A driving force in x dimensions which produces an ouput as function of time and/or position. + Can be connected to the oscillator. + + Args: + dim (int): the dimension of the force (6: 3*linear force + 3*torque + func (callable): Function of agreed arguments (see args) + _args (str): Sequence of 't' (time), 'x' (position), 'v' (velocity), + denoting the force dependencies + """ + + def __init__(self, dim: int, func: Callable): + self.dim = dim + self.func = func + self.current_time = 0.0 + self.dt = 0 + self.out = np.array((0,) * self.dim) + + def __call__(self, **kwargs): + """Calculate the force in dependence on keyword arguments 't', 'x' or 'v'.""" + if "t" in kwargs: + t = kwargs["t"] + if abs(t - self.current_time) < 1e-15: + return self.out + self.dt = t - self.current_time + self.current_time = t + kwargs.update({"dt": self.dt}) # make the step size known to the function + else: + kwargs.update({"t": None, "dt": None}) + self.out = self.func(**kwargs) + return self.out diff --git a/examples/time_table_fmu.py b/examples/time_table_fmu.py index 23737a2..cdae87f 100644 --- a/examples/time_table_fmu.py +++ b/examples/time_table_fmu.py @@ -1,6 +1,8 @@ import logging from typing import Sequence +import numpy as np # noqa + from component_model.model import Model from component_model.variable import Variable from examples.time_table import TimeTable diff --git a/pyproject.toml b/pyproject.toml index d2bc819..2657b35 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ build-backend = "hatchling.build" only-include = [ "src/component_model", "tests", - "tests/examples", + "examples", ".coveragerc", ".editorconfig", "pytest.ini", @@ -22,12 +22,15 @@ packages = [ "examples", ] +[tool.hatch.metadata] +allow-direct-references = true + [project] name = "component-model" version = "0.1.0" description = "Constructs a Functional Mockup Interface component model from a python script (fulfilling some requirements)." readme = "README.rst" -requires-python = ">= 3.10" +requires-python = ">= 3.11" license = { file = "LICENSE" } authors = [ { name = "Siegfried Eisinger", email = "Siegfried.Eisinger@dnv.com" }, @@ -54,30 +57,24 @@ classifiers = [ "Topic :: Software Development :: Libraries :: Python Modules", ] dependencies = [ - "numpy>=1.26,<2.0", - "matplotlib>=3.9.1", - "pint>=0.24", - "sympy>=1.13.3", + "numpy>=2.0", + "pint>=0.24.4", "jsonpath-ng>=1.7.0", - "libcosimpy>=0.0.2", - "pythonfmu>=0.6.7", - "flexparser<0.4", - "pdfminer>=20191125", - "pypdf2>=3.0.1", - "scipy>=1.15.1", + "pythonfmu==0.6.9", + "flexparser>=0.4", ] [project.optional-dependencies] -modelTest = [ - "fmpy==0.3.21", # version 0.3.22 does so far (25.3.25) not workwhen installing through pip - "matplotlib>=3.9.1", -] -rest = [ - "docutils>=0.21", -] -editor = [ - "thonny>=4.1", -] + tests = [ + "fmpy==0.3.21", # version 0.3.22 does so far (25.3.25) not workwhen installing through pip + "scipy>=1.15.1", + "matplotlib>=3.9.1", + "plotly>=6.0.1", +# "libcosimpy>=0.0.5", +# "libcosimpy @ file:///C:/Users/eis/Downloads/libcosimpy-0.0.4-cp312-cp312-win_amd64.whl", + ] + rest = ["docutils>=0.21"] + editor = [] [project.urls] Homepage = "https://github.com/dnv-opensource/component-model" @@ -102,7 +99,12 @@ dev-dependencies = [ "sphinxcontrib-mermaid>=1.0.0", "myst-parser>=4.0", "furo>=2024.8", -] + ] +required-environments = [ + "sys_platform == 'win32'", + "sys_platform == 'linux'", +# "sys_platform == 'darwin'", + ] native-tls = true @@ -113,11 +115,11 @@ plugins = [ mypy_path = "stubs" files = [ "src", - # "tests", - # "demos", + "tests", + "examples", ] exclude = [ - "^src/folder_to_be_excluded/", + "tests/test_working_directory", ] check_untyped_defs = true disable_error_code = [ @@ -134,5 +136,4 @@ include = [ ] exclude = [ "tests/test_working_directory", - "examples/new_pythonfmu_features*", ] diff --git a/pytest.ini b/pytest.ini index 89ecb94..4afd1a3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -1,5 +1,5 @@ [pytest] -testpaths = - tests +testpaths = tests +norecursedirs = tests/osp addopts = --strict-markers --verbose xfail_strict = True diff --git a/src/component_model/__init__.py b/src/component_model/__init__.py index 46ce159..e69de29 100644 --- a/src/component_model/__init__.py +++ b/src/component_model/__init__.py @@ -1,2 +0,0 @@ -from component_model.model import Model # noqa: F401 -from component_model.variable import Variable # noqa: F401 diff --git a/src/component_model/model.py b/src/component_model/model.py index 3a88052..d3c98f7 100644 --- a/src/component_model/model.py +++ b/src/component_model/model.py @@ -7,6 +7,7 @@ from abc import abstractmethod from enum import Enum from math import log +from numbers import Real from pathlib import Path from typing import Generator, Sequence, TypeAlias @@ -20,7 +21,7 @@ from pythonfmu.fmi2slave import FMI2_MODEL_OPTIONS # type: ignore from component_model.enums import ensure_enum -from component_model.variable import Variable +from component_model.variable import Unit, Variable from component_model.variable_naming import ParsedVariable, VariableNamingConvention logger = logging.getLogger(__name__) @@ -71,8 +72,6 @@ class Model(Fmi2Slave): * TypeDefinitions. Instead of defining SimpleType variables, ScalarVariable variables are always based on the pre-defined types and details provided there. - * Explicit (see ) are so far not implemented. - This could be done as an additional (optional) property in Variable. Args: name (str): name of the model. The name is also used to construct the FMU file name. @@ -87,6 +86,11 @@ class Model(Fmi2Slave): Valid keys: startTime,stopTime,stepSize,tolerance guid (str)=None: Unique identifier of the model (supplied or automatically generated) flags (dict)=None: Any of the defined FMI flags with a non-default value (see FMI 2.0.4, Section 4.3.1) + + .. todo:: + + Include support for model units with respect to time, degrees/radians,... + Make sure that such base units are consistently used in the model. """ instances: list[str] = [] @@ -102,7 +106,7 @@ def __init__( copyright: str | None = None, default_experiment: dict[str, float] | None = None, flags: dict | None = None, - guid=None, + guid: str | None = None, **kwargs, ): kwargs.update( @@ -118,8 +122,6 @@ def __init__( self.name = name if "instance_name" not in kwargs: # NOTE: within builder.py this is always changed to 'dummyInstance' kwargs["instance_name"] = self.name # make_instancename(__name__) - if "resources" not in kwargs: - kwargs["resources"] = None super().__init__(**kwargs) # in addition, OrderedDict vars is initialized # Additional variables which are hidden here: .vars, # PythonFMU sets the default_experiment and the following items always to None! Correct it here @@ -138,17 +140,17 @@ def __init__( if guid is not None: self.guid = guid # use a common UnitRegistry for all variables: - self.ureg = UnitRegistry(system=unit_system) + self.ureg: UnitRegistry = UnitRegistry(system=unit_system) self.copyright, self.license = self.make_copyright_license(copyright, license) self.guid = guid if guid is not None else uuid.uuid4().hex # print("FLAGS", flags) variable_naming = kwargs.pop("variable_naming", "structured") self.variable_naming = ensure_enum(variable_naming, VariableNamingConvention.flat) - self._units: dict[str, list] = {} # def units and display units (unitName:conversionFactor). => UnitDefinitions + self._units: list[Unit] = [] # list of all Unit objects defined in the model => UnitDefinitions self.flags = self.check_flags(flags) self._dirty: list = [] # dirty compound variables. Used by (set) during do_step() self.time = self.default_experiment.start_time # keeping track of time when dynamic calculations are performed - self.derivatives: list = [] # list of non-explicit derivatives + self.derivatives: dict = {} # dict of non-explicit derivatives {dername : basevar, ...} def setup_experiment(self, start_time: float = 0.0): """Minimum version of setup_experiment, just setting the start_time. Derived models may need to extend this.""" @@ -159,7 +161,7 @@ def enter_initialization_mode(self): def exit_initialization_mode(self): """Initialize the model after initial variables are set.""" - super().exit_initialization_mode() + # super().exit_initialization_mode() self.dirty_do() # run on_set on all dirty variables @abstractmethod # mark the class as 'still abstract' @@ -169,7 +171,6 @@ def do_step(self, current_time: float, step_size: float) -> bool: """ self.time = current_time self.dirty_do() # run on_set on all dirty variables - for var in self.vars.values(): if var is not None and var.on_step is not None: var.on_step(current_time, step_size) @@ -180,21 +181,12 @@ def _unit_ensure_registered(self, candidate: Variable): To register the units of a compound variable, the whole variable is entered and a recursive call to the underlying display units is made. """ - unit_display = [] for i in range(len(candidate)): - if candidate.display[i] is None: - unit_display.append((candidate.unit[i], None)) - else: - unit_display.append((candidate.unit[i], candidate.display[i])) - # here the actual work is done - for u, du in unit_display: - if u not in self._units: # the main unit is not yet registered - self._units[u] = [] # main unit has no factor - if du is not None: # displays are defined - if not len(self._units[u]) or all( - du[0] not in self._units[u][i][0] for i in range(len(self._units[u])) - ): - self._units[u].append(du) + cu = candidate.unit[i] + for u in self._units: + if cu.u == u.u and cu.du == u.du: # already registered + break + self._units.append(cu) def owner_hierarchy(self, parent: str | None) -> list: """Analyse the parent of a variable down to the Model and return the owners as list.""" @@ -209,10 +201,12 @@ def owner_hierarchy(self, parent: str | None) -> list: elif len(parsed.indices) == 1: idx = parsed.indices[0] else: + logger.critical("Object indices other than 0 and 1D not implement. Found {parsed.indices}") raise NotImplementedError( "Object indices other than 0 and 1D not implement. Found {parsed.indices}" ) from None if parsed.der > 0: + logger.critical("Derivatives are so far not implemented") raise NotImplementedError("Derivatives are so far not implemented") from None ownernames.append((parsed.var, idx)) parent = parsed.parent @@ -246,6 +240,7 @@ def register_variable( # type: ignore [reportIncompatibleMethodOverride] # not assert isinstance(var, Variable), f"Variable object expected here. Found {var}" for idx, v in self.vars.items(): if v is not None and v.name == var.name: + logger.critical(f"Variable {var.name} already used as index {idx} in model {self.name}") raise KeyError(f"Variable {var.name} already used as index {idx} in model {self.name}") from None # ensure that the model has the value as attribute: vref = len(self.vars) @@ -292,15 +287,20 @@ def add_variable(self, *args, **kwargs): """ return Variable(self, *args, **kwargs) - def add_derivative(self, var: Variable, order: int): - """Add the derivative of var to the exposed Variables as virtual variable. + def add_derivative(self, dername: str, basename: str) -> Variable: + """Add the derivative of basename to the dict of virtual derivatives. This is convenient as many physical systems do not tolerate to abruptly change variable values, but require to ramp up/down values by setting the derivative to suitable values. This can be achieved without adding an internal variable to the model. The model will in this case do the ramping when the derivative is set != 0.0. + + Args: + basename (str): the full name of the base variable name, i.e. d basename / dt = dername(t) """ - self.derivatives.append(var) + basevar = self.variable_by_name(basename) + self.derivatives.update({dername: basevar}) + return basevar def variable_by_name(self, name: str) -> Variable: """Return Variable object related to name, or None, if not found. @@ -411,7 +411,7 @@ def build( project_files = [] project_files.append(Path(__file__).parents[0]) - # Make sure the dest path is of type Patch + # Make sure the dest path is of type Path dest = dest if isinstance(dest, Path) else Path(dest) with ( @@ -445,7 +445,7 @@ def build( dest=dest, documentation_folder=doc_dir, newargs=newargs, - ) # , xFunc=None) + ) return asBuilt def to_xml(self, model_options: dict | None = None) -> ET.Element: @@ -523,46 +523,61 @@ def to_xml(self, model_options: dict | None = None) -> ET.Element: def xml_unit_definitions(self): """Make the xml element for the unit definitions used in the model. See FMI 2.0.4 specification 2.2.2.""" defs = ET.Element("UnitDefinitions") - for u in self._units: - ubase = self.ureg(u).to_base_units() - dim = ubase.dimensionality - exponents = {} - for key, value in { - "mass": "kg", - "length": "m", - "time": "s", - "current": "A", - "temperature": "K", - "substance": "mol", - "luminosity": "cd", - }.items(): - if "[" + key + "]" in dim: - exponents.update({value: str(int(dim["[" + key + "]"]))}) - if ( - "radian" in str(ubase.units) - ): # radians are formally a dimensionless quantity. To include 'rad' as specified in FMI standard this dirty trick is used - # udeg = str(ubase.units).replace("radian", "degree") - # print("EXPONENT", ubase.units, udeg, log(ubase.magnitude), log(self.ureg('degree').to_base_units().magnitude)) - exponents.update( - {"rad": str(int(log(ubase.magnitude) / log(self.ureg("degree").to_base_units().magnitude)))} - ) - - unit = ET.Element("Unit", {"name": u}) - base = ET.Element("BaseUnit", exponents) - base.attrib.update({"factor": str(self.ureg(u).to_base_units().magnitude)}) - unit.append(base) - for du in self._units[u]: # list also the displays (if defined) - unit.append( - ET.Element( - "DisplayUnit", - { - "name": du[0], - "factor": str(du[1](1.0)), - "offset": str(du[1](0.0)), - }, + u_done: list[str] = [] + for u in self._units: # all registered unit objects + unit = ET.Element("NoUnit") # dummy element + if u.u not in u_done: # multiple entries are possible if there are multiple display units + ubase = self.ureg(u.u).to_base_units() + dim = ubase.dimensionality + exponents = {} + for key, value in { + "mass": "kg", + "length": "m", + "time": "s", + "current": "A", + "temperature": "K", + "substance": "mol", + "luminosity": "cd", + }.items(): + dim_key = f"[{key}]" + if dim_key not in dim: + continue + dim_value = dim[dim_key] + if not isinstance(dim_value, Real): + logger.debug("Skipping non-real dimensionality entry for %s", dim_key) + continue + exponents.update({value: str(int(float(dim_value)))}) + if ( + "radian" in str(ubase.units) + ): # radians are formally a dimensionless quantity. To include 'rad' as specified in FMI standard this dirty trick is used + # udeg = str(ubase.units).replace("radian", "degree") + # print("EXPONENT", ubase.units, udeg, log(ubase.magnitude), log(self.ureg('degree').to_base_units().magnitude)) + exponents.update( + {"rad": str(int(log(ubase.magnitude) / log(self.ureg("degree").to_base_units().magnitude)))} ) - ) - defs.append(unit) + + unit = ET.Element("Unit", {"name": u.u}) + base = ET.Element("BaseUnit", exponents) + base.attrib.update({"factor": str(self.ureg(u.u).to_base_units().magnitude)}) + unit.append(base) + du_done: list[str] = [] + for _u in self._units: # list also the displays (if defined) + if _u.u not in u_done and u.u == _u.u and _u.du is not None and _u.du not in du_done: + unit.append( + ET.Element( + "DisplayUnit", + { + "name": _u.du, + "factor": str(_u.to_base(1.0) - _u.to_base(0.0)), + "offset": str(_u.to_base(0.0)), + }, + ) + ) + if isinstance(_u.du, str): + du_done.append(_u.du) + u_done.append(u.u) + if unit.tag != "NoUnit": + defs.append(unit) return defs def _xml_default_experiment(self): @@ -615,10 +630,10 @@ def _xml_structure_derivatives(self): ders = ET.Element("Derivatives") for v in filter( - lambda v: v is not None and v.antiderivative() is not None, + lambda v: v is not None and v.primitive() is not None, self.vars.values(), ): - i_a_der = v.antiderivative().value_reference + i_a_der = v.primitive().value_reference for i in range(len(v)): # works for vectors and scalars ders.append( ET.Element( @@ -712,6 +727,7 @@ def vars_iter(self, key=None): yield v else: + logger.critical(f"Unknown iteration key {key} in 'vars_iter'") raise KeyError(f"Unknown iteration key {key} in 'vars_iter'") def ref_to_var(self, vr: int) -> tuple[Variable, int]: @@ -739,6 +755,7 @@ def _vrs_slices(self, vrs: Sequence[int]) -> Generator[tuple[Variable, slice, sl try: test = self.vars[vr] except KeyError as err: + logger.critical(f"valueReference={vr} does not exist in model {self.name}") raise AssertionError(f"valueReference={vr} does not exist in model {self.name}") from err if vr != _vr + 1 or test is not None: # new slice if var is not None: # only if initialized @@ -797,6 +814,7 @@ def _set(self, vrs: Sequence[int], values: Sequence[int | float | bool | str], t var.setter((values[_svr],), idx=_sv) else: # simple Variable var.setter(values[svr], idx=0) + # print(f"{self.name}. Set {vrs}:{values}") def set_integer(self, vrs: Sequence[int], values: Sequence[int]): self._set(vrs, values, int) diff --git a/src/component_model/utils/analysis.py b/src/component_model/utils/analysis.py new file mode 100644 index 0000000..ef0c71f --- /dev/null +++ b/src/component_model/utils/analysis.py @@ -0,0 +1,56 @@ +import logging + +import numpy as np + +logger = logging.getLogger(__name__) + + +def extremum(x: tuple | list | np.ndarray, y: tuple | list | np.ndarray, aerr: float = 0.0): + """Check whether the provided (3) points contain an extremum. + Return 0 (no extremum), -1 (low point), 1 (top point) and the point, or (0,0). + """ + assert len(x) == 3 and len(y) == 3, f"Exactly three points expected. Found {x}, {y}" + a = np.array(((1, x[0], x[0] ** 2), (1, x[1], x[1] ** 2), (1, x[2], x[2] ** 2)), float) + z = np.linalg.solve(a, y) + if (abs(z[1]) < 1e-15 and abs(z[2]) < 1e-15) or abs(z[2]) < abs(z[1] * 1e-15): # very nearly linear. + return (0, (0, 0)) + else: + x0 = -z[1] / 2.0 / z[2] + if x[0] - aerr <= x0 <= x[2] + aerr: # extremum in the range + z0 = z[0] + (z[1] + z[2] * x0) * x0 + if z[2] < 0: + return (1, (x0, z0)) + else: + return (-1, (x0, z0)) + else: + return (0, (0, 0)) + + +def extremum_series(t: tuple | list | np.ndarray, y: tuple | list | np.ndarray, which: str = "max"): + """Estimate the extrema from the time series defined by y(t). + which can be 'max', 'min' or 'all'. + """ + + def w1(x: float): + return x + + def w_1(x: float): + return -x + + def w0(x: float): + return abs(x) + + assert len(t) == len(y) > 2, "Something wrong with lengths of t ({len(t)}) and y ([len(y)})" + if which == "max": + w = w1 + elif which == "min": + w = w_1 + else: + w = w0 + + res = [] + for i in range(1, len(t) - 1): + e, p = extremum(t[i - 1 : i + 2], y[i - 1 : i + 2]) + if e != 0 and w(e) == 1 and p[0] < t[i]: + res.append(p) + return res diff --git a/src/component_model/utils/controls.py b/src/component_model/utils/controls.py new file mode 100644 index 0000000..20f08ba --- /dev/null +++ b/src/component_model/utils/controls.py @@ -0,0 +1,265 @@ +import logging + +import numpy as np + +logger = logging.getLogger(__name__) + + +class Controls(object): + """Keep track of float variable changes. + + * Store and check possible float variable changes, including first and second derivatives + * Set control goals. A goal is either None or a sequence of (time, acceleration) tuples. + In this way an acceleration can be set or the velocity or position can be changed through the step function. + + Args: + names (tuple[str]): Tuple of name strings for the control variables to use. + The names are only used internally an do not need to correlate with outside names and objects + limits: None or tuple of limits one per name. None means 'no limit' for variable(s), order or min/max. + In general the (min,max) is provided for all orders, i.e. 3 tuples of 2-tuples of float per name. + A given order can be fixed through min==max or by providing a single float instead of the tuple. + The sub-orders of a fixed order do not need to be provided and are internally set to (0.0, 0,0) + limit_err: Determines how limit errors are dealt with. + Anything below critical sets the value to the limit and provides a logger message. + Critical leads to a program run error. + """ + + def __init__( + self, + names: tuple[str, ...] = (), + limits: tuple[tuple[tuple[float | None, float | None] | float | None, ...], ...] | None = None, + limit_err: int = logging.WARNING, + ): + self.names = list(names) + self.dim = len(names) + self.goals: list[tuple | None] = [] if self.dim == 0 else [None] * self.dim # Initially no goals set + self.rows = [] if self.dim == 0 else [-1] * self.dim # current row in goal sequence + self.current: list[np.ndarray] = ( + [] if self.dim == 0 else [np.array((0.0, 0.0, 0.0), float) * self.dim] + ) # current positions, speeds, accelerations + self.nogoals: bool = True + self.limit_err = limit_err + self._limits: list[list[tuple[float, float]]] = [] + if isinstance(limits, tuple): + for idx in range(self.dim): + self._limits.append(self._prepare_limits(limits[idx])) + + def _prepare_limits( + self, limits: tuple[tuple[float | None, float | None] | float | None, ...] + ) -> list[tuple[float, float]]: + """Prepare and check 'limits', so that they can be appended to Controls. + + Args: + limits : optional specification of limits for var, d_var/dt and d2_var/dt2 of single float variable: + + * None: Denotes 'no specified limits' => -inf ...inf + * single number: The variable is fixed to this single value + * tuple(min,max): minimum and maximum value + """ + _limits = [(float("-inf"), float("inf"))] * 3 # default limits for value, 1.deriv., 2.deriv + + if limits is None: # default values for all orders + return _limits + + for order in range(3): + if len(limits) <= order: + if _limits[order - 1][0] == _limits[order - 1][1]: # order-1 fixed + _limits[order] = (0.0, 0.0) # derivative fixed zero + else: + raise ValueError(f"Explicit limit needed for order {order} in {limits}.") from None + else: + lim = limits[order] + if lim is None: # use the default value + pass + elif isinstance(lim, (float, int)): # single value provided + assert lim + fixed = float(lim) + _limits[order] = (fixed, fixed) + elif isinstance(lim, tuple): # both values provided + assert len(lim) == 2, f"Need both minimum and maximum. Found {limits[order]}" + if lim[0] is not None and lim[1] is not None: + _limits[order] = (lim[0], lim[1]) + assert _limits[order][0] <= _limits[order][1], f"Wrong order of limits: {limits[order]}" + elif lim[0] is not None: + _limits[order] = (float(lim[0]), float("inf")) + elif lim[1] is not None: + _limits[order] = (float("-inf"), float(lim[1])) + else: + raise ValueError(f"Unknown type of limits[{order}]: {lim}") from None + return _limits + + def append(self, name: str, limits: tuple[tuple[float | None, float | None] | float | None, ...]): + self.names.append(name) + self.dim += 1 + self.goals.append(None) + self.rows.append(-1) + self.current.append(np.array((0.0, 0.0, 0.0), float)) + self._limits.append(self._prepare_limits(limits)) + + def idx(self, name: str) -> int: + """Find index from name.""" + return self.names.index(name) + + def limit(self, ident: int | str, order: int, minmax: int, value: float | None = None) -> float: + """Get/Set the single limit for 'idx', 'order', 'minmax'.""" + idx = ident if isinstance(ident, int) else self.names.index(ident) + assert 0 <= idx < 3, f"Only idx = 0,1,2 allowed. Found {idx}" + assert 0 <= order < 3, f"Only order = 0,1,2 allowed. Found {order}" + assert 0 <= minmax < 2, f"Only minmax = 0,1 allowed. Found {minmax}" + if value is not None: + lim = self._limits[idx][order] + self.limits(idx, order, (value if minmax == 0 else lim[0], value if minmax == 1 else lim[1])) + return self._limits[idx][order][minmax] + + def limits(self, ident: int | str, order: int, value: tuple | None = None) -> tuple[float, float]: + """Get/Set the min/max limit for 'idx', 'order'.""" + idx = ident if isinstance(ident, int) else self.names.index(ident) + assert 0 <= idx < 3, f"Only idx = 0,1,2 allowed. Found {idx}" + assert 0 <= order < 3, f"Only order = 0,1,2 allowed. Found {order}" + if value is not None: + assert value[0] <= value[1], f"Wrong order:{value}" + self._limits[idx][order] = value + return self._limits[idx][order] + + def check_limit(self, ident: int | str, order: int, value: float) -> float | None: + idx = ident if isinstance(ident, int) else self.names.index(ident) + if value < self.limit(idx, order, 0): # check goal value wrt. limits + msg = f"Goal value {value} is below the limit {self.limit(idx, order, 0)}." + if self.limit_err == logging.CRITICAL: + raise ValueError(msg + "Stopping execution.") from None + else: + logger.log(self.limit_err, msg + " Setting value to minimum.") + return self.limit(idx, order, 0) # corrected value + if value > self.limit(idx, order, 1): + msg = f"Goal value {value} is above the limit {self.limit(idx, order, 1)}." + if self.limit_err == logging.CRITICAL: + raise ValueError(msg + "Stopping execution.") from None + else: + logger.log(self.limit_err, msg + " Setting value to maximum.") + return self.limit(idx, order, 1) # corrected value + return value + + def setgoal(self, ident: int | str, order: int, value: float | None, t0: float = 0.0): + """Set a new goal for 'ident', i.e. set the required time-acceleration sequence + to reach value with all derivatives = 0.0. + + Args: + ident (int|str): the identificator of the control element (as integer or name) + order (int): the order 0,1,2 of the goal to be set + value (float|None): the goal value (acceleration, velocity or position) to be reached. + None to unset the goal. + t0 (float): the current time + """ + idx = ident if isinstance(ident, int) else self.names.index(ident) + # check the index, the order and the value with respect to limits + if not 0 <= idx < 3: + raise ValueError(f"Only idx = 0,1,2 allowed. Found {idx}") from None + if not 0 <= order < 3: + raise ValueError(f"Only order = 0,1,2 allowed. Found {order}") from None + # assert value is None or self.goals[idx] is None, "Change of goals is currently not implemented." + if value is None: # unset goal + self.goals[idx] = None + else: + value = self.check_limit(idx, order, value) + # print(f"SET {idx}, {order}: {value}. Current:{current}. Limits:{self.limits(idx, order)}") + if ( + ( + order == 0 and abs(self.current[idx][0] - value) < 1e-13 + ) # (adjusted) position goal is already reached + or (order == 2 and value == 0.0) + ): # zero acceleration requested + self.goals[idx] = None + elif order == 2: # set the acceleration from now and 'forever' + self.goals[idx] = ((float("inf"), value),) + elif order == 1: # accelerate to another velocity and keep that 'forever' + _speed = self.current[idx][1] + acc = self.limit(idx, 2, int(_speed < value)) # maximum acceleration or deceleration + self.goals[idx] = ((t0 + (value - _speed) / acc, acc), (float("inf"), 0.0)) + elif order == 0: # sequence of acceleration and deceleration to reach a new position + _pos = self.current[idx][0] + if abs(self.current[idx][1]) > 1e-12: # the initial velocity is not zero. Need to decelerate + v0 = self.current[idx][1] + a = self.limit(idx, 2, int(bool(-np.sign(v0) + 1))) + goal0 = (t0, a) + t0 += -v0 / a + _pos = -v0 * v0 / 2 / a # updated position when the velocity is zero + else: + goal0 = None + acc1 = self.limit(idx, 2, int(_pos < value)) # maximum acceleration on first leg + acc2 = self.limit(idx, 2, int(_pos > value)) # maximum acceleration on last leg + if acc1 == 0 or acc2 == 0: + _acc = np.sign(int(_pos < value) + 1) * float("inf") + else: + _acc = 0.5 * (1.0 / acc1 - 1.0 / acc2) + vmax = self.limit(idx, 1, int(_pos < value)) # maximum velocity towards goal + dx1_dx3 = vmax**2 * _acc + dx2 = value - _pos - dx1_dx3 + if np.sign(value - _pos) != np.sign(dx2): # maximum velocity is not reached + v1 = np.sign(value - _pos) * np.sqrt(_acc * (value - _pos)) + dt1 = v1 / acc1 + dt2 = -v1 / acc2 + if goal0 is None: + self.goals[idx] = ((t0 + dt1, acc1), (t0 + dt1 + dt2, acc2), (float("inf"), 0.0)) + else: + self.goals[idx] = (goal0, (t0 + dt1, acc1), (t0 + dt1 + dt2, acc2), (float("inf"), 0.0)) + else: + dt1 = vmax / acc1 + dt2 = dx2 / vmax + dt3 = -vmax / acc2 + if goal0 is None: + self.goals[idx] = ( + (t0 + dt1, acc1), + (t0 + dt1 + dt2, 0.0), + (t0 + dt1 + dt2 + dt3, acc2), + (float("inf"), 0.0), + ) + else: + self.goals[idx] = ( + goal0, + (t0 + dt1, acc1), + (t0 + dt1 + dt2, 0.0), + (t0 + dt1 + dt2 + dt3, acc2), + (float("inf"), 0.0), + ) + logger.info(f"New goals: {self.goals}") + self.nogoals = all(x is None for x in self.goals) + self.rows[idx] = -1 if self.goals[idx] is None else 0 # set start row + + def getgoal(self, ident: int | str) -> tuple: + idx = ident if isinstance(ident, int) else self.names.index(ident) + assert 0 <= idx < 3, f"Only idx = 0,1,2 allowed. Found {idx}" + return (self.current[idx], self.goals[idx]) + + def step(self, time: float, dt: float): + """Step towards the goals (if goals are set).""" + if not self.nogoals: + for idx in range(self.dim): + goals = self.goals[idx] + if goals is not None: + _current = self.current[idx] + + _t, _current[2] = goals[self.rows[idx]] + while time > _t: # move row so that it starts in the right time-acc row + self.rows[idx] += 1 + _t, _current[2] = goals[self.rows[idx]] + while dt > 0: + if time + dt < _t: # covers the whole + _current[0] = self.check_limit( + idx, 0, _current[0] + _current[1] * dt + 0.5 * _current[2] * dt * dt + ) + _current[1] = self.check_limit(idx, 1, _current[1] + _current[2] * dt) + dt = 0 + else: # dt must be split + dt1 = _t - time + _current[0] = self.check_limit( + idx, 0, _current[0] + _current[1] * dt1 + 0.5 * _current[2] * dt1 * dt1 + ) + _current[1] = self.check_limit(idx, 1, _current[1] + _current[2] * dt1) + time = _t + dt -= dt1 + self.rows[idx] += 1 + _t, _current[2] = goals[self.rows[idx]] + + if np.isinf(_t) and abs(_current[2]) < 1e-12 and abs(_current[1]) < 1e-12: + self.goals[idx] = None + self.nogoals = all(x is None for x in self.goals) diff --git a/src/component_model/utils/transform.py b/src/component_model/utils/transform.py new file mode 100644 index 0000000..ddb292e --- /dev/null +++ b/src/component_model/utils/transform.py @@ -0,0 +1,217 @@ +import logging +from math import isinf + +import numpy as np +from scipy.spatial.transform import Rotation as Rot + +logger = logging.getLogger(__name__) + + +# Utility functions for handling special variable types +def spherical_to_cartesian(vec: np.ndarray | tuple, deg: bool = False) -> np.ndarray: + """Turn spherical vector 'vec' (defined according to ISO 80000-2 (r,polar,azimuth)) into cartesian coordinates.""" + if deg: + theta = np.radians(vec[1]) + phi = np.radians(vec[2]) + else: + theta = vec[1] + phi = vec[2] + sinTheta = np.sin(theta) + cosTheta = np.cos(theta) + sinPhi = np.sin(phi) + cosPhi = np.cos(phi) + r = vec[0] + return np.array((r * sinTheta * cosPhi, r * sinTheta * sinPhi, r * cosTheta)) + + +def cartesian_to_spherical(vec: np.ndarray | tuple, deg: bool = False) -> np.ndarray: + """Turn the vector 'vec' given in cartesian coordinates into spherical coordinates. + (defined according to ISO 80000-2, (r, polar, azimuth)). + """ + r = np.linalg.norm(vec) + if vec[0] == vec[1] == 0: + if vec[2] == 0: + return np.array((0, 0, 0), float) + else: + return np.array((r, 0, 0), float) + elif deg: + return np.array((r, np.degrees(np.arccos(vec[2] / r)), np.degrees(np.arctan2(vec[1], vec[0]))), float) + else: + return np.array((r, np.arccos(vec[2] / r), np.arctan2(vec[1], vec[0])), float) + + +def cartesian_to_cylindrical(vec: np.ndarray | tuple, deg: bool = False) -> np.ndarray: + """Turn the vector 'vec' given in cartesian coordinates into cylindrical coordinates. + (defined according to ISO, (r, phi, z), with phi right-handed wrt. x-axis). + """ + phi = np.arctan2(vec[1], vec[0]) + if deg: + phi = np.degrees(phi) + return np.array((np.sqrt(vec[0] * vec[0] + vec[1] * vec[1]), phi, vec[2]), float) + + +def cylindrical_to_cartesian(vec: np.ndarray | tuple, deg: bool = False) -> np.ndarray: + """Turn cylinder coordinate vector 'vec' (defined according to ISO (r,phi,z)) into cartesian coordinates. + The angle phi is measured with respect to x-axis, right hand. + """ + phi = np.radians(vec[1]) if deg else vec[1] + return np.array((vec[0] * np.cos(phi), vec[0] * np.sin(phi), vec[2]), float) + + +def euler_rot_spherical( + rpy: tuple | list | Rot, + vec: tuple | list | None = None, + seq: str = "XYZ", # sequence of axis of rotation as defined in scipy Rotation object + degrees: bool = False, +) -> tuple | list | np.ndarray: + """Rotate the spherical vector vec using the Euler angles (yaw,pitch,roll). + + Args: + rpy (Sequence|Rotation): The sequence of (yaw,pitch,roll) Euler angles or the pre-calculated Rotation object + vec: (Sequence): the spherical vector to be rotated + None: Use unit vector in z-direction, i.e. (1,0,0) + 2-sequence: only polar and azimuth provided and returned => (polar,azimuth) + 3-sequence: (r,polar,azimuth) + seq (str) = 'XYZ': Sequence of rotations as defined in scipy.spatial.transform.Rotation.from_euler() + degrees (bool): angles optionally provided in degrees. Default: radians + Returns: + The rotated vector in spherical coordinates (radius only if 3-vector is provided) + """ + if isinstance(rpy, Rot): + r = rpy + elif isinstance(rpy, (tuple, list, np.ndarray)): + r = Rot.from_euler(seq, (rpy[0], rpy[1], rpy[2]), degrees) # 0: roll, 1: pitch, 2: yaw + else: + logger.critical(f"Unknown object {rpy} to rotate") + raise NotImplementedError(f"Unknown object {rpy} to rotate") from None + radius = 0.0 # avoid spurious unbound error + if vec is None: + tp = [0.0, 0.0] + else: # explicit vector provided + if len(vec) == 3: + radius = vec[0] + tp = list(vec[1:]) + else: + tp = list(vec) + if degrees: + tp = [np.radians(x) for x in tp] + st = np.sin(tp[0]) + # rotate the cartesian vector (r is definitely not a list, even if pyright might think so) + x = r.apply((st * np.cos(tp[1]), st * np.sin(tp[1]), np.cos(tp[0]))) # type: ignore[reportAttributeAccessIssue] + x2 = x[2] + if abs(x2) < 1.0: + pass + elif abs(x2 - 1.0) < 1e-10: + x2 = 1.0 + elif abs(x2 + 1.0) < 1e-10: + x2 = -1.0 + else: + logger.critical(f"Invalid argument {x2} for arccos calculation") + raise ValueError(f"Invalid argument {x2} for arccos calculation") from None + if abs(x[0]) < 1e-10 and abs(x[1]) < 1e-10: # define the normally undefined arctan + phi = 0.0 + else: + phi = np.arctan2(x[1], x[0]) + if vec is not None and len(vec) == 3: + return (radius, np.arccos(x2), phi) # return the spherical vector + else: + return (np.arccos(x2), phi) # return only the direction + + +def rot_from_spherical(vec: tuple | list | np.ndarray, degrees: bool = False): + """Return a scipy Rotation object from the spherical coordinates vec, + i.e. the rotation which turns a vector along the z-axis into vec. + + Args: + vec (Sequence | np.ndarray): a spherical vector as 3D or 2D (radius omitted) + degrees (bool): optional possibility to provide angles in degrees + """ + angle = vec[1:] if len(vec) == 3 else vec + return Rot.from_rotvec((0.0, 0.0, angle[1]), degrees) * Rot.from_rotvec((0.0, angle[0], 0.0), degrees) + + +def rot_from_vectors(vec1: np.ndarray, vec2: np.ndarray): + """Find the rotation object which rotates vec1 into vec2. Lengths of vec1 and vec2 shall be equal.""" + n = np.linalg.norm(vec1) + assert abs(n - np.linalg.norm(vec2)) < 1e-10, f"Vectors len({vec1}={n} != len{vec2}. Cannot rotate into each other" + if abs(n - 1.0) > 1e-10: + vec1 /= n + vec2 /= n + _c = vec1.dot(vec2) # type: ignore + if abs(_c + 1.0) < 1e-10: # vectors are exactly opposite to each other + imax, vmax, _sum = (-1, float("-inf"), 0.0) + for k, v in enumerate(vec1): + if isinf(vmax) or abs(v) > abs(vmax): + imax, vmax = (k, v) + _sum += v + vec = np.zeros(3) + vec[imax] = -(_sum - vmax) / vmax + vec[imax + 1 if imax < 2 else 0] = 0.0 + i_remain = imax + 2 if imax < 1 else 1 + vec[i_remain] = np.sqrt(1.0 / (1 + (vec1[i_remain] / vmax) ** 2)) + return Rot.from_rotvec(np.pi * vec) + else: + x = np.cross(vec1, vec2) + vx = np.array([[0, -x[2], x[1]], [x[2], 0, -x[0]], [-x[1], x[0], 0]], float) + # print(vec1, vec2, _c, x,'\n',vx,'\n',np.matmul(vx,vx)) + return Rot.from_matrix(np.identity(3) + vx + np.matmul(vx, vx) / (1 + _c)) + + +def spherical_unique(vec: np.ndarray, eps: float = 1e-10) -> np.ndarray: + if len(vec) == 3: + if abs(vec[0]) < eps: + return np.array((0, 0, 0), float) + elif vec[0] < 0: + return np.append(-vec[0], spherical_unique(np.array((vec[1] + np.pi, vec[2]), float))) + else: + return np.append(vec[0], spherical_unique(vec[1:], eps)) + else: + if abs(vec[0]) < eps: + return np.array((0, 0), float) + elif 0 <= vec[0] <= np.pi and 0 <= vec[1] <= 2 * np.pi: + return vec + # angles not in unique range + theta = vec[0] + phi = vec[1] + _2pi = np.pi + np.pi + while theta > np.pi: + theta -= _2pi + while theta < -np.pi: + theta += _2pi + if theta < 0: + theta = -theta + phi += np.pi + while phi < 0: + phi += _2pi + while phi >= _2pi: + phi -= _2pi + return np.array((theta, phi), float) + + +def quantity_direction(quantity_direction: tuple, spherical: bool = False, deg: bool = False) -> np.ndarray: + """Turn a 4-tuple, consisting of quantity (float) and a direction 3-vector to a direction 3-vector, + where the norm denotes the direction and the length denotes the quantity. + The return vector is always a cartesian vector. + + Args: + quantity_direction (tuple): a 4-tuple consisting of the desired length of the resulting vector (in standard units (m or m/s)) + and the direction 3-vector (in standard units) + spherical (bool)=False: Optional possibility to provide the input direction vector in spherical coordinates + deg (bool)=False: Optional possibility to provide the input angle (of spherical coordinates) in degrees. Only relevant if spherical=True + """ + if quantity_direction[0] < 1e-15: + return np.array((0, 0, 0), float) + if spherical: + direction = spherical_to_cartesian(quantity_direction[1:], deg) # turn to cartesian coordinates, if required + else: + direction = np.array(quantity_direction[1:], float) + n = np.linalg.norm(direction) # normalize + return quantity_direction[0] / n * direction + + +def normalized(vec: np.ndarray): + """Return the normalized vector. Helper function.""" + assert len(vec) == 3, f"{vec} should be a 3-dim vector" + norm = np.linalg.norm(vec) + assert norm > 0, f"Zero norm detected for vector {vec}" + return vec / norm diff --git a/src/component_model/variable.py b/src/component_model/variable.py index dbe5dd5..f90fe43 100644 --- a/src/component_model/variable.py +++ b/src/component_model/variable.py @@ -4,11 +4,10 @@ import xml.etree.ElementTree as ET # noqa: N817 from enum import Enum, IntFlag from functools import partial -from math import acos, atan2, cos, degrees, radians, sin, sqrt from typing import Any, Callable, Sequence, TypeAlias import numpy as np -from pint import Quantity # management of units +from pint import Quantity, UnitRegistry # management of units from pythonfmu.enums import Fmi2Causality as Causality # type: ignore from pythonfmu.enums import Fmi2Initial as Initial # type: ignore from pythonfmu.enums import Fmi2Variability as Variability # type: ignore @@ -48,8 +47,170 @@ class Check(IntFlag): all = 3 -def linear(x: float, b: float, a: float = 0.0): - return a + b * x +class Unit: + """Helper class to store and manage units and display units, + i.e. base unit of variable and unit differences 'outside' and 'inside' the model. + + One Unit object represents one scalar variable. + """ + + def __init__(self): + self.u = "" # unit as string (placeholder) + self.du = None # display unit (default: same as u, no transformation) + self.to_base = partial(Unit.identity) # ensure a definition + self.from_base = partial(Unit.identity) # ensure a definition + + def __str__(self): + txt = f"Unit {self.u}, display:{self.du}" + if self.du is not None: + txt += f". Offset:{self.to_base(0)}, factor:{self.to_base(1.0) - self.to_base(0.0)}" + return txt + + def parse_quantity(self, quantity: PyType, ureg: UnitRegistry, typ: type | None = None) -> PyType: + """Parse the provided quantity in terms of magnitude and unit, if provided as string. + If another type is provided, dimensionless units are assumed. + + Args: + quantity (PyType): the quantity to disect. Should be provided as string, but also the trivial cases (int,float,Enum) are allowed. + A free string should not be used and leads to a warning + Returns: + the magnitude in base units, the base unit and the unit as given (display units), + together with the conversion functions between the units. + """ + if typ is str: + self.u = "dimensionless" + self.du = None + val = quantity + elif isinstance(quantity, str): # only string variable make sense to disect + assert ureg is not None, f"UnitRegistry not found, while providing units: {quantity}" + try: + q = ureg(quantity) # parse the quantity-unit and return a Pint Quantity object + if isinstance(q, (int, float)): + self.u = "" + self.du = None + return q # integer or float variable with no units provided + elif isinstance(q, Quantity): # pint.Quantity object + # transform to base units ('SI' units). All internal calculations will be performed with these + val = self.val_unit_display(q, ureg) + else: + logger.critical(f"Unknown quantity {quantity} to disect") + raise VariableInitError(f"Unknown quantity {quantity} to disect") from None + # no recognized units. Assume a free string. ??Maybe we should be more selective about the exact error type: + except Exception as warn: + logger.warning(f"Unhandled quantity {quantity}: {warn}. A str? Set explicit 'typ=str'.") + self.u = "" + self.du = None + val = str(quantity) + else: + self.u = "dimensionless" + self.du = None + val = quantity + if typ is not None and type(val) is not typ: # check variable type + try: # try to convert the magnitude to the correct type. + val = typ(val) + except Exception as err: + logger.critical(f"Value {val} is not of the correct type {typ}") + raise VariableInitError(f"Value {val} is not of the correct type {typ}") from err + return val + + @classmethod + def linear(cls, x: float, b: float, a: float = 0.0): + return a + b * x + + @classmethod + def identity(cls, x: float): + return x + + def val_unit_display(self, q: Quantity, ureg: UnitRegistry) -> float: + """Identify base units and calculate the transformations between display and base units. + + Returns + ------- + The numerical value of q. As side effect + + * the unit `u` is set. Might be `dimensionless` + * the display unit `du` is set to None if same as unit, else + + - it is set to the display unit name and + - the transformations `to_base` and `from_base` are set. + """ + qb = q.to_base_units() + self.u = str(qb.units) + val = qb.magnitude # Note: numeric types are not converted, e.g. int to float + if qb.units == q.units: # no conversion + self.du = None + else: # calculate the conversion functions + # we generate a second value and calculate the straight line conversion function + # did not find a better way in pint + self.du = str(q.units) + q2 = ureg.Quantity(10.0 * (q.magnitude + 10.0), q.units) + qb2 = q2.to_base_units() + a = (qb.magnitude * q2.magnitude - qb2.magnitude * q.magnitude) / (q2.magnitude - q.magnitude) + b = (qb2.magnitude - qb.magnitude) / (q2.magnitude - q.magnitude) + if abs(a) < 1e-9: # multiplicative conversion + if abs(b - 1.0) < 1e-9: # unit and display unit are compatible. No transformation + self.du = None + self.to_base = partial(Unit.linear, b=b) + self.from_base = partial(Unit.linear, b=1.0 / b) + else: # there is a constant (e.g. Celsius to Fahrenheit) + self.to_base = partial(Unit.linear, b=b, a=a) + self.from_base = partial(Unit.linear, b=1.0 / b, a=-a / b) + return val + + @classmethod + def make(cls, quantity: PyType, ureg: UnitRegistry, typ: type | None = None) -> tuple[tuple[PyType], tuple[Unit]]: + u = Unit() + val = u.parse_quantity(quantity, ureg, typ) + return ((val,), (u,)) + + @classmethod + def make_tuple( + cls, quantities: tuple | list | np.ndarray, ureg: UnitRegistry, typ: type | None = None + ) -> tuple[tuple[PyType, ...], tuple[Unit, ...]]: + """Make a tuple of Unit objects from the tuple of quantities.""" + values: list[PyType] = [] + units: list[Unit] = [] + for q in quantities: + val, u = cls.make(q, ureg, typ) + values.extend(val) + units.extend(u) + return (tuple(values), tuple(units)) + + @classmethod + def derivative(cls, baseunits: tuple[Unit, ...], tu: str = "s") -> tuple[tuple[float, ...], tuple[Unit, ...]]: + """Construct units for a derivative variable of basevars. tu is the time unit.""" + units: list[Unit] = [] + for bu in baseunits: + u = Unit() + u.u = f"{bu.u}/{tu}" + u.du = None if bu.du is None else f"{bu.du}/{tu}" + if bu.du is not None: + u.to_base = bu.to_base + u.from_base = bu.from_base + units.append(u) + values = [0.0] * len(baseunits) + return (tuple(values), tuple(units)) + + def compatible( + self, quantity: PyType, ureg: UnitRegistry, typ: type | None = None, strict: bool = True + ) -> tuple[bool, PyType]: + """Check whether the supplied quantity 'q' is compatible with this unit. + If strict==True, the supplied quantity shall be in display units. + """ + _q, _unit = Unit.make(quantity, ureg, typ) + q = _q[0] + unit = _unit[0] + # no explicit unit needed when the quantity is 0 or inf (anything compatible) + if ( + ( + (q == 0 or q == float("inf") or q == float("-inf")) and unit.u == "dimensionless" + ) # 0, +/-inf without unit + or (strict and self.u == unit.u and self.du == unit.du) + or (not strict and self.u == unit.u) + ): + return (True, q) + else: + return (False, q) # Some special error classes @@ -83,7 +244,7 @@ class Variable(ScalarVariable): #. The Variable value is per default owned by the related model (see `self.model`). Through the `owner` parameter this can be changes. In structured models like the crane_fmu this might be adequate. - #. The current value of the variable directly accessible through the owner. + #. The current value of the variable is directly accessible through the owner. Direct value access assumes always internal units (per default: SI units) and range checking is not performed. #. Other access to the value is achieved through the `self.getter()` and the `self.setter( v)` functions, which are also used by the FMU getxxx and setxxx functions (external access). @@ -117,12 +278,15 @@ class Variable(ScalarVariable): If None, _typ is set to Enum/str if derived from these after disection or float if a number. 'int' is not automatically detected. start (PyType): The initial value of the variable. - Optionally, the unit can be included, providing the initial value as string, evaluating to quantity of type typ a display unit and base unit. - Note that the quantities are always converted to standard units of the same type, while the display unit may be different, i.e. the preferred user communication. - rng (tuple) = (): Optional range of the variable in terms of a tuple of the same type as initial value. Should be specified with units (as string). + Optionally, the unit can be included, providing the initial value as string, + evaluating to quantity of type typ a display unit and base unit. + Note that the quantities are always converted to standard units of the same type, while the display unit may be different, + i.e. the preferred user communication. + rng (tuple) = (): Optional range of the variable in terms of a tuple of the same type as initial value. + Should be specified with units (as string). * If an empty tuple is specified, the range is automatically determined. - That is only possible float or enum type variables, where the former evaluates to (-inf, inf). + That is only possible for float or enum type variables, where the former evaluates to (-inf, inf). Maximum or minimum int values do not exist in Python, such that these always must be provided explicitly. It is not possible to set only one of the elements of the tuple automatically. * If None is specified, the initial value is chosen, i.e. no range. @@ -143,14 +307,14 @@ class Variable(ScalarVariable): and do not need to be repeated on every simulation step. If given, the function shall apply to the whole (vecor) variable, and after unit conversion and range checking. - The function is completely invisible by the user specifying inputs to the variable. + The function is invisible by the user specifying inputs to the variable. owner = None: Optional possibility to overwrite the default owner. If the related model uses structured variable naming this should not be necessary, but for flat variable naming within complex models (not recommended) ownership setting might be necessary. local_name (str) = None: Optional possibility to overwrite the automatic determination of local_name, which is used to access the variable value and must be a property of owner. - This is convenient for example to link a derivative name to a variable if the default name der_ - is not acceptable. + This is convenient for example to link a derivative name to a variable if the default name, + i.e. der_ is not acceptable. """ def __init__( @@ -181,53 +345,59 @@ def __init__( super().__init__(name=name, description=description, getter=self.getter, setter=self.setter) parsed = ParsedVariable(name, self.model.variable_naming) + + self._annotations = annotations + self._check = value_check # unique for all elements in compound variables + self._typ: type | None = typ # preliminary. Will be adapted if not explicitly provided (None) + + self.on_step = on_step # hook to define a function of currentTime and time step dT, + # to be performed during Model.do_step for input variables + self.on_set = on_set + # Note: the _len is a central property, distinguishing scalar and compound variables. + self._unit: tuple[Unit, ...] + self._start: tuple[PyType, ...] = tuple() + if owner is None: oh = self.model.owner_hierarchy(parsed.parent) if owner is None: self.owner = oh[-1] else: self.owner = owner + basevar: Variable | None = None if local_name is None: if parsed.der > 0: # is a derivative of 'var' self.local_name = f"der{parsed.der}_{parsed.var}" - if not hasattr(self.owner, self.local_name): # the derivative is not explicitly defined in the model - if parsed.der == 1: # first derivative - self.local_name = f"der{parsed.der}_{parsed.var}" - setattr(self.owner, self.local_name, 0.0) - if on_step is None: - on_step = self.der1 - else: - raise NotImplementedError("None-explicit higher order derivatives not implemented") from None + if not hasattr(self.owner, self.local_name): # a virtual derivative + basevar = self.model.add_derivative( + self.name, parsed.as_string(("parent", "var", "der"), primitive=True) + ) + assert isinstance(basevar, Variable), f"The primitive of {self.name} must be a Variable object" + assert basevar.typ is float, f"The primitive of {self.name} shall be float. Found {basevar.typ}" + self._typ = float + if start is None: + self._start, self._unit = Unit.derivative(basevar.unit) + if self.on_step is None: + self.on_step = self.der1 else: self.local_name = parsed.var else: - self.local_name = local_name - - self._annotations = annotations - self._check = value_check # unique for all elements in compound variables - self._typ: type | None = typ # preliminary. Will be adapted if not explicitly provided (None) + self.local_name = local_name # use explicitly provided local name - self.on_step = on_step # hook to define a function of currentTime and time step dT, - # to be performed during Model.do_step for input variables - self.on_set = on_set - # Note: the _len is a central property, distinguishing scalar and compound variables. - - self._start: tuple - # First we check for str (since these are also iterable), then we can check for the presence of __getitem__ - # Determine the (element) type (unique for all elements in compound variables) if self._typ is str: # explicit free string + assert isinstance(start, str) self._len = 1 - self.unit = "dimensionless" - self.display = None + self._start, self._unit = Unit.make(start, self.model.ureg, typ=str) self.range = ("", "") # just a placeholder. Strings are not range checked - self.start = ("",) if start is None else (str(start),) else: # if type is provided and no (initial) value. We set a default value of the correct type as 'example' value - assert start is not None, f"{self.name}: start value is mandatory, at least for type and unit determination" - _start, _unit, _display = self._disect_unit(start) # do that first. units included as str! - self.start = _start - self.unit = _unit - self.display = _display + if not len(self._start): # not yet set + assert start is not None, ( + f"{self.name}: start value is mandatory, at least for type and unit determination" + ) + if isinstance(start, (tuple | list | np.ndarray)): + self._start, self._unit = Unit.make_tuple(start, self.model.ureg, self._typ) + else: + self._start, self._unit = Unit.make(start, self.model.ureg, self._typ) self._len = len(self._start) if self._typ is None: # try to adapt using start self._typ = self.auto_type(self._start) @@ -237,29 +407,40 @@ def __init__( self.range = self._init_range(rng) if not self.check_range(self._start, disp=False): # range checks of initial value + logger.critical(f"The provided value {self._start} is not in the valid range {self._range}") raise VariableInitError(f"The provided value {self._start} is not in the valid range {self._range}") self.model.register_variable(self) + assert len(self._start) > 0, "Empty tuples are not handled here:" try: - setattr(self.owner, self.local_name, np.array(self.start, self.typ) if self._len > 1 else self.start[0]) + setattr(self.owner, self.local_name, np.array(self._start, self.typ) if self._len > 1 else self._start[0]) except AttributeError as _: # can happen if a @property is defined for local_name, but no @local_name.setter pass def der1(self, current_time: float, step_size: float): - der = self.getter() # the current slope value - if any(x != 0.0 for x in der): # the value is (currently) set to > or < 0 - varname = self.local_name[5:] - if len(der) == 1: - assert isinstance(der[0], float) - setattr(self.owner, varname, getattr(self.owner, varname) + der[0] * step_size) + """Ramp the base variable value up or down within step_size.""" + der = getattr(self.owner, self.local_name) # the current slope value + if (isinstance(der, float) and der != 0.0) or ( + isinstance(der, (Sequence, np.ndarray)) and any(x != 0.0 for x in der) + ): # there is a slope + # varname = self.local_name[5:] # local name of the base variable + basevar = self.model.derivatives[self.name] # base variable object + val = getattr( + self.owner, basevar.local_name + ) # getattr(self.owner, varname) # previous value of base variable # + if not isinstance(der, (Sequence, np.ndarray)): + der = [der] + assert not isinstance(val, (Sequence, np.ndarray)), "Should be the same as der" + val = [val] + if isinstance(val, np.ndarray): + newval = val + step_size * np.array(der, float) + basevar.setter_internal(newval, -1, True) else: - setattr( - self.owner, - varname, - np.array(getattr(self.owner, varname), float) + np.array(der, float) * step_size, - ) + newval_list = [val[i] + step_size * der[i] for i in range(len(der))] + basevar.setter_internal(newval_list, -1, False) # disable super() functions and properties which are not in use here def to_xml(self) -> ET.Element: + logger.critical("The function to_xml() shall not be used from component-model") raise NotImplementedError("The function to_xml() shall not be used from component-model") from None # External access to read-only variables: @@ -271,40 +452,17 @@ def start(self): return self._start @start.setter - def start(self, val): - if isinstance(val, (str, int, float, bool, Enum)): - self._start = (val,) - elif isinstance(val, (tuple, list, np.ndarray)): + def start(self, val: PyType | Compound): + if isinstance(val, (Sequence, np.ndarray)): self._start = tuple(val) else: - raise VariableInitError(f"Unallowed start value setting {val} for variable {self.name}") from None + self._start = (val,) @property def unit(self): + """Get the unit object.""" return self._unit - @unit.setter - def unit(self, val): - if isinstance(val, (tuple, list)): - self._unit = tuple(val) - elif isinstance(val, str): - self._unit = (val,) - else: - raise VariableInitError(f"Unallowed unit setting {val} for variable {self.name}") from None - - @property - def display(self): - return self._display - - @display.setter - def display(self, val): - if val is None or (isinstance(val, tuple) and isinstance(val[0], str)): # single variable - self._display = (val,) - elif isinstance(val, tuple) and (val[0] is None or isinstance(val[0], (tuple))): # compound variable - self._display = tuple(val) - else: - raise VariableInitError(f"Unallowed display setting {val} for variable {self.name}") from None - @property def range(self): return self._range @@ -357,23 +515,25 @@ def setter(self, values: Sequence[int | float | bool | str | Enum] | np.ndarray, if self._check & Check.ranges: # do that before unit conversion, since range is stored in display units! if not self.check_range(values, idx): - raise VariableRangeError(f"set(): values {values} outside range.") from None + logger.error(f"set(): values {values} outside range.") if self._check & Check.units: #'values' expected as displayUnit. Convert to unit if idx >= 0: # explicit index of single values - if self._display[idx] is not None: - dvals = [self.display[idx][1](values[0])] - else: + if self._unit[idx].du is None: dvals = list(values) + else: + # assert isinstance(values[0], float) + dvals = [self._unit[idx].to_base(values[0])] # type: ignore ## values[0] is float! else: # the whole array dvals = [] for i in range(self._len): if values[i] is None: # keep the value dvals.append(getattr(self.owner, self.local_name)[i]) - elif self._display[i] is None: + elif self._unit[i].du is None: dvals.append(values[i]) else: - dvals.append(self.display[i][1](values[i])) + # assert isinstance(values[i], float) or (self._typ is int and isinstance(values[i], int)) + dvals.append(self._unit[i].to_base(values[i])) # type: ignore ## it is a float! else: # no unit issues if self._len == 1: dvals = [values[0] if values[0] is not None else getattr(self.owner, self.local_name)] @@ -382,39 +542,51 @@ def setter(self, values: Sequence[int | float | bool | str | Enum] | np.ndarray, values[i] if values[i] is not None else getattr(self.owner, self.local_name)[i] for i in range(self._len) ] + self.setter_internal(dvals, idx, is_ndarray) # do the setting, or flag as dirty + def setter_internal( + self, + values: Sequence[int | float | bool | str | Enum | None] | np.ndarray, + idx: int = -1, + is_ndarray: bool = False, + ): + """Do internal setting of values (no range checking and units expected internal), including dirty flags.""" if self._len == 1: - setattr(self.owner, self.local_name, dvals[0] if self.on_set is None else self.on_set(dvals[0])) # type: ignore + setattr(self.owner, self.local_name, values[0] if self.on_set is None else self.on_set(values[0])) # type: ignore elif idx >= 0: - if dvals[0] is not None: - getattr(self.owner, self.local_name)[idx] = dvals[0] + if values[0] is not None: # Note: only the indexed value is provided, as list! + val = getattr(self.owner, self.local_name) + val[idx] = values[0] + setattr(self.owner, self.local_name, val) if self.on_set is not None: self.model.dirty_ensure(self) else: # the whole array if is_ndarray: # Note: on_set might contain array operations - arr: np.ndarray = np.array(dvals, self._typ) + arr: np.ndarray = np.array(values, self._typ) setattr(self.owner, self.local_name, arr if self.on_set is None else self.on_set(arr)) else: - setattr(self.owner, self.local_name, dvals if self.on_set is None else self.on_set(dvals)) + setattr(self.owner, self.local_name, values if self.on_set is None else self.on_set(values)) if self.on_set is None: logger.debug(f"SETTER {self.name}, {values}[{idx}] => {getattr(self.owner, self.local_name)}") - def getter(self) -> Sequence[PyType]: + def getter(self) -> list[PyType]: """Get the value (output a value from the model), including range checking and unit conversion. - The whole variable value is returned. - The return a list of values. Can later be indexed/sliced to get elements of compound variables. + For compound variables, the whole variable is returned as list (even for scalar variables). + Returned value lists can later be indexed/sliced to get elements of (compound) variables. """ assert self._typ is not None, "Need a proper type at this stage" if self._len == 1: - value = getattr(self.owner, self.local_name) + value = getattr(self.owner, self.local_name) # work with the single value if issubclass(self._typ, Enum): # native Enums do not exist in FMI2. Convert to int - value = value.value - elif not isinstance(value, self._typ): # other type conversion - value = self._typ(value) # type: ignore[call-arg] - if self._check & Check.units: # Convert 'value' display.u -> base unit - if self._display[0] is not None: - value = self.display[0][2](value) - values = [value] + values = [value.value] + else: + if not isinstance(value, self._typ): # other type conversion + value = self._typ(value) # type: ignore[call-arg] + if self._check & Check.units: # Convert 'value' base unit -> display.u + if self._unit[0].du is not None: + assert isinstance(value, float) + value = self._unit[0].from_base(value) + values = [value] else: # compound variable values = list(getattr(self.owner, self.local_name)) # make value available as copy @@ -425,13 +597,13 @@ def getter(self) -> Sequence[PyType]: for i in range(self._len): # check whether conversion to _typ is necessary if not isinstance(values[i], self._typ): values[i] = self._typ(values[i]) # type: ignore[call-arg] - if self._check & Check.units: # Convert 'value' display.u -> base unit + if self._check & Check.units: # Convert 'value' base unit -> display.u for i in range(self._len): - if self._display[i] is not None: - values[i] = self.display[i][2](values[i]) + if self._unit[i].du is not None: + values[i] = self._unit[i].from_base(values[i]) - if self._check & Check.ranges and not self.check_range(values, -1): - raise VariableRangeError(f"getter(): Value {values} outside range.") from None + if self._check & Check.ranges and not self.check_range(values, -1): # check the range if so instructed + logger.error(f"getter(): Value of {self.name}: {values} outside range {self.range}!") return values def _init_range(self, rng: tuple | None) -> tuple: @@ -443,19 +615,6 @@ def _init_range(self, rng: tuple | None) -> tuple: Always for the whole variable with scalar variables packed in a singleton """ - def ensure_display_limits(val: PyType, idx: int, right: bool): - """Ensure that value is provided as display unit and that limits are included in range.""" - if self._display[idx] is not None: # Range in display units! - _val = self._display[idx] - assert isinstance(_val, tuple) - val = _val[2](val) - if isinstance(val, float) and abs(val) != float("inf") and int(val) != val: - if right: - val += 1e-15 - else: - val -= 1e-15 - return val - assert hasattr(self, "_start") and hasattr(self, "_unit"), "Missing self._start / self._unit" assert isinstance(self._typ, type), "init_range(): Need a defined _typ at this stage" # Configure input. Could be None, () or (min,max) of scalar @@ -466,12 +625,10 @@ def ensure_display_limits(val: PyType, idx: int, right: bool): for idx in range(self._len): # go through all elements _rng = rng[idx] if _rng is None: # => no range. Used for compound variables if not all elements have a range - _range.append( - ( - ensure_display_limits(self._start[idx], idx, right=False), - ensure_display_limits(self._start[idx], idx, right=True), - ) - ) # no range + s0 = self._start[idx] + assert isinstance(s0, float) + v = self._unit[idx].from_base(s0) if self._unit[idx].du is not None else s0 + _range.append((v, v)) elif isinstance(_rng, tuple) and not len(_rng): # empty tuple => try automatic range _range.append(self._auto_extreme(self._start[idx])) elif isinstance(_rng, tuple) and len(_rng) == 2: # normal range as 2-tuple @@ -480,26 +637,29 @@ def ensure_display_limits(val: PyType, idx: int, right: bool): if r is None: # no range => fixed to initial value q = self._start[idx] else: - q, u, du = self._disect_unit(r) - # no explicit unit needed when the quantity is 0 or inf - if (q == 0 or q == float("inf") or q == float("-inf")) and u == "dimensionless": - u = self._unit[idx] - elif self._unit[idx] != u: - raise VariableInitError( - f"The supplied range value {str(r)} does not conform to the unit type {self._unit[idx]}" - ) - elif du is not None and self._display[idx] is not None and du[0] != self._display[idx][0]: # type: ignore[index] - raise VariableInitError(f"Range unit {du[0]} != start {self._display[idx][0]}!") # type: ignore[index] - q = ensure_display_limits(q, idx, len(i_range) > 0) + check, q = self._unit[idx].compatible(r, self.model.ureg, self._typ, strict=True) + if not check: + check, q = self._unit[idx].compatible(r, self.model.ureg, self._typ, strict=False) + if check: + logger.warn(f"{self.name}[{idx}] range {r}: Use display units {self._unit[idx].du}!") + else: + msg = f"{self.name}[{idx}]: range {r} not conformant to the unit type {self._unit[idx]}" + logger.critical(msg) + raise VariableInitError(msg) + assert isinstance(q, float) or (self._typ is int and isinstance(q, int)) + if self._unit[idx].du is not None: + q = self._unit[idx].from_base(q) i_range.append(q) try: # check variable type i_range = [self._typ(x) for x in i_range] except Exception as err: - raise VariableRangeError(f"Incompatible types range {rng} - {self.start}") from err + logger.critical(f"Incompatible types range {rng} - {self._start}") + raise VariableRangeError(f"Incompatible types range {rng} - {self._start}") from err assert all(isinstance(x, self._typ) for x in i_range) _range.append(tuple(i_range)) # type: ignore else: + logger.critical(f"init_range(): Unhandled range argument {rng}") raise AssertionError(f"init_range(): Unhandled range argument {rng}") return tuple(_range) @@ -524,13 +684,12 @@ def check_range_single(self, value: PyType | None, idx: int = 0, disp: bool = Tr return isinstance(value, self._typ) elif isinstance(value, (int, float)) and all(isinstance(x, (int, float)) for x in self._range[idx]): - if not disp and self._display[idx] is not None: # check an internal unit values - _val = self._display[idx] - assert isinstance(_val, tuple) - value = _val[2](value) - return self._range[idx] is None or self._range[idx][0] <= value <= self._range[idx][1] # type: ignore + if not disp and self._unit[idx].du is not None: # check an internal unit values + value = self._unit[idx].from_base(value) + return self._range[idx] is None or self._range[idx][0] <= value <= self._range[idx][1] else: - raise VariableUseError(f"check_range(): value={value}, type={self.typ}, range={self.range}") from None + logger.error(f"check_range(): value={value}, type={self.typ}, range={self.range}") + return False def check_range(self, values: Sequence[PyType | None] | np.ndarray, idx: int = 0, disp: bool = True) -> bool: """Check the provided 'values' with respect to the range. @@ -594,8 +753,10 @@ def auto_type(cls, val: PyType | Compound, allow_int: bool = False): elif typ is int and t is float: # we allow that, even if no subclass typ = float else: + logger.critical(f"Incompatible variable types {typ}, {t} in {val}") raise VariableInitError(f"Incompatible variable types {typ}, {t} in {val}") from None else: + logger.critical(f"auto_type(). Unhandled {t}, {typ}") raise ValueError(f"auto_type(). Unhandled {t}, {typ}") return typ else: # single value @@ -613,8 +774,10 @@ def _auto_extreme(cls, var: PyType) -> tuple: """Return the extreme values of the variable. Args: - var: the variable for which to determine the extremes. Represented by an instantiated object - Returns: + var: the variable for which to determine the extremes, represented by an instantiated object (example) + + Returns + ------- A tuple containing the minimum and maximum value the given variable can have """ if isinstance(var, bool): @@ -622,79 +785,13 @@ def _auto_extreme(cls, var: PyType) -> tuple: elif isinstance(var, float): return (float("-inf"), float("inf")) elif isinstance(var, int): - raise VariableInitError(f"Range must be specified for int variable {cls} or use float.") + logger.critical(f"Range must be specified for int variable {cls} or use float.") + return (var, var) # restrict to start value elif isinstance(var, Enum): return (min(x.value for x in type(var)), max(x.value for x in type(var))) else: return tuple() # return an empty tuple (no range specified, e.g. for str) - def _disect_unit(self, quantity: PyType | Compound) -> tuple: - """Disect the provided quantity in terms of magnitude and unit, if provided as string. - If another type is provided, dimensionless units are assumed. - - Args: - quantity (PyType): the quantity to disect. Should be provided as string, but also the trivial cases (int,float,Enum) are allowed. - A free string should not be used and leads to a warning - Returns: - the magnitude in base units, the base unit and the unit as given (display units), - together with the conversion functions between the units. - """ - if isinstance(quantity, (tuple, list, np.ndarray)): # handle composit values - _val, _ub, _disp = [], [], [] - for q in quantity: # disect components and collect results - v, u, d = self._disect_unit(q) - _val.append(v) - _ub.append(u) - _disp.append(d) - return (tuple(_val), tuple(_ub), None if _disp is None else tuple(_disp)) - - elif isinstance(quantity, str): # only string variable make sense to disect - assert self.model.ureg is not None, f"UnitRegistry not found, while providing units: {quantity}" - try: - q = self.model.ureg(quantity) # parse the quantity-unit and return a Pint Quantity object - if isinstance(q, (int, float)): - return q, "", None # integer or float variable with no units provided - elif isinstance(q, Quantity): # pint.Quantity object - # transform to base units ('SI' units). All internal calculations will be performed with these - val, ub, display = self._get_transformation(q) - else: - raise VariableInitError(f"Unknown quantity {quantity} to disect") from None - # no recognized units. Assume a free string. ??Maybe we should be more selective about the exact error type: - except Exception as warn: - logger.warning(f"Unhandled quantity {quantity}: {warn}. A str? Set explicit 'typ=str'.") - val, ub, display = (str(quantity), "", None) # type: ignore - else: - val, ub, display = (quantity, "dimensionless", None) # type: ignore - if self._typ is not None and type(val) is not self._typ: # check variable type - try: # try to convert the magnitude to the correct type. - val = self._typ(val) - except Exception as err: - raise VariableInitError(f"Value {val} is not of the correct type {self._typ}") from err - return (val, ub, display) - - def _get_transformation(self, q: Quantity) -> tuple[float, str, tuple | None]: - """Identity base units and calculate the transformations between display and base units.""" - qb = q.to_base_units() - val = qb.magnitude # Note: numeric types are not converted, e.g. int to float - if qb.units == q.units: # no conversion - return (val, str(qb.units), None) - else: # calculate the conversion functions - # we generate a second value and calculate the straight line conversion function - # did not find a better way in pint - q2 = self.model.ureg.Quantity(10.0 * (q.magnitude + 10.0), q.units) - qb2 = q2.to_base_units() - a = (qb.magnitude * q2.magnitude - qb2.magnitude * q.magnitude) / (q2.magnitude - q.magnitude) - b = (qb2.magnitude - qb.magnitude) / (q2.magnitude - q.magnitude) - if abs(a) < 1e-9: # multiplicative conversion - if abs(b - 1.0) < 1e-9: # unit and display unit are compatible. No transformation - return (val, str(qb.units), None) - to_base = partial(linear, b=b) - from_base = partial(linear, b=1.0 / b) - else: # there is a constant (e.g. Celsius to Fahrenheit) - to_base = partial(linear, b, a) - from_base = partial(linear, b=1.0 / b, a=-a / b) - return (val, str(qb.units), (str(q.units), to_base, from_base)) - def xml_scalarvariables(self): """Generate XML code with respect to this variable and return xml element. For compound variables, all elements are included. @@ -708,20 +805,21 @@ def xml_scalarvariables(self): List of ScalarVariable xml elements """ - def substr(alt1: str, alti: str): - return alt1 if self._len == 1 else alti - _type = {"int": "Integer", "bool": "Boolean", "float": "Real", "str": "String", "Enum": "Enumeration"}[ self.typ.__qualname__ ] # translation of python to FMI primitives. Same for all components do_use_start = use_start(causality=self._causality, variability=self._variability, initial=self._initial) svars = [] - a_der = self.antiderivative() # d a_der /dt = self, or None + a_der = self.primitive() # d a_der /dt = self, or None for i in range(self._len): + if self._len > 1: + varname = ParsedVariable(self.name).as_string(index=str(i)) + else: + varname = ParsedVariable(self.name).as_string(include=("parent", "var", "der")) sv = ET.Element( "ScalarVariable", { - "name": self.name + substr("", f"[{i}]"), + "name": varname, "valueReference": str(self.value_reference + i), "description": "" if self.description is None else self.description, "causality": self.causality.name, @@ -732,13 +830,13 @@ def substr(alt1: str, alti: str): sv.attrib.update({"initial": self._initial.name}) if self._annotations is not None and i == 0: sv.append(ET.Element("annotations", self._annotations)) - # if self.display is None or (self._len>1 and self.display[i] is None): + # if self._unit[ is None or (self._len>1 and self._unit[i].du is None): # "display" = (self.unit, 1.0) # detailed variable definition info = ET.Element(_type) if do_use_start: # a start value is to be used - info.attrib.update({"start": self.fmi_type_str(self.start[i])}) + info.attrib.update({"start": self.fmi_type_str(self._start[i])}) if _type in ("Real", "Integer", "Enumeration"): # range to be specified xMin = self.range[i][0] if _type != "Real" or xMin > float("-inf"): @@ -751,9 +849,9 @@ def substr(alt1: str, alti: str): else: info.attrib.update({"unbounded": "true"}) if _type == "Real": # other attributes apply only to Real variables - info.attrib.update({"unit": self.unit[i]}) - if self.display is not None and self.display[i] is not None and self.unit[i] != self.display[i][0]: - info.attrib.update({"displayUnit": self.display[i][0]}) + info.attrib.update({"unit": self.unit[i].u}) + if isinstance(self._unit[i].du, str) and self.unit[i].du != self._unit[i].u: + info.attrib.update({"displayUnit": self._unit[i].du}) # type: ignore ## it is a str! if a_der is not None: info.attrib.update({"derivative": str(a_der.value_reference + i + 1)}) @@ -761,83 +859,13 @@ def substr(alt1: str, alti: str): svars.append(sv) return svars - def antiderivative(self) -> Variable | None: + def primitive(self) -> Variable | None: """Determine the variable which self is the derivative of. Return None if self is not a derivative. """ - if self.name.startswith("der(") and self.name.endswith(")"): - return self.model.variable_by_name(self.name[4:-1]) - else: + parsed = ParsedVariable(self.name) + if parsed.der == 0: return None - - -# Utility functions for handling special variable types -def spherical_to_cartesian(vec: np.ndarray | tuple, deg: bool = False) -> np.ndarray: - """Turn spherical vector 'vec' (defined according to ISO 80000-2 (r,polar,azimuth)) into cartesian coordinates.""" - if deg: - theta = radians(vec[1]) - phi = radians(vec[2]) - else: - theta = vec[1] - phi = vec[2] - sinTheta = sin(theta) - cosTheta = cos(theta) - sinPhi = sin(phi) - cosPhi = cos(phi) - r = vec[0] - return np.array((r * sinTheta * cosPhi, r * sinTheta * sinPhi, r * cosTheta)) - - -def cartesian_to_spherical(vec: np.ndarray | tuple, deg: bool = False) -> np.ndarray: - """Turn the vector 'vec' given in cartesian coordinates into spherical coordinates. - (defined according to ISO 80000-2, (r, polar, azimuth)). - """ - r = np.linalg.norm(vec) - if vec[0] == vec[1] == 0: - if vec[2] == 0: - return np.array((0, 0, 0), dtype="float") else: - return np.array((r, 0, 0), dtype="float") - elif deg: - return np.array((r, degrees(acos(vec[2] / r)), degrees(atan2(vec[1], vec[0]))), dtype="float64") - else: - return np.array((r, acos(vec[2] / r), atan2(vec[1], vec[0])), dtype="float") - - -def cartesian_to_cylindrical(vec: np.ndarray | tuple, deg: bool = False) -> np.ndarray: - """Turn the vector 'vec' given in cartesian coordinates into cylindrical coordinates. - (defined according to ISO, (r, phi, z), with phi right-handed wrt. x-axis). - """ - phi = atan2(vec[1], vec[0]) - if deg: - phi = degrees(phi) - return np.array((sqrt(vec[0] * vec[0] + vec[1] * vec[1]), phi, vec[2]), dtype="float") - - -def cylindrical_to_cartesian(vec: np.ndarray | tuple, deg: bool = False) -> np.ndarray: - """Turn cylinder coordinate vector 'vec' (defined according to ISO (r,phi,z)) into cartesian coordinates. - The angle phi is measured with respect to x-axis, right hand. - """ - phi = radians(vec[1]) if deg else vec[1] - return np.array((vec[0] * cos(phi), vec[0] * sin(phi), vec[2]), dtype="float") - - -def quantity_direction(quantity_direction: tuple, spherical: bool = False, deg: bool = False) -> np.ndarray: - """Turn a 4-tuple, consisting of quantity (float) and a direction 3-vector to a direction 3-vector, - where the norm denotes the direction and the length denotes the quantity. - The return vector is always a cartesian vector. - - Args: - quantity_direction (tuple): a 4-tuple consisting of the desired length of the resulting vector (in standard units (m or m/s)) - and the direction 3-vector (in standard units) - spherical (bool)=False: Optional possibility to provide the input direction vector in spherical coordinates - deg (bool)=False: Optional possibility to provide the input angle (of spherical coordinates) in degrees. Only relevant if spherical=True - """ - if quantity_direction[0] < 1e-15: - return np.array((0, 0, 0), dtype="float") - if spherical: - direction = spherical_to_cartesian(quantity_direction[1:], deg) # turn to cartesian coordinates, if required - else: - direction = np.array(quantity_direction[1:], dtype="float") - n = np.linalg.norm(direction) # normalize - return quantity_direction[0] / n * direction + name = parsed.as_string(("parent", "var", "der"), simplified=True, primitive=True) + return self.model.variable_by_name(name) diff --git a/src/component_model/variable_naming.py b/src/component_model/variable_naming.py index 048592e..207c4b8 100644 --- a/src/component_model/variable_naming.py +++ b/src/component_model/variable_naming.py @@ -27,7 +27,7 @@ class ParsedVariable: * der: unsigned integer, defining the derivation order. 0 for no derivation """ - def __init__(self, varname: str, convention: VariableNamingConvention): + def __init__(self, varname: str, convention: VariableNamingConvention = VariableNamingConvention.structured): self.parent: str | None # None indicates no parent self.var: str self.indices: list[int] = [] # empty list indicates no indices @@ -40,19 +40,20 @@ def __init__(self, varname: str, convention: VariableNamingConvention): self.indices = indices self.der = 0 else: # structured variable naming (only these two are defined) - m = re.match(r"der\((.+)\)", varname) - if m is not None: + self.der = 0 # default and count start + var = varname + while True: + m = re.match(r"der\((.+)\)", var) # der(*) + if m is None: + break vo = m.group(1) - m = re.match(r"(.+),(\d+)$", vo) + m = re.match(r"(.+),\s*(\d+)$", vo) # check order of der if m is not None: var = m.group(1) - self.der = int(m.group(2)) + self.der += int(m.group(2)) else: var = vo - self.der = 1 - else: - var = varname - self.der = 0 + self.der += 1 varlist = var.split(".") if len(varlist) > 1: self.parent = varlist[0] + "".join("." + varlist[i] for i in range(1, len(varlist) - 1)) @@ -67,9 +68,49 @@ def as_tuple(self): """Return all fields as tuple.""" return (self.parent, self.var, self.indices, self.der) + def as_string( + self, + include: tuple[str, ...] = ("parent", "var", "indices", "der"), + simplified: bool = True, + primitive: bool = False, + index: str = "", + ): + """Re-construct the variable name, including what is requested and optionally adapting. + + Args: + include (tuple): list of strings of what to incdule of "parent", "var", "indices", "der" + simplified (bool)= True: Optionally change the style to also including superfluous info + primitive (bool)= False: If true, 'der' included and >0, determines the name of the primitive. + index (str)="": If !="" and 'indices' included, specifies the index to use + + This is convenient to e.g. leave out indices (vector variables) or finding parent names of derivatives. + """ + if "parent" in include: + name = "" if self.parent is None else self.parent + else: + name = "" + if "var" in include: + name = name + "." + self.var if len(name) else self.var + if "indices" in include and (len(self.indices) or index != ""): # never empty parantheses + if index != "": + name += f"[{index}]" + else: + name += str(self.indices) + if "der" in include and self.der > 0: + der = self.der if not primitive else self.der - 1 + if der == 0: + pass # just the name + elif not simplified or der > 1: + name = f"der({name},{der})" + elif simplified and der == 1: + name = f"der({name})" + else: + raise NotImplementedError(f"Unknown combination simplified={simplified}, der={der}") from None + return name + @staticmethod def disect_indices(txt: str) -> tuple[str, list[int]]: - m = re.match(r"(.+)\[([\d,]+)\]", txt) + m = re.match(r"(.+)\[([\d,\s*]+)\]", txt) if m is None: return (txt, []) else: diff --git a/tests/conftest.py b/tests/conftest.py index a45ad4c..893c43b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,10 +1,59 @@ import logging import os +import xml.etree.ElementTree as ET # noqa: N817 from pathlib import Path from shutil import rmtree import pytest +from component_model.utils.xml import read_xml + + +def system_structure_change(structure_file: Path, change: dict, newname: str | None = None): + """Do changes to an existing 'structure_file' and save as newname. + Changes are provided as dict: { tag : (what, newval),...} + where 'what'='text' marks the text part else, an attribute is assumed. + """ + + def register_all_namespaces(filename: Path): + namespaces: dict = {} + for _, (ns, uri) in ET.iterparse(filename, events=["start-ns"]): + # print("TYPES", ns, type(ns), uri, type(uri)) + namespaces.update({ns: uri}) + ET.register_namespace(str(ns), str(uri)) + # namespaces: dict = dict([node )]) + # for ns in namespaces: + # ET.register_namespace(ns, namespaces[ns]) + return namespaces + + if newname is None: + newname = structure_file.name # same as old + nss = register_all_namespaces(structure_file) + el = read_xml(structure_file) + for tag, (what, newval) in change.items(): + elements = el.findall(f".//ns:{tag}", {"ns": nss[""]}) + for e in elements: + if what == "text": + e.text = newval + else: # assume attribute name + e.attrib[what] = newval + path = structure_file.parent / newname + ET.ElementTree(el).write(path, encoding="utf-8") + return path + + +# @pytest.fixture(scope="session", autouse=True) +# def instantiate_cosim_execution() -> None: +# """ +# Fixture that instantiates a CosimExecution object for the entire package. +# This fixture is automatically used for the entire package. +# """ +# +# from libcosimpy.CosimExecution import CosimExecution +# +# _ = CosimExecution.from_step_size(1) +# return + @pytest.fixture(scope="package", autouse=True) def chdir() -> None: @@ -77,4 +126,4 @@ def pytest_addoption(parser): @pytest.fixture(scope="session") def show(request): - return request.config.getoption("--show") == "True" + return request.config.getoption("--show") == "False" diff --git a/tests/test_analysis.py b/tests/test_analysis.py new file mode 100644 index 0000000..8bd08ea --- /dev/null +++ b/tests/test_analysis.py @@ -0,0 +1,34 @@ +# pyright: ignore[reportAttributeAccessIssue] # PythonFMU generates variable value objects using setattr() +import logging + +import numpy as np +import pytest + +from component_model.utils.analysis import extremum, extremum_series + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def test_extremum(): + t = [np.radians(10 * x) for x in range(100)] + x = [np.cos(x) for x in t] + e, p = extremum(t[0:3], x[0:3], 2e-3) # allow a small error + assert e == 1 + assert p[0] > -2e-3 and p[1] < 1 + 1e-6, ( + "Top of parabola somewhat to the left due to cos not exactly equal to 2.order" + ) + # for i in range(100): + # print(i, t[i], x[i]) + e, p = extremum(t[17:20], x[17:20]) + assert e == -1 and abs(p[0] - np.pi) < 1e-10 and np.isclose(p[1], -1) + ex = extremum_series(t, x, "all") + assert len(ex) == 2 + assert np.allclose(ex[0], (12.566370614359142, 1.0)) + assert np.allclose(ex[1], (15.707963267948958, -1.0)) + + +if __name__ == "__main__": + retcode = pytest.main(["-rP -s -v", __file__]) + assert retcode == 0, f"Return code {retcode}" + # test_extremum() diff --git a/tests/test_structured_variables.py b/tests/test_axle_fmu.py similarity index 98% rename from tests/test_structured_variables.py rename to tests/test_axle_fmu.py index 44d95bd..a6d3fae 100644 --- a/tests/test_structured_variables.py +++ b/tests/test_axle_fmu.py @@ -99,7 +99,7 @@ def _axle_fmu(): build_path = Path.cwd() build_path.mkdir(exist_ok=True) fmu_path = Model.build( - script=str(Path(__file__).parent.parent / "examples" / "axle_fmu.py"), + script=Path(__file__).parent.parent / "examples" / "axle_fmu.py", project_files=[Path(__file__).parent.parent / "examples" / "axle.py"], dest=build_path, ) diff --git a/tests/test_bouncing_ball_3d.py b/tests/test_bouncing_ball_3d_fmu.py similarity index 72% rename from tests/test_bouncing_ball_3d.py rename to tests/test_bouncing_ball_3d_fmu.py index cf58a67..216fab2 100644 --- a/tests/test_bouncing_ball_3d.py +++ b/tests/test_bouncing_ball_3d_fmu.py @@ -7,15 +7,11 @@ from zipfile import ZipFile import matplotlib.pyplot as plt +import numpy as np import pytest from fmpy import plot_result, simulate_fmu # type: ignore from fmpy.util import fmu_info # type: ignore from fmpy.validation import validate_fmu # type: ignore -from libcosimpy.CosimEnums import CosimErrorCode, CosimExecutionState -from libcosimpy.CosimExecution import CosimExecution -from libcosimpy.CosimManipulator import CosimManipulator -from libcosimpy.CosimObserver import CosimObserver -from libcosimpy.CosimSlave import CosimLocalSlave from pythonfmu.default_experiment import DefaultExperiment from component_model.model import Model @@ -26,14 +22,6 @@ def _in_interval(x: float, x0: float, x1: float): return x0 <= x <= x1 or x1 <= x <= x0 -def arrays_equal(arr1, arr2, eps=1e-7): - assert len(arr1) == len(arr2), "Length not equal!" - - for i in range(len(arr1)): - # assert type(arr1[i]) == type(arr2[i]), f"Array element {i} type {type(arr1[i])} != {type(arr2[i])}" - assert abs(arr1[i] - arr2[i]) < eps, f"Component {i}: {arr1[i]} != {arr2[i]}" - - def _to_et(file: str, sub: str = "modelDescription.xml"): with ZipFile(file) as zp: xml = zp.read(sub) @@ -75,7 +63,8 @@ def test_bouncing_ball_class(show): from examples.bouncing_ball_3d import BouncingBall3D bb = BouncingBall3D() - assert bb._pos.display is not None + assert bb._pos.unit[0].u == "meter" + assert bb._pos.unit[2].du == "inch" assert bb._pos.setter is not None assert bb._pos.getter is not None assert bb._speed.getter is not None @@ -94,9 +83,9 @@ def get_result(): result.append((bb.time, *_pos, *_speed, *_p_bounce)) h_fac = 1.0 - if len(bb._pos.display) > 1 and bb._pos.display[2] is not None: # the main test settings - arrays_equal(bb.pos, (0, 0, 10 * 0.0254)) # was provided as inch - arrays_equal(bb.speed, (1, 0, 0)) + if len(bb._pos) > 1 and bb._pos.unit[2].du is not None: # the main test settings + assert np.allclose(bb.pos, (0, 0, 10 * 0.0254)) # was provided as inch + assert np.allclose(bb.speed, (1, 0, 0)) assert bb.g == 9.81 assert bb.e == 0.9 h_fac = 0.0254 @@ -109,18 +98,18 @@ def get_result(): dt = bb.default_experiment.step_size assert dt == 0.01 # set start values (in display units. Are translated to internal units - if len(bb._pos.display) > 1 and bb._pos.display[2] is not None: + if len(bb._pos) > 1 and bb._pos.unit[2].du is not None: bb._pos.setter((0, 0, 10)) t_b, p_b = bb.next_bounce() assert t_bounce == t_b # print("Bounce", t_bounce, x_bounce, p_b) - arrays_equal((x_bounce, 0, 0), p_b), f"x_bounce:{x_bounce} != {p_b[0]}" # type: ignore ##?? + assert np.allclose((x_bounce, 0, 0), p_b), f"x_bounce:{x_bounce} != {p_b[0]}" # type: ignore ##?? get_result() # after one step bb.do_step(time, dt) get_result() # print("After one step", result(bb)) - arrays_equal( + assert np.allclose( result[-1], ( 0.01, # time @@ -143,7 +132,7 @@ def get_result(): bb.do_step(time, dt) get_result() # print(f"Just before bounce @{t_bounce}, {t_before}: {result[-1]}") - arrays_equal( + assert np.allclose( result[-1], ( t_before, @@ -157,7 +146,7 @@ def get_result(): 0, 0, ), - eps=0.003, + atol=0.003, ) # just after bounce # print(f"Step {len(z)}, time {bb.time}, pos:{bb.pos}, speed:{bb.speed}, t_bounce:{bb.t_bounce}, p_bounce:{bb.p_bounce}") @@ -165,7 +154,7 @@ def get_result(): get_result() ddt = t_before + dt - t_bounce # time from bounce to end of step x_bounce2 = x_bounce + 2 * v_bounce * bb.e * 1.0 * bb.e / bb.g - arrays_equal( + assert np.allclose( result[-1], ( t_before + dt, @@ -179,7 +168,7 @@ def get_result(): 0, 0, ), - eps=0.03, + atol=0.03, ) # from bounce to bounce v_x, v_z, t_b, x_b = ( @@ -227,6 +216,12 @@ def test_make_bouncing_ball(bouncing_ball_fmu): def test_use_fmu(bouncing_ball_fmu, show): """Test and validate the basic BouncingBall using fmpy and not using OSP or case_study.""" + + def check_result(res: np.ndarray, expected: tuple, eps=1e-10): # res is a structured array + assert len(res) == len(expected), f"Lengths {res} != {expected}" + for r, e in zip(res, expected, strict=True): + assert abs(r - e) < eps, f"{r} != {e}" + assert bouncing_ball_fmu.exists(), f"File {bouncing_ball_fmu} does not exist" dt = 0.01 result = simulate_fmu( # type: ignore[reportArgumentType] @@ -256,24 +251,11 @@ def test_use_fmu(bouncing_ball_fmu, show): v_bounce = g * 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! - arrays_equal(list(result[0])[:7], [0, 0, 0, 10, 1, 0, 0]) # time,pos-3, speed-3(, p_bounce-3 not calculated) + assert np.allclose(list(result[0])[:7], [0, 0, 0, 10, 1, 0, 0]) # time,pos-3, speed-3(, p_bounce-3 not calculated) # print(f"Result[1]: {result[1]}") - arrays_equal( - result[1], - ( - 0.01, # time - 0.01, # pos - 0, - (h0 - 0.5 * g * 0.01**2) / h_fac, - 1, # speed - 0, - -g * 0.01, - x_bounce, # p_bounce - 0, - 0, - ), - ) - arrays_equal( + check_result(result[1], (0.01, 0.01, 0, (h0 - 0.5 * g * 0.01**2) / h_fac, 1, 0, -g * 0.01, x_bounce, 0, 0)) + + check_result( result[1], ( 0.01, @@ -294,7 +276,7 @@ def test_use_fmu(bouncing_ball_fmu, show): if t_before == t_bounce: # at the interval border t_before -= dt # print(f"Just before bounce @{t_bounce}, {t_before}: {result[-1]}") - arrays_equal( + check_result( result[int(t_before / dt)], ( t_before, @@ -314,7 +296,7 @@ def test_use_fmu(bouncing_ball_fmu, show): # print(f"Step {len(z)}, time {bb.time}, pos:{bb.pos}, speed:{bb.speed}, t_bounce:{bb.t_bounce}, p_bounce:{bb.p_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( + check_result( result[int((t_before + dt) / dt)], ( t_before + dt, @@ -359,75 +341,6 @@ def test_use_fmu(bouncing_ball_fmu, show): assert abs(result[row - 1][4] * e - result[row][4]) < 1e-15, "Reduced speed in x-direction" -def test_from_osp(bouncing_ball_fmu): - def get_status(sim): - status = sim.status() - return { - "currentTime": status.current_time, - "state": CosimExecutionState(status.state).name, - "error_code": CosimErrorCode(status.error_code).name, - "real_time_factor": status.real_time_factor, - "rolling_average_real_time_factor": status.rolling_average_real_time_factor, - "real_time_factor_target": status.real_time_factor_target, - "is_real_time_simulation": status.is_real_time_simulation, - "steps_to_monitor": status.steps_to_monitor, - } - - sim = CosimExecution.from_step_size(step_size=1e7) # empty execution object with fixed time step in nanos - bb = CosimLocalSlave(fmu_path=str(bouncing_ball_fmu.absolute()), instance_name="bb") - - ibb = sim.add_local_slave(bb) - assert ibb == 0, f"local slave number {ibb}" - info = sim.slave_infos() - assert info[0].name.decode() == "bb", "The name of the component instance" - assert info[0].index == 0, "The index of the component instance" - assert sim.slave_index_from_instance_name("bb") == 0 - assert sim.num_slaves() == 1 - assert sim.num_slave_variables(0) == 11, "3*pos, 3*speed, g, e, 3*p_bounce" - variables = {var_ref.name.decode(): var_ref.reference for var_ref in sim.slave_variables(ibb)} - assert variables == { - "pos[0]": 0, - "pos[1]": 1, - "pos[2]": 2, - "speed[0]": 3, - "speed[1]": 4, - "speed[2]": 5, - "g": 6, - "e": 7, - "p_bounce[0]": 8, - "p_bounce[1]": 9, - "p_bounce[2]": 10, - } - - # Set initial values - sim.real_initial_value(ibb, variables["g"], 1.5) # actual setting will only happen after start_initialization_mode - - assert get_status(sim)["state"] == "STOPPED" - - observer = CosimObserver.create_last_value() - assert sim.add_observer(observer) - manipulator = CosimManipulator.create_override() - assert sim.add_manipulator(manipulator) - - values = observer.last_real_values(0, list(range(11))) - assert values == [0.0] * 11, "No initial values yet! - as expected" - - # that does not seem to work (not clear why): assert sim.step()==True - assert sim.simulate_until(target_time=1e7), "Simulate for one base step did not work" - assert get_status(sim)["currentTime"] == 1e7, "Time after simulation not correct" - values = observer.last_real_values(0, list(range(11))) - assert values[6] == 1.5, "Initial setting did not work" - assert values[5] == -0.015, "Initial setting did not have the expected effect on speed" - - -# values = observer.last_real_values(0, list(range(11))) -# print("VALUES2", values) -# -# manipulator.reset_variables(0, CosimVariableType.REAL, [6]) - -# sim.simulate_until(target_time=3e9) - - def test_from_fmu(bouncing_ball_fmu): assert bouncing_ball_fmu.exists(), "FMU not found" model = model_from_fmu(bouncing_ball_fmu) @@ -456,7 +369,6 @@ def test_from_fmu(bouncing_ball_fmu): os.chdir(Path(__file__).parent / "test_working_directory") # test_bouncing_ball_class(show=False) - test_make_bouncing_ball(_bouncing_ball_fmu()) - # test_use_fmu(_bouncing_ball_fmu(), True) + # test_make_bouncing_ball(_bouncing_ball_fmu()) + test_use_fmu(_bouncing_ball_fmu(), True) # test_from_fmu( _bouncing_ball_fmu()) - # test_from_osp( _bouncing_ball_fmu()) diff --git a/tests/test_controls.py b/tests/test_controls.py new file mode 100644 index 0000000..662a0e4 --- /dev/null +++ b/tests/test_controls.py @@ -0,0 +1,138 @@ +import logging + +import matplotlib.pyplot as plt +import numpy as np +import pytest + +from component_model.utils.controls import Controls + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def test_limits(): + _b = Controls( + ("len", "polar", "azimuth"), # bcrane boom control + (((1, 20), None, (100, 100)), ((-2, 2), (-1, 1), (0, 0)), ((-1, 1), (-2, 2), (0, 0))), + ) + assert _b.limits(0, 1) == _b.limits("len", 1) == (float("-inf"), float("inf")), ( + f"Found {_b.limits(0, 1)}, {_b.limits('len', 1)}" + ) + assert _b.limits(1, 2) == _b.limits("polar", 2) == (0, 0), f"Found {_b.limits(1, 2)}, {_b.limits('polar', 2)}" + with pytest.raises(AssertionError) as err: + _b.append("additional", ((1, 0), (0, 1), 0, 1)) + assert err.value.args[0].startswith("Wrong order of limits:") + # Controls can also be built step by step: + _b = Controls() + _b.append("len", ((1, 20), None, (1, 100))) + _b.append("polar", ((-2, 2), 1)) # fixed velocity and the acceleration limits are not needed + _b.append("azimuth", ((-1, 1), (-2, 2), (0, 0))) + assert _b.limit(1, 2, 0) == _b.limit(1, 2, 1) == 0.0, "No polar acceleration allowed" + assert _b.nogoals, "No goals yet set" + # try to set goal outside limits + _b.limit_err = logging.CRITICAL + with pytest.raises(ValueError) as err: # type: ignore[assignment] #it is a 'ValueError' + _b.setgoal(1, 2, 9.9, 0.0) + assert err.value.args[0] == "Goal value 9.9 is above the limit 0.0.Stopping execution." + _b.limit_err = logging.WARNING + _b.setgoal(1, 2, 9.9, 0.0) + assert _b.nogoals, f"No goals expected, because the adjusted goal is already reached. Found {_b.goals}" + _b.setgoal(0, 2, 1.1, 0.0) + assert _b.goals == [((float("inf"), 1.1),), None, None], f"Found {_b.goals}" + assert not _b.nogoals, "At least one goal set" + + +def test_goal(show: bool = False): + def do_goal(order: int, value: float, current: np.ndarray | None = None, t_end: float = 10.0): + time = 0.0 + if current is not None: + _b.current[0] = current + if order == 2: + _b.setgoal("len", order, value, time) + elif order == 1: + _b.setgoal("len", order, value, time) + else: + _b.setgoal("len", order, value, time) + dt = 0.1 + res: list[tuple] = [] + assert _b.goals[0] is not None + res.append((time, _b.current[0][0], _b.current[0][1], _b.goals[0][0][1])) + while time + dt < t_end: + _b.step(time, dt) + time += dt + res.append((time, *_b.current[0])) + if show: + do_show(res) + return res + + _b = Controls(limit_err=logging.CRITICAL) + _b.append("len", ((-100.0, 90.0), None, (-1.0, 0.5))) + _b.append("polar", ((-2.0, 2.0), 1.0)) # fixed velocity and the acceleration limits are not needed + _b.append("azimuth", ((-1.0, 1.0), (-2.0, 2.0), (0.0, 0.0))) + + # get from one position to another (zero velocity and acceleration on both ends) + res = do_goal(0, 0.0, current=np.array((10.0, 0.0, 0.0), float), t_end=11.7) + assert abs(_b.current[0][0] + 12.5) < 1e-13, f"Found {_b.current[0][0]}" + assert abs(_b.current[0][1]) < 1e-13 + assert abs(_b.current[0][2]) < 1e-13 + + # get from one position to another (non-zero velocity on start) + res = do_goal(0, 0.0, current=np.array((10.0, 1.0, 0.0), float), t_end=3.65) + assert np.allclose(_b.current[0], (9.375, 0.0, 0.0)), f"Found {_b.current[0]}" + + # get from one velocity to another + res = do_goal(1, -2.0, current=np.array((10.0, 1.0, 0.0), float), t_end=3.1) + assert np.allclose(_b.current[0], (8.5, -2.0, 0.0)) + + # set an acceleration (non-zero position and velocity) + res = do_goal(2, -0.1, current=np.array((10.0, 1.0, 0.0), float), t_end=2.01) + expected = (10 + 1.0 * 2.0 - 0.5 * 0.1 * 2.0**2, 1.0 - 0.1 * 2.0, -0.1) + assert np.allclose(_b.current[0], expected), f"{_b.current[0]} != {expected} " + + _b.limit_err = logging.WARNING # allow corrections from now on + _b.current = [np.array((0.0, 0.0, 0.0), float)] * 3 + + # accelerate in 10 time units + res = do_goal(2, 1.1) + for t, x, v, a in res: + assert np.allclose((x, v, a), (0.5 * 0.5 * t * t, 0.5 * t, 0.5)), f"@{t}: Found {(x, v, a)}" + + assert abs(_b.current[0][1] - 5.0) < 1e-9, f"Found {_b.current[0][1]}" + assert abs(_b.current[0][0] - 25.0) < 1e-9, f"Found {_b.current[0][0]}" + + # Speed from 5 to 2.2 + res = do_goal(1, 2.2) + tgoal = (2.2 - 5) / (-1.0) + for t, x, v, a in res: + if t < tgoal: + assert abs(a + 1) < 1e-9, f"@{t} Acc.: {a}" + assert abs(v - 5.0 - a * t) < 1e-9, f"@{t} Velocity: {v}" + assert abs(x - 25.0 - 5 * t - 0.5 * a * t * t) < 1e-9, f"@{t} Pos.: {x} != {25 + 5 * t - 0.5 * 1 * t * t}" + else: + _x = 25.0 + 5.0 * tgoal - 0.5 * 1.0 * tgoal**2 + 2.2 * (t - tgoal) + assert np.allclose((x, v, a), (_x, 2.2, 0.0)), f"@{t}: {(x, v, a)} != {(_x, 2.2, 0)}" + + res = do_goal(0, 0.0) + + +def do_show(results: list[tuple]): + """Plot selected traces.""" + times = [row[0] for row in results] + fig, (ax1, ax2, ax3) = plt.subplots(1, 3) + fig.set_size_inches(10, 6) + _ = ax1.plot(times, [row[1] for row in results], label="position") + _ = ax2.plot(times, [row[2] for row in results], label="velocity") + _ = ax3.plot(times, [row[3] for row in results], label="acceleration") + _ = ax1.legend() + _ = ax2.legend() + _ = ax3.legend() + plt.show() + + +if __name__ == "__main__": + retcode = pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) + assert retcode == 0, f"Non-zero return code {retcode}" + logging.basicConfig(level=logging.DEBUG) + plt.set_loglevel(level="warning") + # test_limits() + # test_goal(show=True) diff --git a/tests/test_make_oscillator_fmu.py b/tests/test_make_oscillator_fmu.py deleted file mode 100644 index 5930423..0000000 --- a/tests/test_make_oscillator_fmu.py +++ /dev/null @@ -1,258 +0,0 @@ -import shutil -import sys -from collections.abc import Iterable -from pathlib import Path -from typing import Any - -import matplotlib.pyplot as plt -import numpy as np -import pytest -from fmpy.simulation import simulate_fmu -from fmpy.util import fmu_info, plot_result -from fmpy.validation import validate_fmu -from libcosimpy.CosimEnums import CosimExecutionState -from libcosimpy.CosimExecution import CosimExecution -from libcosimpy.CosimLogging import CosimLogLevel, log_output_level -from libcosimpy.CosimManipulator import CosimManipulator -from libcosimpy.CosimObserver import CosimObserver -from libcosimpy.CosimSlave import CosimLocalSlave - -from component_model.model import Model - - -def arrays_equal( - res: Iterable[Any], - expected: Iterable[Any], - eps: float = 1e-7, -): - len_res = len(list(res)) - len_exp = len(list(expected)) - if len_res != len_exp: - raise ValueError(f"Arrays of different lengths cannot be equal. Found {len_res} != {len_exp}") - 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 do_show(time: list[float], z: list[float], v: list[float]): - fig, ax = plt.subplots() - _ = ax.plot(time, z, label="z-position") - _ = ax.plot(time, v, label="z-speed") - _ = ax.legend() - plt.show() - - -def force(t: float, ampl: float = 1.0, omega: float = 0.1): - return np.array((0, 0, ampl * np.sin(omega * t)), dtype=float) - - -@pytest.fixture(scope="session") -def oscillator_fmu(): - return _oscillator_fmu() - - -def _oscillator_fmu(): - """Make FMU and return .fmu file with path.""" - build_path = Path.cwd() - build_path.mkdir(exist_ok=True) - src = Path(__file__).parent.parent / "examples" / "oscillator_fmu.py" - fmu_path = Model.build( - script=str(src), - project_files=[src], - dest=build_path, - ) - return fmu_path - - -@pytest.fixture(scope="session") -def driver_fmu(): - return _driver_fmu() - - -def _driver_fmu(): - """Make FMU and return .fmu file with path.""" - build_path = Path.cwd() - build_path.mkdir(exist_ok=True) - src = Path(__file__).parent.parent / "examples" / "driving_force_fmu.py" - fmu_path = Model.build( - script=str(src), - project_files=[src], - dest=build_path, - ) - return fmu_path - - -@pytest.fixture(scope="session") -def system_structure(): - return _system_structure() - - -def _system_structure(): - """Make a OSP structure file and return the path""" - # path = make_osp_system_structure( - # name="ForcedOscillator", - # simulators={ - # "osc": {"source": "HarmonicOscillator.fmu", "stepSize": 0.01}, - # "drv": {"source": "DrivingForce.fmu", "stepSize": 0.01}, - # }, - # connections_variable=(("drv", "f[2]", "osc", "f[2]"),), - # version="0.1", - # start=0.0, - # base_step=0.01, - # algorithm="fixedStep", - # path=Path.cwd(), - # ) - shutil.copy(Path(__file__).parent.parent / "examples" / "ForcedOscillator.xml", Path.cwd()) - return Path.cwd() / "ForcedOscillator.xml" - - -def test_make_fmus( - oscillator_fmu: Path, - driver_fmu: Path, -): - info = fmu_info(filename=str(oscillator_fmu)) # this is a formatted string. Not easy to check - print(f"Info Oscillator: {info}") - val = validate_fmu(filename=str(oscillator_fmu)) - assert not len(val), f"Validation of of {oscillator_fmu.name} was not successful. Errors: {val}" - - info = fmu_info(filename=str(driver_fmu)) # this is a formatted string. Not easy to check - print(f"Info Driver: {info}") - val = validate_fmu(filename=str(driver_fmu)) - assert not len(val), f"Validation of of {oscillator_fmu.name} was not successful. Errors: {val}" - - -# def test_make_system_structure(system_structure: Path): -# assert Path(system_structure).exists(), "System structure not created" -# el = read_xml(Path(system_structure)) -# assert isinstance(el, ET.Element), f"ElementTree element expected. Found {el}" -# ns = el.tag.split("{")[1].split("}")[0] -# print("NS", ns, system_structure) -# for s in el.findall(".//{*}Simulator"): -# assert (Path(system_structure).parent / s.get("source", "??")).exists(), f"Component {s.get('name')} not found" -# for _con in el.findall(".//{*}VariableConnection"): -# for c in _con: -# assert c.attrib in ({"simulator": "drv", "name": "f[2]"}, {"simulator": "osc", "name": "f[2]"}) -# - - -def test_use_fmu(oscillator_fmu: Path, driver_fmu: Path, show: bool): - """Test single FMUs.""" - # sourcery skip: move-assign - result = simulate_fmu( - oscillator_fmu, - stop_time=50, - step_size=0.01, - validate=True, - solver="Euler", - debug_logging=True, - logger=print, # fmi_call_logger=print, - start_values={"x[2]": 1.0, "c[2]": 0.1}, - step_finished=None, # pyright: ignore[reportArgumentType] # (typing incorrect in fmpy) - fmu_instance=None, # pyright: ignore[reportArgumentType] # (typing incorrect in fmpy) - ) - if show: - plot_result(result) - - -def test_run_osp(oscillator_fmu: Path, driver_fmu: Path): - # sourcery skip: extract-duplicate-method - sim = CosimExecution.from_step_size(step_size=1e8) # empty execution object with fixed time step in nanos - osc = CosimLocalSlave(fmu_path=str(oscillator_fmu), instance_name="osc") - _osc = sim.add_local_slave(osc) - assert _osc == 0, f"local slave number {_osc}" - reference_dict = {var_ref.name.decode(): var_ref.reference for var_ref in sim.slave_variables(_osc)} - - dri = CosimLocalSlave(fmu_path=str(driver_fmu), instance_name="dri") - _dri = sim.add_local_slave(dri) - assert _dri == 1, f"local slave number {_dri}" - - # Set initial values - sim.real_initial_value(slave_index=_osc, variable_reference=reference_dict["x[2]"], value=1.0) - sim.real_initial_value(slave_index=_osc, variable_reference=reference_dict["c[2]"], value=0.1) - - sim_status = sim.status() - assert sim_status.current_time == 0 - assert CosimExecutionState(sim_status.state) == CosimExecutionState.STOPPED - - # Simulate for 1 second - _ = sim.simulate_until(target_time=15e9) - - -@pytest.mark.skipif(sys.platform.startswith("linux"), reason="HarmonicOsciallatorFMU.fmu throws an error on Linux") -def test_run_osp_system_structure(system_structure: Path, show: bool): - "Run an OSP simulation in the same way as the SimulatorInterface of sim-explorer is implemented" - log_output_level(CosimLogLevel.TRACE) - print("STRUCTURE", system_structure) - simulator = CosimExecution.from_osp_config_file(str(system_structure)) - sim_status = simulator.status() - assert sim_status.current_time == 0 - assert CosimExecutionState(sim_status.state) == CosimExecutionState.STOPPED - comps = [] - for comp in list(simulator.slave_infos()): - name = comp.name.decode() - comps.append(name) - assert comps == ["osc", "drv"] - variables = {} - for idx in range(simulator.num_slave_variables(0)): - struct = simulator.slave_variables(0)[idx] - variables[struct.name.decode()] = { - "reference": struct.reference, - "type": struct.type, - "causality": struct.causality, - "variability": struct.variability, - } - - for idx in range(simulator.num_slave_variables(1)): - struct = simulator.slave_variables(1)[idx] - variables |= { - struct.name.decode(): { - "reference": struct.reference, - "type": struct.type, - "causality": struct.causality, - "variability": struct.variability, - } - } - assert variables["c[2]"]["type"] == 0 - assert variables["c[2]"]["causality"] == 1 - assert variables["c[2]"]["variability"] == 1 - - assert variables["x[2]"]["type"] == 0 - assert variables["x[2]"]["causality"] == 2 - assert variables["x[2]"]["variability"] == 4 - - assert variables["v[2]"]["type"] == 0 - assert variables["v[2]"]["causality"] == 2 - assert variables["v[2]"]["variability"] == 4 - - # Instantiate a suitable observer for collecting results. - # Instantiate a suitable manipulator for changing variables. - manipulator = CosimManipulator.create_override() - simulator.add_manipulator(manipulator=manipulator) - simulator.real_initial_value(slave_index=0, variable_reference=5, value=0.5) # c[2] - simulator.real_initial_value(slave_index=0, variable_reference=9, value=1.0) # x[2] - observer = CosimObserver.create_last_value() - simulator.add_observer(observer=observer) - times = [] - pos = [] - speed = [] - for step in range(1, 1000): - time = step * 0.01 - _ = simulator.simulate_until(step * 1e8) - values = observer.last_real_values(slave_index=0, variable_references=[9, 12]) # x[2], v[2] - times.append(time) - pos.append(values[0]) - speed.append(values[1]) - if show: - do_show(time=times, z=pos, v=speed) - - -if __name__ == "__main__": - retcode = 0 # pytest.main(args=["-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_fmus(_oscillator_fmu(), _driver_fmu()) - # test_make_system_structure( _system_structure()) - # test_use_fmu(_oscillator_fmu(), _driver_fmu(), show=True) - # test_run_osp(_oscillator_fmu(), _driver_fmu()) - test_run_osp_system_structure(_system_structure(), show=True) diff --git a/tests/test_model.py b/tests/test_model.py index b44eb27..64736c1 100644 --- a/tests/test_model.py +++ b/tests/test_model.py @@ -147,9 +147,9 @@ def test_from_fmu(bouncing_ball_fmu): if __name__ == "__main__": - retcode = 0 # pytest.main(["-rA", "-v", __file__]) + retcode = pytest.main(["-rA", "-v", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" # test_license() # test_xml() - test_from_fmu(_bouncing_ball_fmu()) + # test_from_fmu(_bouncing_ball_fmu()) # test_variable_naming() diff --git a/tests/test_oscillator.py b/tests/test_oscillator.py index 683704b..a003636 100644 --- a/tests/test_oscillator.py +++ b/tests/test_oscillator.py @@ -1,8 +1,11 @@ from functools import partial from math import atan2, cos, exp, pi, sin, sqrt +from pathlib import Path import matplotlib.pyplot as plt import numpy as np +import pytest +from scipy.integrate import solve_ivp def arrays_equal(res: tuple[float, ...] | list[float], expected: tuple[float, ...] | list[float], eps=1e-7): @@ -14,10 +17,18 @@ def arrays_equal(res: tuple[float, ...] | list[float], expected: tuple[float, .. return True -def do_show(time: list, z: list, v: list, compare1: list | None = None, compare2: list | None = None): +def do_show( + time: list, + z: list, + v: list, + compare1: list | None = None, + compare2: list | None = None, + z_label: str = "z-position", + v_label: str = "z-speed", +): fig, ax = plt.subplots() - ax.plot(time, z, label="z-position") - ax.plot(time, v, label="z-speed") + ax.plot(time, z, label=z_label) + ax.plot(time, v, label=v_label) if compare1 is not None: ax.plot(time, compare1, label="compare1") if compare2 is not None: @@ -26,8 +37,11 @@ def do_show(time: list, z: list, v: list, compare1: list | None = None, compare2 plt.show() -def force(t: float, ampl: float = 1.0, omega: float = 0.1): - return np.array((0, 0, ampl * sin(omega * t)), float) +def force(t: float, ampl: float = 1.0, omega: float = 0.1, d_omega: float = 0.0): + if d_omega == 0.0: + return np.array((0, 0, ampl * sin(omega * t)), float) # fixed frequency + else: + return np.array((0, 0, ampl * sin((omega + d_omega * t) * t)), float) # frequency sweep def forced_oscillator( @@ -115,7 +129,43 @@ def run_oscillation_z( return (osc, times, z, v) -def test_oscillator_class(show): +def sweep_oscillation_z( + k: float, + c: float, + m: float, + ampl: float, + d_omega: float, + x0: float = 1.0, + v0: float = 0.0, + dt: float = 0.01, + end: float = 30.0, + tol: float = 1e-3, +): + """Run the oscillator with the given settings + with linearly increasing force frequency + for the given time (only z-direction activated) + and return the oscillator object and the time series for z-position and z-velocity.""" + + from examples.oscillator import Oscillator + + f_func = f_func = partial(force, ampl=ampl, omega=0.0, d_omega=d_omega) + osc = Oscillator(k=(1.0, 1.0, k), c=(0.0, 0.0, c), m=m, tolerance=tol, f_func=f_func) + osc.x[2] = x0 # set initial z value + osc.v[2] = v0 # set initial z-speed + times, z, v, f = [], [], [], [] + time = 0.0 + while time < end: + times.append(time) + z.append(osc.x[2]) + v.append(osc.v[2]) + osc.do_step(time, dt) + f.append(f_func(time)[2]) + time += dt + + return (osc, times, z, v, f) + + +def test_oscillator_class(show: bool = False): """Test the Oscillator class in isolation. Such tests are strongly recommended before compiling the model into an FMU. @@ -156,7 +206,7 @@ def test_oscillator_class(show): print(f". Max absolute error: {emax}") -def test_2d(show): +def test_2d(show: bool = False): from examples.oscillator import Oscillator def run_2d( @@ -224,8 +274,102 @@ def area(x: list[float], y: list[float]): show_2d(x, y) +def test_sweep_oscillator(show: bool = False): + """A forced oscillator where the force frequency is changed linearly as d_omega*time. + The test demonstrates that a monolithic simulation provides accurate results in all ranges of the force frequency. + Co-simulating the oscillator and the force, this does not work. + """ + osc, times0, z0, v0, f0 = sweep_oscillation_z( + k=1.0, + c=0.1, + m=1.0, + ampl=1.0, + d_omega=0.1, + x0=0.0, + v0=0.0, + dt=0.1, # 'ground truth', small dt + end=100.0, + tol=1e-3, + ) + with open(Path.cwd() / "oscillator_sweep0.dat", "w") as fp: + for i in range(len(times0)): + fp.write(f"{times0[i]}\t{z0[i]}\t{v0[i]}\t{f0[i]}\n") + + if show: + freq = [0.1 * t / 2 / np.pi for t in times0] + fig, ax = plt.subplots() + ax.plot(freq, z0, label="z0(t)") + ax.plot(freq, v0, label="v0(t)") + # ax.plot(freq, f0, label="F0(t)") + ax.legend() + plt.show() + + osc, times, z, v, f = sweep_oscillation_z( + k=1.0, + c=0.1, + m=1.0, + ampl=1.0, + d_omega=0.1, + x0=0.0, + v0=0.0, + dt=1, # dt similar to resonance frequency + end=100.0, + tol=1e-3, + ) + i0 = 0 + for i in range(len(times)): # demonstrate that the results are accurate, even if dt is large + t = times[i] + while abs(times0[i0] - t) > 1e-10: + i0 += 1 + assert times0[i0] - t < 0.1, f"Time entry for time {t} not found in times0" + + assert abs(z0[i0] - z[i]) < 2e-2, f"Time {t}. Found {z0[i0]} != {z[i]}" + assert abs(v0[i0] - v[i]) < 2e-2, f"Time {t}. Found {v0[i0]} != {v[i]}" + + if show: + fig, ax = plt.subplots() + ax.plot(times0, z0, label="z0(t)") + ax.plot(times, z, label="z(t)") + ax.legend() + plt.show() + + +def test_ivp(show: bool = False): + """Perform a few tests to get more acquainted with the IVP solver. Taken from scipy documentation""" + + def upward_cannon(t, y): # return speed and accelleration as function of (position, speed) + return [y[1], -9.81] + + def hit_ground(t, y): + return y[0] + + sol = solve_ivp( + upward_cannon, # initial value function + [0, 100], # time range + [0, 200], # start values (position, speed) + t_eval=[t for t in range(100)], # evaluate at these points (not only last time value. For plotting) + ) + assert sol.status == 0, "No events involved. Successful status should be 0" + assert len(sol.y) == 2, "y is a double vector of (position, speed), which is also reflected in results" + if show: + do_show(sol.t, sol.y[0], sol.y[1], z_label="pos", v_label="speed") + # include hit_ground event. Monkey patching function (which mypy, pyright do not like) + hit_ground.terminal = True # type: ignore[attr-defined] + hit_ground.direction = -1 # type: ignore[attr-defined] + sol = solve_ivp(upward_cannon, [0, 100], [0, 200], t_eval=[t for t in range(100)], events=hit_ground) + assert np.allclose(sol.t_events, [2 * 200 / 9.81]), "Time when hitting the ground" + assert np.allclose(sol.y_events, [[0.0, -200.0]]), "Position and speed when hitting the ground" + if show: + do_show(sol.t, sol.y[0], sol.y[1], z_label="pos", v_label="speed") + + if __name__ == "__main__": - retcode = 0 # pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) + retcode = pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" - test_oscillator_class(show=True) + import os + + os.chdir(Path(__file__).parent.absolute() / "test_working_directory") + # test_oscillator_class(show=True) # test_2d(show=True) + # test_sweep_oscillator(show=True) + # test_ivp() diff --git a/tests/test_oscillator_6dof_fmu.py b/tests/test_oscillator_6dof_fmu.py new file mode 100644 index 0000000..4b501b6 --- /dev/null +++ b/tests/test_oscillator_6dof_fmu.py @@ -0,0 +1,145 @@ +# ruff: noqa: I001 +from collections.abc import Iterable +from pathlib import Path +from typing import Any +import matplotlib.pyplot as plt +import pytest +from fmpy.simulation import simulate_fmu +from fmpy.util import fmu_info, plot_result +from fmpy.validation import validate_fmu +from component_model.model import Model + + +@pytest.fixture(scope="module") +def oscillator_6d_fmu(): + return _oscillator_6d_fmu() + + +def _oscillator_6d_fmu(): + """Make FMU and return .fmu file with path.""" + src = Path(__file__).parent.parent / "examples" / "oscillator_6dof_fmu.py" + assert src.exists(), f"Model file {src} not found." + fmu_path = Model.build( + script=str(src), + dest=Path(__file__).parent.parent / "examples" / "HarmonicOscillator6D.fmu", + newargs={ + "k": ("1N/m", "1N/m", "1N/m", "1N*m/rad", "1N*m/rad", "1N*m/rad"), + "c": ("0.1N*s/m", "0.1N*s/m", "0.1N*s/m", "0.1N*m*s/rad", "0.1N*m*s/rad", "0.1N*m*s/rad"), + "m": "1.0kg", + "mi": ("1.0 kg*m**2", "1.0 kg*m**2", "1.0 kg*m**2"), + "x0": ("0.0m", "0.0m", "0.0m", "0.0rad", "0.0rad", "0.0rad"), + "v0": ("0.0m/s", "0.0m/s", "0.0m/s", "0.0rad/s", "0.0rad/s", "0.0rad/s"), + }, + ) + return fmu_path + + +@pytest.fixture(scope="module") +def driver_6d_fmu(): + return _driver_6d_fmu() + + +def _driver_6d_fmu(): + """Make FMU and return .fmu file with path.""" + src = Path(__file__).parent.parent / "examples" / "driving_force_fmu.py" + assert src.exists(), f"Model file {src} not found." + fmu_path = Model.build( + script=str(src), + dest=Path(__file__).parent.parent / "examples" / "DrivingForce6D.fmu", + newargs={"ampl": ("1.0N", "1.0N", "1.0N", "1.0N*m", "1.0N*m", "1.0N*m"), "freq": ("1.0Hz",) * 6}, + ) + return fmu_path + + +def arrays_equal( + res: Iterable[Any], + expected: Iterable[Any], + eps: float = 1e-7, +): + len_res = len(list(res)) + len_exp = len(list(expected)) + if len_res != len_exp: + raise ValueError(f"Arrays of different lengths cannot be equal. Found {len_res} != {len_exp}") + 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 do_show( + traces: dict[str, tuple[list[float], list[float]]], + xlabel: str = "frequency in Hz", + ylabel: str = "position/angle", + title: str = "External force frequency sweep with time between co-sim calls = 1.0", +): + fig, ax = plt.subplots() + ax.set_title(title) + ax.set_xlabel(xlabel) + ax.set_ylabel(ylabel) + for label, trace in traces.items(): + _ = ax.plot(trace[0], trace[1], label=label) + _ = ax.legend() + plt.show() + + +# def force(t: float, ampl: float = 1.0, omega: float = 0.1): +# return np.array((0, 0, ampl * np.sin(omega * t)), dtype=float) + + +def test_make_fmus( + oscillator_6d_fmu: Path, + driver_6d_fmu: Path, +): + info = fmu_info(filename=str(oscillator_6d_fmu)) # this is a formatted string. Not easy to check + print(f"Info Oscillator: {info}") + val = validate_fmu(filename=str(oscillator_6d_fmu)) + assert not len(val), f"Validation of of {oscillator_6d_fmu.name} was not successful. Errors: {val}" + + info = fmu_info(filename=str(driver_6d_fmu)) # this is a formatted string. Not easy to check + print(f"Info Driver: {info}") + val = validate_fmu(filename=str(driver_6d_fmu)) + assert not len(val), f"Validation of of {driver_6d_fmu.name} was not successful. Errors: {val}" + + +# def test_make_system_structure(system_structure: Path): +# assert Path(system_structure).exists(), "System structure not created" +# el = read_xml(Path(system_structure)) +# assert isinstance(el, ET.Element), f"ElementTree element expected. Found {el}" +# ns = el.tag.split("{")[1].split("}")[0] +# print("NS", ns, system_structure) +# for s in el.findall(".//{*}Simulator"): +# assert (Path(system_structure).parent / s.get("source", "??")).exists(), f"Component {s.get('name')} not found" +# for _con in el.findall(".//{*}VariableConnection"): +# for c in _con: +# assert c.attrib in ({"simulator": "drv", "name": "f[2]"}, {"simulator": "osc", "name": "f[2]"}) +# + + +pytest.mark.skip() + + +def test_use_fmu(oscillator_6d_fmu: Path, show: bool = False): # , driver_6d_fmu: Path, show: bool = False): + """Test single FMUs.""" + # sourcery skip: move-assign + result = simulate_fmu( + oscillator_6d_fmu, + stop_time=50, + step_size=0.01, + validate=True, + solver="Euler", + debug_logging=True, + logger=print, # fmi_call_logger=print, + start_values={"x[2]": 1.0, "c[2]": 0.1}, + ) + if show: + plot_result(result) + + +if __name__ == "__main__": + retcode = pytest.main(args=["-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") + drv = _driver_6d_fmu() + osc = _oscillator_6d_fmu() + # test_make_fmus(osc, drv) + # test_use_fmu(osc, drv, show=True) diff --git a/tests/test_oscillator_fmu.py b/tests/test_oscillator_fmu.py new file mode 100644 index 0000000..cbfd32f --- /dev/null +++ b/tests/test_oscillator_fmu.py @@ -0,0 +1,155 @@ +# ruff: noqa: I001 +from pathlib import Path + +import matplotlib.pyplot as plt +import pytest +from fmpy.simulation import simulate_fmu +from fmpy.util import fmu_info, plot_result +from fmpy.validation import validate_fmu + +from component_model.model import Model + + +@pytest.fixture(scope="module") +def oscillator_fmu(): + return _oscillator_fmu() + + +def _oscillator_fmu(): + """Make FMU and return .fmu file with path.""" + fmu_path = Model.build( + script=Path(__file__).parent.parent / "examples" / "oscillator_fmu.py", + dest=Path(__file__).parent.parent / "examples" / "HarmonicOscillator.fmu", + ) + return fmu_path + + +@pytest.fixture(scope="module") +def driver_fmu(): + return _driver_fmu() + + +def _driver_fmu(): + """Make FMU and return .fmu file with path.""" + fmu_path = Model.build( + script=Path(__file__).parent.parent / "examples" / "driving_force_fmu.py", + dest=Path(__file__).parent.parent / "examples" / "DrivingForce.fmu", + newargs={"ampl": ("3N", "2N", "1N"), "freq": ("3Hz", "2Hz", "1Hz")}, + ) + return fmu_path + + +def do_show(traces: dict[str, tuple[list[float], list[float]]]): + fig, ax = plt.subplots() + for label, trace in traces.items(): + _ = ax.plot(trace[0], trace[1], label=label) + _ = ax.legend() + plt.show() + + +# def force(t: float, ampl: float = 1.0, omega: float = 0.1): +# return np.array((0, 0, ampl * np.sin(omega * t)), dtype=float) + + +@pytest.fixture(scope="session") +def system_structure(): + return _system_structure() + + +def _system_structure(): + """Make a OSP structure file and return the path""" + return Path(__file__).parent.parent / "examples" / "ForcedOscillator.xml" + + +def test_make_fmus( + oscillator_fmu: Path, + driver_fmu: Path, + show: bool = False, +): + info = fmu_info(filename=str(oscillator_fmu)) # this is a formatted string. Not easy to check + if show: + print(f"Info Oscillator @{oscillator_fmu}") + print(info) + val = validate_fmu(filename=str(oscillator_fmu)) + assert not len(val), f"Validation of of {oscillator_fmu.name} was not successful. Errors: {val}" + + info = fmu_info(filename=str(driver_fmu)) # this is a formatted string. Not easy to check + if show: + print(f"Info Driver: @{driver_fmu}") + print(info) + val = validate_fmu(filename=str(driver_fmu)) + assert not len(val), f"Validation of of {driver_fmu.name} was not successful. Errors: {val}" + + +# def test_make_system_structure(system_structure: Path): +# assert Path(system_structure).exists(), "System structure not created" +# el = read_xml(Path(system_structure)) +# assert isinstance(el, ET.Element), f"ElementTree element expected. Found {el}" +# ns = el.tag.split("{")[1].split("}")[0] +# print("NS", ns, system_structure) +# for s in el.findall(".//{*}Simulator"): +# assert (Path(system_structure).parent / s.get("source", "??")).exists(), f"Component {s.get('name')} not found" +# for _con in el.findall(".//{*}VariableConnection"): +# for c in _con: +# assert c.attrib in ({"simulator": "drv", "name": "f[2]"}, {"simulator": "osc", "name": "f[2]"}) +# + + +def test_run_fmpy(oscillator_fmu: Path, driver_fmu: Path, show: bool = False): + """Test single FMUs.""" + # sourcery skip: move-assign + result = simulate_fmu( + oscillator_fmu, + stop_time=50, + step_size=0.01, + validate=True, + solver="Euler", + debug_logging=True, + logger=print, # fmi_call_logger=print, + start_values={"x[2]": 1.0, "c[2]": 0.1}, + step_finished=None, # pyright: ignore[reportArgumentType] # (typing incorrect in fmpy) + fmu_instance=None, # pyright: ignore[reportArgumentType] # (typing incorrect in fmpy) + ) + if show: + plot_result(result) + + +def test_run_fmpy2(oscillator_fmu: Path, driver_fmu: Path, show: bool = False): + """Test oscillator in setting similar to 'crane_on_spring'""" + # sourcery skip: move-assign + result = simulate_fmu( + oscillator_fmu, + stop_time=10, + step_size=0.01, + validate=True, + solver="Euler", + debug_logging=True, + logger=print, # fmi_call_logger=print, + start_values={ + "m": 10000.0, + "k[0]": 10000.0, + "k[1]": 10000.0, + "k[2]": 10000.0, + "x[0]": 0.0, + "x[1]": 0.0, + "x[2]": 0.0, + "v[0]": 1.0, + }, + step_finished=None, # pyright: ignore[reportArgumentType] # (typing incorrect in fmpy) + fmu_instance=None, # pyright: ignore[reportArgumentType] # (typing incorrect in fmpy) + ) + if show: + plot_result(result) + + +if __name__ == "__main__": + retcode = pytest.main(args=["-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") + osc = _oscillator_fmu() + drv = _driver_fmu() + # test_make_fmus(osc, drv, show=True) + # test_run_fmpy(osc, drv, show=True) + # test_run_fmpy2(osc, drv, show=True) diff --git a/tests/test_oscillator_xd.py b/tests/test_oscillator_xd.py new file mode 100644 index 0000000..efd43c0 --- /dev/null +++ b/tests/test_oscillator_xd.py @@ -0,0 +1,409 @@ +from functools import partial +from math import atan2, cos, exp, pi, sin, sqrt +from pathlib import Path + +import matplotlib.pyplot as plt +import numpy as np +import pytest + + +def do_show( + time: list, + z: list, + v: list, + compare1: list | None = None, + compare2: list | None = None, + z_label: str = "z-position", + v_label: str = "z-speed", +): + fig, ax = plt.subplots() + ax.plot(time, z, label=z_label) + ax.plot(time, v, label=v_label) + if compare1 is not None: + ax.plot(time, compare1, label="compare1") + if compare2 is not None: + ax.plot(time, compare2, label="compare2") + ax.legend() + plt.show() + + +def force_xv( + dim: int = 3, + t: float | None = None, + x: np.ndarray | None = None, + v: np.ndarray | None = None, + dt: float | None = None, + const: float | None = None, + d_omega: float = 0.0, + ampl: float | None = None, + omega: float = 0.1, + ampl_x: float | None = None, + ampl_v: float | None = None, +): + """Use a force which is dependent on position and/or velocity. Time is ignored""" + force = np.array((0,) * dim, float) + if isinstance(const, float): + force += np.array((const,) * dim, float) + if t is not None and ampl is not None: + if d_omega == 0.0: + force += np.array((0, 0, ampl * sin(omega * t)), float) # fixed frequency + else: + force += np.array((0, 0, ampl * sin((omega + d_omega * t) * t)), float) # frequency sweep + if isinstance(ampl_x, float): + force += ampl_x * x # type: ignore[operator] ##it is definitely float* ndarray + if isinstance(ampl_v, float): + force += ampl_v * v # type: ignore[operator] ##it is definitely float* ndarray + return force + + +def forced_oscillator( + t: float, k: float, c: float, m: float, a: float = 0.0, wf: float = 0.1, x0: float = 1.0, v0: float = 0.0 +): + """Calculates the expected (analytic) position and speed of a harmonic oscillator (in one dimension) + with the given parameter setting. + + Args: + t (float): time + k,c,m (float): harmonic oscillator parameters + a,wf (float): sinusoidal force parameters (amplitude and angular frequency) + x0, v0 (float): start values for harmonic oscillator (force has fixed start values) + """ + from math import atan2, sin, sqrt + + w0 = sqrt(k / m) # omega0 + b = c / (2 * m) # beta + if a != 0: + assert x0 == 0 and v0 == 0, "Checking of forced oscillations is only implemented for x0=0 and v0=0" + A = a / sqrt(((w0**2 - wf**2) ** 2 + (2 * b * wf) ** 2)) + d = atan2(2 * b * wf, w0**2 - wf**2) # phase angle in equilibrium + x0 = A * sin(d) + v0 = -A * wf * cos(d) + x_e = A * sin(wf * t - d) + v_e = A * wf * cos(wf * t - d) + # return x_e, v_e + else: + x_e, v_e = (0.0, 0.0) + + if w0 - b > 1e-10: # damped oscillation + w1 = sqrt(w0**2 - b**2) # angular frequency of oscillation + x = exp(-b * t) * (x0 * cos(w1 * t) + (x0 * b + v0) / w1 * sin(w1 * t)) + v = exp(-b * t) * (v0 * cos(w1 * t) - (w1 * x0 + b**2 / w1 * x0 + b / w1 * v0) * sin(w1 * t)) + elif abs(w0 - b) < 1e-10: # critically damped oscillation + x = ((v0 + b * x0) * t + x0) * exp(-b * t) + v = -b * (v0 + b * x0) * t * exp(-b * t) + else: # over-damped oscillation + w1_ = sqrt(b**2 - w0**2) + _b1 = 2 * b - w1_ + _b2 = 2 * b + w1_ + x = ((b + w1_) * x0 + v0) / 2 / w1_ * exp(-(b - w1_) * t) - ((b - w1_) * x0 + v0) / 2 / w1_ * exp( + -(b + w1_) * t + ) + v = -((b + w1_) * x0 + v0) / 2 / w1_ * (b - w1_) * exp(-(b - w1_) * t) + ((b - w1_) * x0 + v0) / 2 / w1_ * ( + b + w1_ + ) * exp(-(b + w1_) * t) + if a != 0: + return (x + x_e, v + v_e) + else: + return (x, v) + + +def run_oscillation_z( + k: float, + c: float, + m: float, + ampl: float, + omega: float, + x0: float = 1.0, + v0: float = 0.0, + dt: float = 0.01, + end: float = 30.0, + tol: float = 1e-3, +): + """Run the oscillator with the given settings for the given time (only z-direction activated) + and return the oscillator object and the time series for z-position and z-velocity.""" + + from examples.oscillator_xd import Force, OscillatorXD + + _f = partial(force_xv, dim=3, ampl=ampl, omega=omega) + _force = Force(3, _f) + osc = OscillatorXD(dim=3, k=(1.0, 1.0, k), c=(0.0, 0.0, c), m=m, tolerance=tol, force=_force) + osc.x[2] = x0 # set initial z value + osc.v[2] = v0 # set initial z-speed + times, z, v = [], [], [] + time = 0.0 + while time < end: + times.append(time) + z.append(osc.x[2]) + v.append(osc.v[2]) + osc.do_step(time, dt) + time += dt + + return (osc, times, z, v) + + +def sweep_oscillation_z( + k: float, + c: float, + m: float, + ampl: float, + d_omega: float, + x0: float = 1.0, + v0: float = 0.0, + dt: float = 0.01, + end: float = 30.0, + tol: float = 1e-3, +): + """Run the oscillator with the given settings + with linearly increasing force frequency + for the given time (only z-direction activated) + and return the oscillator object and the time series for z-position and z-velocity.""" + + from examples.oscillator_xd import Force, OscillatorXD + + _f = partial(force_xv, dim=3, ampl=ampl, omega=0.0, d_omega=d_omega) + _force = Force(3, _f) + osc = OscillatorXD(dim=3, k=(1.0, 1.0, k), c=(0.0, 0.0, c), m=m, tolerance=tol, force=_force) + osc.x[2] = x0 # set initial z value + osc.v[2] = v0 # set initial z-speed + times, z, v, f = [], [], [], [] + time = 0.0 + while time < end: + times.append(time) + z.append(osc.x[2]) + v.append(osc.v[2]) + osc.do_step(time, dt) + f.append(_f(t=time)[2]) + time += dt + + return (osc, times, z, v, f) + + +def test_oscillator_class(show): + """Test the Oscillator class in isolation. + Such tests are strongly recommended before compiling the model into an FMU. + + With respect to `wiki `_ our parameters are: + b = c, k=k => beta = c/(2m), w0 = sqrt(k/m) => w1 = sqrt(beta^2 - w0^2) = sqrt( c^2/4/m^2 - k/m) + """ + test_cases: list[tuple[float, float, float, float, float, float, str]] = [ + # k c m a w x0 description + (1.0, 0.0, 1.0, 0.0, 0.1, 1.0, "Oscillator without damping and force"), + (1.0, 0.2, 1.0, 0.0, 0.1, 1.0, "Oscillator include damping"), + (1.0, 2.0, 1.0, 0.0, 0.1, 1.0, "Oscillator critically damped"), + (1.0, 5.0, 1.0, 0.0, 0.1, 1.0, "Oscillator over-damped"), + (1.0, 0.2, 1.0, 1.0, 0.5, 0.0, "Forced oscillation. Less than resonance freq"), + (1.0, 0.2, 1.0, 1.0, 1.0, 0.0, "Forced oscillation. Damped. Resonant"), + (1.0, 0.2, 1.0, 1.0, 2.0, 0.0, "Forced oscillation. Damped. Above resonance freq"), + (1.0, 2.0, 1.0, 1.0, 1.0, 0.0, "Forced oscillation. Crit. damped. Resonant"), + (1.0, 5.0, 1.0, 1.0, 1.0, 0.0, "Forced oscillation. Over-damped. Resonant"), + ] + tol = 1e-3 # tolerance for simulation + for k, c, m, a, w, x0, msg in test_cases: + print(f"{msg}: k={k}, c={c}, m={m}, a={a}, wf={w}", end="") + osc, t, z, v = run_oscillation_z(k=k, c=c, m=m, ampl=a, omega=w, x0=x0, tol=tol) + if c < 2.0: # only if damping is small enough + cp = 2.0 * pi / sqrt(k / m - (c / 2.0 / m) ** 2) + assert abs(osc.period[2] - cp) < 1e-12, f"Period[{2}]: {osc.period[2]} != {cp}. {osc.w2}, {osc.gam}" + x_expect, v_expect = [], [] + for ti in t: + _x, _v = forced_oscillator(ti, k, c, m, a, w, x0=x0) + x_expect.append(_x) + v_expect.append(_v) + if show: + do_show(t, z, v, x_expect, v_expect) + emax = 0.0 + for i, ti in enumerate(t): + assert abs(z[i] - x_expect[i]) < 50 * tol, f"@{ti}: z={z[i]} != {x_expect[i]}" + assert abs(v[i] - v_expect[i]) < 50 * tol, f"@{ti}: v={v[i]} != {v_expect[i]}" + emax = max(emax, abs(z[i] - x_expect[i]), abs(v[i] - v_expect[i])) + print(f". Max absolute error: {emax}") + + +def test_2d(show): + from examples.oscillator_xd import OscillatorXD + + def run_2d( + x0: tuple[float, float, float], + v0: tuple[float, float, float], + k: tuple[float, float, float] = (1.0, 1.0, 1.0), + c: tuple[float, float, float] = (0.0, 0.0, 0.0), + end: float = 100.0, + dt: float = 0.01, + tolerance: float = 1e-5, + ): + osc = OscillatorXD(dim=3, k=k, c=c, tolerance=tolerance) + osc.x[:3] = np.array(x0, float) # set initial 3D position + osc.x[3:] = np.array(v0, float) # set initial 3D speed + x, y = [], [] + t0 = 0.0 + for time in np.linspace(dt, end, int(end / dt), endpoint=True): + x.append(osc.x[0]) + y.append(osc.x[1]) + osc.do_step(time, time - t0) + t0 = time + x.append(osc.x[0]) + y.append(osc.x[1]) + + return (osc, x, y) + + def show_2d(x: list[float], y: list[float]): + fig, ax = plt.subplots() + ax.plot(x, y, label="x-y") + ax.legend() + plt.show() + + def area(x: list[float], y: list[float]): + """Calculate the area within the curve.""" + angle0 = 0.0 + area = 0.0 + anglesum = 0.0 + for _x, _y in zip(x, y, strict=False): + angle = atan2(_y, _x) + dangle = min((2 * np.pi) - abs(angle0 - angle), abs(angle0 - angle)) + area += (_x**2 + _y**2) * dangle / 2 + anglesum += dangle + angle0 = angle + return area + + osc, x, y = run_2d(x0=(1.0, 0.0, 0.0), v0=(0.0, 1.0, 0.0), end=2 * np.pi, tolerance=1e-5) + assert np.allclose(osc.period, (2 * np.pi, 2 * np.pi, 2 * np.pi)), f"Found {osc.period}" + assert abs(area(x, y) - np.pi) < 1e-10, f"Found area {area(x, y)}" + if show: + show_2d(x, y) + for _x, _y in zip(x, y, strict=False): + assert abs(_x**2 + _y**2 - 1.0) < 1e-10, f"Found {_x}**2 + {_y}**2 = {_x**2 + _y**2} != 1.0" + + osc, x, y = run_2d(x0=(1.0, 0.0, 0.0), v0=(0.0, 1.0, 0.0), c=(0.5, 0.5, 0), end=10 * np.pi) + assert (area(x, y) - 0.9977641389836932) < 1e-15 + + if show: + show_2d(x, y) + + osc, x, y = run_2d(x0=(1.0, 0.0, 0.0), v0=(0.0, 1.0, 0.0), k=(1.0, 1.0 / 16, 0), end=10 * np.pi) + assert abs(x[-1] - 1.0) < 1e-12, f"Found {x[-1]}" + assert abs(y[-1] - 4.0) < 1e-12, f"Found {y[-1]}" + osc, x, y = run_2d(x0=(1.0, 0.0, 0.0), v0=(0.0, 1.0, 0.0), k=(1.0, 1.0 / 15.8, 0), end=20 * np.pi) + if show: + show_2d(x, y) + + +def test_sweep_oscillator(show: bool = False): + """A forced oscillator where the force frequency is changed linearly as d_omega*time. + The test demonstrates that a monolithic simulation provides accurate results in all ranges of the force frequency. + Co-simulating the oscillator and the force, this does not work. + """ + osc, times0, z0, v0, f0 = sweep_oscillation_z( + k=1.0, + c=0.1, + m=1.0, + ampl=1.0, + d_omega=0.1, + x0=0.0, + v0=0.0, + dt=0.1, # 'ground truth', small dt + end=100.0, + tol=1e-3, + ) + with open(Path.cwd() / "oscillator_sweep0.dat", "w") as fp: + for i in range(len(times0)): + fp.write(f"{times0[i]}\t{z0[i]}\t{v0[i]}\t{f0[i]}\n") + + if show: + freq = [0.1 * t / 2 / np.pi for t in times0] + fig, ax = plt.subplots() + ax.plot(freq, z0, label="z0(t)") + ax.plot(freq, v0, label="v0(t)") + # ax.plot(freq, f0, label="F0(t)") + ax.legend() + plt.show() + + osc, times, z, v, f = sweep_oscillation_z( + k=1.0, + c=0.1, + m=1.0, + ampl=1.0, + d_omega=0.1, + x0=0.0, + v0=0.0, + dt=1, # dt similar to resonance frequency + end=100.0, + tol=1e-3, + ) + i0 = 0 + for i in range(len(times)): # demonstrate that the results are accurate, even if dt is large + t = times[i] + while abs(times0[i0] - t) > 1e-10: + i0 += 1 + assert times0[i0] - t < 0.1, f"Time entry for time {t} not found in times0" + + assert abs(z0[i0] - z[i]) < 4e-2, f"Time {t}. Found {z0[i0]} != {z[i]}" + assert abs(v0[i0] - v[i]) < 4e-2, f"Time {t}. Found {v0[i0]} != {v[i]}" + + if show: + fig, ax = plt.subplots() + ax.plot(times0, z0, label="z0(t)") + ax.plot(times, z, label="z(t)") + ax.legend() + plt.show() + + +def test_forced_xv(show: bool = False): + from examples.oscillator_xd import Force, OscillatorXD + + def do_scenario( + k: float = 1.0, + c: float = 0.0, + ax: float = 0.0, + av: float = 0.0, + const: float | None = None, + v0: float = 1.0, + show=show, + title: str = "Scenario", + ): + _f = partial(force_xv, dim=6, ampl_x=ax, ampl_v=av, const=const) + _force = Force(6, _f) + osc = OscillatorXD(dim=6, k=(k,) * 6, c=(c,) * 6, m=1.0, tolerance=1e-3, force=_force) + osc.v[2] = v0 # set initial z-speed + times, z, v = [], [], [] + time = 0.0 + dt = 0.1 + while time < 50.0: + times.append(time) + z.append(osc.x[2]) + v.append(osc.v[2]) + osc.do_step(time, dt) + time += dt + + if show: + fig, axis = plt.subplots() + axis.plot(times, z, label="z0(t)") + axis.plot(times, v, label="v0(t)") + plt.legend() + axis.set_title(title) + plt.show() + return (z, v) + + z, v = do_scenario(const=-10.0, v0=0.0, show=False, title="Constant force -10.0") + assert abs(sum(v) / len(v)) < 0.01, "Velocity still around +-0" + assert abs(sum(z) / len(z) + 10.0) < 0.06, "New equilibrium with constant force" + return + z0, v0 = do_scenario(show=False, title="Basic oscillator with start velocity 1.0") + z, v = do_scenario(k=0, ax=-1.0, show=False, title="Spring constant 0.0, replaced with force(position)") + assert np.allclose(z0, z), "Same effect as change in frequency when force dependent on position." + assert np.allclose(v0, v), "Same effect as change in frequency when force dependent on position." + z0, v0 = do_scenario(c=0.1, show=False, title="Basic oscillator with damping=0.1 and start veocity=1.0") + z, v = do_scenario(c=0.0, av=-0.1, show=False, title="Daming replaced by force(velocity)") + assert np.allclose(z0, z), "Same effect as change in damping when force dependent on velocity." + assert np.allclose(v0, v), "Same effect as change in damping when force dependent on velocity." + + +if __name__ == "__main__": + retcode = pytest.main(["-rA", "-v", "--rootdir", "../", "--show", "False", __file__]) + assert retcode == 0, f"Non-zero return code {retcode}" + import os + + os.chdir(Path(__file__).parent.absolute() / "test_working_directory") + # test_oscillator_class(show=True) + # test_2d(show=True) + # test_sweep_oscillator(show=True) + # test_forced_xv(show=True) diff --git a/tests/test_pint.py b/tests/test_pint.py index 40d2d86..4fd9a90 100644 --- a/tests/test_pint.py +++ b/tests/test_pint.py @@ -7,11 +7,13 @@ logger = logging.getLogger(__name__) -_reg = UnitRegistry(system="SI", autoconvert_offset_to_baseunit=True) # , auto_reduce_dimensions=True) +_reg: UnitRegistry = UnitRegistry(system="SI", autoconvert_offset_to_baseunit=True) # , auto_reduce_dimensions=True) def test_needed_functions(): - _reg = UnitRegistry(system="SI", autoconvert_offset_to_baseunit=True) # , auto_reduce_dimensions=True) + _reg: UnitRegistry = UnitRegistry( + system="SI", autoconvert_offset_to_baseunit=True + ) # , auto_reduce_dimensions=True) print("AVAILABLE UNITS", dir(_reg.sys.SI)) print( "degrees_Celsius defined?", diff --git a/tests/test_time_table.py b/tests/test_time_table.py index f108462..a12e40a 100644 --- a/tests/test_time_table.py +++ b/tests/test_time_table.py @@ -28,6 +28,8 @@ def arrays_equal( def test_time_table(show: bool = False): + from examples.time_table import TimeTable + tbl = TimeTable( data=((0.0, 1, 0, 0), (1.0, 1, 1, 1), (3.0, 1, 3, 9), (7.0, 1, 7, 49)), header=("x", "y", "z"), diff --git a/tests/test_make_time_table.py b/tests/test_time_table_fmu.py similarity index 63% rename from tests/test_make_time_table.py rename to tests/test_time_table_fmu.py index ce9aab4..8a79be8 100644 --- a/tests/test_make_time_table.py +++ b/tests/test_time_table_fmu.py @@ -9,12 +9,6 @@ from fmpy.simulation import simulate_fmu # type: ignore from fmpy.util import fmu_info, plot_result # type: ignore from fmpy.validation import validate_fmu # type: ignore -from libcosimpy.CosimEnums import CosimExecutionState -from libcosimpy.CosimExecution import CosimExecution -from libcosimpy.CosimLogging import CosimLogLevel, log_output_level -from libcosimpy.CosimManipulator import CosimManipulator # type: ignore -from libcosimpy.CosimObserver import CosimObserver # type: ignore -from libcosimpy.CosimSlave import CosimLocalSlave from pythonfmu.enums import Fmi2Causality as Causality from pythonfmu.enums import Fmi2Variability as Variability @@ -149,88 +143,13 @@ def test_use_fmu(time_table_fmu, show: bool = False): assert abs(result[i][3] - t**2) < 1e-10, f"Result for {ipol}, time={t}: {result[i][1]} != {i**2}" -def test_run_osp(time_table_fmu): - log_output_level(CosimLogLevel.DEBUG) - sim = CosimExecution.from_step_size(step_size=1e8) # empty execution object with fixed time step in nanos - st = CosimLocalSlave(fmu_path=str(time_table_fmu), instance_name="st") - - ist = sim.add_local_slave(st) - assert ist == 0, f"local slave number {ist}" - - reference_dict = {var_ref.name.decode(): var_ref.reference for var_ref in sim.slave_variables(ist)} - - # Set initial values - sim.boolean_initial_value(ist, reference_dict["interpolate"], True) - - sim_status = sim.status() - assert sim_status.current_time == 0 - assert CosimExecutionState(sim_status.state) == CosimExecutionState.STOPPED - - # Simulate for 1 second - sim.simulate_until(target_time=15e9) - - -def test_check_osp_system_structure(time_table_system_structure): - "Instantiate OSP from system structure" - log_output_level(CosimLogLevel.DEBUG) - simulator = CosimExecution.from_osp_config_file(str(time_table_system_structure)) - comps = [] - for comp in list(simulator.slave_infos()): - name = comp.name.decode() - comps.append(name) - assert comps == ["tab"], f"Components: {comps}" - variables = {} - for idx in range(simulator.num_slave_variables(0)): - struct = simulator.slave_variables(0)[idx] - variables.update( - { - struct.name.decode(): { - "reference": struct.reference, - "type": struct.type, - "causality": struct.causality, - "variability": struct.variability, - } - } - ) - assert variables["outs[0]"] == {"reference": 1, "type": 0, "causality": 2, "variability": 4} # similar: [1],[2] - assert variables["interpolate"] == {"reference": 0, "type": 1, "causality": 1, "variability": 1} - - -def test_run_osp_system_structure(time_table_system_structure): - "Run an OSP simulation in the same way as the SystemInterface of sim-explorer is implemented" - log_output_level(CosimLogLevel.TRACE) - for ipol in range(4): - simulator = CosimExecution.from_osp_config_file(str(time_table_system_structure)) # reset - simulator.integer_initial_value(0, 0, ipol) # set 'interpolate' - # manipulator and obeserver - manipulator = CosimManipulator.create_override() - simulator.add_manipulator(manipulator=manipulator) - observer = CosimObserver.create_last_value() - simulator.add_observer(observer=observer) - for time in np.linspace(0.1, 10, 100): - simulator.simulate_until(time * 1e9) - if time == 0.1: - assert observer.last_integer_values(0, [0]) == [ipol], ( - f"iPol {observer.last_integer_values(0, [0])} != {ipol}" - ) - if ipol == 0: - _x = observer.last_real_values(0, [1])[0] - assert _x == 1.0, f"Result for {ipol}: {_x} != 1.0" - elif ipol == 1: - _x = observer.last_real_values(0, [2])[0] - assert abs(_x - time) < 1e-10, f"Result for {ipol}: {_x} != {time}" - elif ipol == 2: - _x = observer.last_real_values(0, [3])[0] - assert abs(_x - time**2) < 1e-10, f"Result for {ipol}: {_x} != {time**2}" - - def test_make_with_new_data(): """Test and example how keyword arguments of the Model class can be used (changed) when building FMU.""" times = np.linspace(0, 2 * np.pi, 100) data = list(zip(times, np.cos(times), np.sin(times), strict=False)) build_path = Path.cwd() build_path.mkdir(exist_ok=True) - fmu_path = Model.build( + _ = Model.build( script=str(Path(__file__).parent.parent / "examples" / "time_table_fmu.py"), project_files=[Path(__file__).parent.parent / "examples" / "time_table.py"], dest=build_path, @@ -241,13 +160,11 @@ def test_make_with_new_data(): "default_experiment": {"startTime": 0, "stopTime": 2 * np.pi, "stepSize": 0.1, "tolerance": 1e-5}, }, ) - (build_path / "TimeTableFMU.fmu").replace(build_path / "NewDataFMU.fmu") - return fmu_path -@pytest.mark.skip(reason="Does so far not work within pytest, only stand-alone") +# @pytest.mark.skip(reason="Does so far not work within pytest, only stand-alone") def test_use_with_new_data(show): - fmu_path = Path(__file__).parent / "test_working_directory" / "NewDataFMU.fmu" + fmu_path = Path(__file__).parent / "test_working_directory" / "TimeTableFMU.fmu" result = simulate_fmu( # type: ignore[reportArgumentType] fmu_path, stop_time=2 * np.pi, @@ -278,8 +195,5 @@ def test_use_with_new_data(show): # test_time_table_fmu() # test_make_time_table(_time_table_fmu()) # test_use_fmu(_time_table_fmu(), show=True) - # test_run_osp(_time_table_fmu()) - # test_check_osp_system_structure(_time_table_system_structure(_time_table_fmu())) - # test_run_osp_system_structure(_time_table_system_structure(_time_table_fmu())) # test_make_with_new_data() # test_use_with_new_data(show=True) diff --git a/tests/test_transform.py b/tests/test_transform.py new file mode 100644 index 0000000..2e27d1c --- /dev/null +++ b/tests/test_transform.py @@ -0,0 +1,180 @@ +import logging + +import numpy as np +import pytest +from scipy.spatial.transform import Rotation as Rot + +from component_model.utils.transform import ( + cartesian_to_spherical, + euler_rot_spherical, + normalized, + rot_from_spherical, + rot_from_vectors, + spherical_to_cartesian, + spherical_unique, +) + +logger = logging.getLogger(__name__) +logging.basicConfig(level=logging.INFO) + + +def arrays_equal(arr1: np.ndarray | tuple | list, arr2: np.ndarray | tuple | list, dtype="float", eps=1e-7): + assert len(arr1) == len(arr2), "Length not equal!" + + for i in range(len(arr1)): + # assert type(arr1[i]) == type(arr2[i]), f"Array element {i} type {type(arr1[i])} != {type(arr2[i])}" + assert abs(arr1[i] - arr2[i]) < eps, f"Component {i}: {arr1[i]} != {arr2[i]}" + + +def tuples_nearly_equal(tuple1: tuple, tuple2: tuple, eps=1e-10): + """Check whether the values in tuples (of any tuple-structure) are nearly equal""" + assert isinstance(tuple1, tuple), f"{tuple1} is not a tuple" + assert isinstance(tuple2, tuple), f"{tuple2} is not a tuple" + assert len(tuple1) == len(tuple2), f"Lenghts of tuples {tuple1}, {tuple2} not equal" + for t1, t2 in zip(tuple1, tuple2, strict=False): + if isinstance(t1, tuple): + assert isinstance(t2, tuple), f"Tuple expected. Found {t2}" + assert tuples_nearly_equal(t1, t2) + elif isinstance(t1, float) or isinstance(t2, float): + assert t1 == t2 or abs(t1 - t2) < eps, f"abs({t1} - {t2}) >= {eps}" + else: + assert t1 == t2, f"{t1} != {t2}" + return True + + +def test_spherical_cartesian(): + for vec in [ + (0, 0, 0), + (0, 0, 1), + (0, 1, 0), + (0, 1, 1), + (1, 0, 0), + (1, 0, 1), + (1, 1, 0), + (1, 1, 1), + ]: + sVec = cartesian_to_spherical(vec) + _vec = spherical_to_cartesian(sVec) + arrays_equal(np.array(vec, dtype="float"), _vec) + + +def test_spherical_unique(): + def do_test(x0: tuple, x1: tuple): + if len(x0) == 3: + _unique = spherical_unique(np.append(x0[0], np.radians(x0[1:]))) + unique = np.append(_unique[0], np.degrees(_unique[1:])) + else: + unique = np.degrees(spherical_unique(np.radians(x0))) + assert np.allclose(unique, x1), f"{x0} -> {list(unique)} != {list(x1)}" + + do_test((0, 99), (0.0, 0.0)) + do_test((-1, 10, 20), (1, 180 - 10.0, 180 + 20)) + do_test((10, 300), (10, 300)) + do_test((190, 300), (170.0, 300.0 + 180 - 360)) + do_test((170, 300), (170.0, 300.0)) + do_test((170, 720), (170.0, 0.0)) + + +def test_rot_from_spherical(): + assert np.allclose(rot_from_spherical((0, 0)).as_matrix(), Rot.identity().as_matrix()) + assert np.allclose(rot_from_spherical((90, 0), True).apply((0, 0, 1)), (1.0, 0, 0)) + assert np.allclose(rot_from_spherical((0, 90), True).apply((0, 0, 1)), (0.0, 0, 1.0)) + assert np.allclose(rot_from_spherical((0, 90), True).apply((1.0, 0, 0)), (0.0, 1.0, 0.0)) + assert np.allclose(rot_from_spherical((1.0, 45, 45), True).apply((0.0, 0, 1.0)), (0.5, 0.5, np.sqrt(2) / 2)) + down = rot_from_spherical((180, 0), True) + print("down+2", down, down * rot_from_spherical((2, 0), True)) + + +def test_rot_from_vectors(): + def do_check(vec1: tuple | list | np.ndarray, vec2: tuple | list | np.ndarray): + v1 = np.array(vec1, float) + v2 = np.array(vec2, float) + r = rot_from_vectors(v1, v2) + v = r.apply(v1) + assert np.allclose(v2, v), f"{r.as_matrix} does not turn {v1} into {v2}" + + do_check((1, 0, 0), (-1, 0, 0)) + return + do_check((1, 0, 0), (0, 1, 0)) + rng = np.random.default_rng(12345) + for _i in range(100): + v1 = normalized(rng.random(3)) + do_check(v1, -v1) # opposite vectors + do_check(v1, v1) # identical vectors + for _i in range(1000): + do_check(normalized(rng.random(3)), normalized(rng.random(3))) + + +def test_euler_rot_spherical(): + """Test euler rotations. + Note: We use XYZ + (roll, pitch, yaw) convention Tait-Brian.""" + _re = Rot.from_euler("zyx", (20, 40, 60), degrees=True) # extrinsic rotation + _ri = Rot.from_euler("XYZ", (60, 40, 20), degrees=True) # intrinsic rotation + assert np.allclose(_re.as_matrix(), _ri.as_matrix()), "Rotation matrices for extrinsic == intrisic+reversed" + _re_inv = Rot.from_euler("xyz", (-60, -40, -20), degrees=True) + assert np.allclose(_re.as_matrix(), _re_inv.as_matrix().transpose()), "_re_inv is inverse to _re" + assert np.allclose(_re.as_matrix() @ _re_inv.as_matrix(), Rot.identity(3).as_matrix()), "_re_inv is the inverse." + + assert np.allclose(Rot.from_euler("XYZ", (90, 0, 0), degrees=True).apply((1, 0, 0)), (1, 0, 0)), "Roll invariant x" + assert np.allclose(Rot.from_euler("XYZ", (90, 0, 0), degrees=True).apply((0, 1, 0)), (0, 0, 1)), ( + "Roll y(SB) -> z(down)" + ) + assert np.allclose(Rot.from_euler("XYZ", (90, 0, 0), degrees=True).apply((0, 0, -1)), (0, 1, 0)), ( + "Roll -z(up) -> y(SB)" + ) + assert np.allclose(Rot.from_euler("XYZ", (0, 90, 0), degrees=True).apply((1, 0, 0)), (0, 0, -1)), ( + "Pitch x(FW) -> -z(up)" + ) + assert np.allclose(Rot.from_euler("XYZ", (0, 90, 0), degrees=True).apply((0, 1, 0)), (0, 1, 0)), "Pitch invariant y" + assert np.allclose(Rot.from_euler("XYZ", (0, 90, 0), degrees=True).apply((0, 0, 1)), (1, 0, 0)), ( + "Pitch z(down) -> x(FW)" + ) + assert np.allclose(Rot.from_euler("XYZ", (0, 0, 90), degrees=True).apply((1, 0, 0)), (0, 1, 0)), ( + "Yaw x(FW) -> y(SB)" + ) + assert np.allclose(Rot.from_euler("XYZ", (0, 0, 90), degrees=True).apply((0, 1, 0)), (-1, 0, 0)), ( + "Yaw y(SB) -> -x(BW)" + ) + assert np.allclose(Rot.from_euler("XYZ", (0, 0, 90), degrees=True).apply((0, 0, 1)), (0, 0, 1)), "Yaw invariant z" + assert np.allclose(np.degrees(euler_rot_spherical((90, 0, 0), (90, 90), degrees=True)), (0, 0)), "Roll y -> z" + assert np.allclose(np.degrees(euler_rot_spherical((0, 90, 0), (0, 0), degrees=True)), (90, 0)), "Pitch z -> x" + assert np.allclose(np.degrees(euler_rot_spherical((0, 0, 90), (90, 0), degrees=True)), (90, 90)), "Yaw x -> y" + + +def test_euler_rot(): + """Test general issues about 3D rotations.""" + _rot = Rot.from_euler("XYZ", (90, 0, 0), degrees=True) # roll 90 deg + assert np.allclose(_rot.apply((0, 0, 1)), (0, -1, 0)), "z -> -y" + _rot2 = Rot.from_euler("XYZ", (90, 0, 0), degrees=True) * _rot # another 90 deg in same direction + assert np.allclose(_rot2.apply((0, 0, 1)), (0, 0, -1)), "z -> -z" + assert np.allclose(_rot2.apply((0, 0, 1)), Rot.from_euler("XYZ", (180, 0, 0), degrees=True).apply((0, 0, 1))), ( + "Angles added" + ) + _rot2 = _rot.from_euler("XYZ", (0, 90, 0), degrees=True) * _rot # + pitch 90 deg + print(_rot2.as_euler(seq="XYZ", degrees=True)) + with pytest.warns(UserWarning, match="Gimbal lock detected"): + print(Rot.from_euler("XYZ", (90, 90, 0), degrees=True).as_euler(seq="XYZ", degrees=True)) + _rot3 = Rot.from_euler("XYZ", (0, 0, 90), degrees=True) * _rot2 # +yaw 90 deg + assert np.allclose(_rot3.apply((1, 0, 0)), (0, 0, -1)) + assert np.allclose(_rot3.apply((0, 1, 0)), (0, 1, 0)) + assert np.allclose(_rot3.apply((0, 0, 1)), (1, 0, 0)) + assert np.allclose(np.cross(_rot3.apply((1, 0, 0)), _rot3.apply((0, 1, 0))), _rot3.apply((0, 0, 1))), ( + "Still right-hand" + ) + + +def test_normalized(): + assert np.allclose(normalized(np.array((1, 0, 0), float)), (1, 0, 0)) + assert np.allclose(normalized(np.array((1, 1, 1), float)), np.array((1, 1, 1), float) / np.sqrt(3)) + + +if __name__ == "__main__": + retcode = 0 # pytest.main(["-rP -s -v", __file__]) + assert retcode == 0, f"Return code {retcode}" + # test_spherical_cartesian() + # test_spherical_unique() + # test_rot_from_spherical() + # test_rot_from_vectors() + # test_euler_rot_spherical() + test_euler_rot() + # test_normalized() diff --git a/tests/test_unit.py b/tests/test_unit.py new file mode 100644 index 0000000..7e59be4 --- /dev/null +++ b/tests/test_unit.py @@ -0,0 +1,105 @@ +from math import degrees, radians + +import pytest +from pint import UnitRegistry + +from component_model.variable import Unit + + +@pytest.fixture +def ureg(scope="module", autouse=True): + return _ureg() + + +def _ureg(): + return UnitRegistry(system="SI", autoconvert_offset_to_baseunit=True) + + +def test_parsing(ureg): + u1 = Unit() + # default values: + assert u1.u == "" + assert u1.du is None + val = u1.parse_quantity("9.9m", ureg) + assert val == 9.9 + assert u1.u == "meter" + assert u1.du is None + val = u1.parse_quantity("9.9inch", ureg) + assert val == u1.to_base(9.9), f"Found val={val}" + assert u1.u == "meter" + assert u1.du == "inch" + assert abs(123.456 - u1.to_base(u1.from_base(123.456))) < 1e-13, f"Found {u1.to_base(u1.from_base(123.456))}" + val = u1.parse_quantity("99.0%", ureg) + assert val == 0.99 + assert u1.u == "dimensionless" + assert u1.du == "percent" + assert str(u1) == "Unit dimensionless, display:percent. Offset:0.0, factor:0.01" + + +def test_make(ureg): + val, unit = Unit.make("2m", ureg) + assert val[0] == 2 + assert unit[0].u == "meter", f"Found {unit[0].u}" + assert unit[0].du is None + val, unit = Unit.make("Hello World", ureg=ureg, typ=str) + assert val[0] == "Hello World" + assert unit[0].u == "dimensionless" + assert unit[0].du is None + val, unit = Unit.make("99.0%", ureg=ureg) + assert val[0] == 0.99 + assert unit[0].u == "dimensionless" + assert unit[0].du == "percent" + + +def test_make_tuple(ureg): + vals, units = Unit.make_tuple(("2m", "3deg", "0.0 degF"), ureg) + k2degc = 273.15 + assert units[0].u == "meter" + assert units[0].du is None + assert vals[0] == 2 + assert units[1].u == "radian", f"Found {units[1].u}" + assert units[1].du == "degree" + assert units[1].to_base(1.0) == radians(1.0) + assert units[1].from_base(1.0) == degrees(1.0) + assert vals[1] == radians(3) + assert units[2].u == "kelvin", f"Found {units[2].u}" + assert units[2].du == "degree_Fahrenheit", f"Found {units[2].du}" + assert abs(units[2].from_base(k2degc) - (k2degc * 9 / 5 - 459.67)) < 1e-10 + assert abs(units[2].to_base(0.0) - (0.0 + 459.67) * 5 / 9) < 1e-10, ( + f"Found {units[2].to_base(0.0)}, {(0.0 + 459.67) * 5 / 9}" + ) + + +def test_derivative(ureg): + bv, bu = Unit.make_tuple(("2m", "3deg"), ureg) + vals, units = Unit.derivative(bu) + assert vals == (0.0, 0.0) + assert units[0].u == "meter/s" + assert units[0].du is None + assert units[1].u == "radian/s", f"Found {units[1].u}" + assert units[1].du == "degree/s" + assert units[1].to_base == bu[1].to_base + assert units[1].from_base == bu[1].from_base + + +def test_compatible(ureg): + v, u = Unit.make_tuple(("2m", "3deg"), ureg) + ck, q = u[0].compatible("4m", ureg, strict=True) + assert ck + assert q == 4 + ck, q = u[1].compatible("5 radian", ureg, strict=True) + assert not ck, "Not compatible for 'strict'" + ck, q = u[1].compatible("5 radian", ureg, strict=False) + assert ck, "Ok for non-strict" + ck, q = u[0].compatible("5 radian", ureg, strict=False) + assert not ck, "Totally wrong units" + + +if __name__ == "__main__": + retcode = 0 # pytest.main(["-rP -s -v", __file__]) + assert retcode == 0, f"Return code {retcode}" + # test_parsing(_ureg()) + # test_make(_ureg()) + # test_make_tuple( _ureg()) + # test_derivative( _ureg()) + test_compatible(_ureg()) diff --git a/tests/test_utils.py b/tests/test_utils.py index 46ffe39..15688d4 100644 --- a/tests/test_utils.py +++ b/tests/test_utils.py @@ -28,12 +28,24 @@ def dicts_equal(d1: dict, d2: dict): assert isinstance(d2, dict), f"Dict expected. Found {d2}" for key in d1: assert key in d2, f"Key {key} not found in {d2}" - if key != "copyright": # copyright changes with the year! - assert d1[key] == d2[key], f"Value of key {key} {d1[key]} != {d2[key]}" + if key == "copyright": + continue # copyright changes with the year! + if key == "license": + assert " ".join(str(d1[key]).split()) == " ".join(str(d2[key]).split()), ( + f"Value of key {key} differs after whitespace normalization\n{d1[key]!r}\n!=\n{d2[key]!r}" + ) + continue + assert d1[key] == d2[key], f"Value of key {key} {d1[key]} != {d2[key]}" for key in d2: assert key in d1, f"Key {key} not found in {d1}" - if key != "copyright": # copyright changes with the year! - assert d1[key] == d2[key], f"Value of key {key} {d1[key]} != {d2[key]}" + if key == "copyright": + continue # copyright changes with the year! + if key == "license": + assert " ".join(str(d1[key]).split()) == " ".join(str(d2[key]).split()), ( + f"Value of key {key} differs after whitespace normalization\n{d1[key]!r}\n!=\n{d2[key]!r}" + ) + continue + assert d1[key] == d2[key], f"Value of key {key} {d1[key]} != {d2[key]}" def test_xml_to_python_val(): @@ -77,7 +89,7 @@ def test_model_description(bouncing_ball_fmu): "canNotUseMemoryManagementFunctions": "true", }, ) - assert el.find("./SourceFiles") is not None, "SourceFiles expected" + # assert el.find("./SourceFiles") is not None, "SourceFiles expected" el = et.find("./UnitDefinitions") assert el is not None, "UnitDefinitions element expected" assert len(el) == 4, f"4 UnitDefinitions expected. Found {el}" @@ -152,9 +164,9 @@ def test_variables_from_fmu(bouncing_ball_fmu): if __name__ == "__main__": - retcode = 0 # pytest.main(["-rA", "-v", __file__]) + retcode = pytest.main(["-rA", "-v", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" # import os # os.chdir( Path(__file__).parent / "test_working_directory") # test_model_from_fmu(_bouncing_ball_fmu()) - test_model_description(_bouncing_ball_fmu()) + # test_model_description(_bouncing_ball_fmu()) diff --git a/tests/test_variable.py b/tests/test_variable.py index 9e56c43..d8a8b03 100644 --- a/tests/test_variable.py +++ b/tests/test_variable.py @@ -3,22 +3,27 @@ import math import xml.etree.ElementTree as ET # noqa: N817 from enum import Enum +from typing import Sequence import numpy as np import pytest from pythonfmu.enums import Fmi2Causality as Causality # type: ignore from pythonfmu.enums import Fmi2Initial as Initial # type: ignore from pythonfmu.enums import Fmi2Variability as Variability # type: ignore +from scipy.spatial.transform import Rotation as Rot from component_model.model import Model -from component_model.variable import ( - Check, - Variable, - VariableInitError, - VariableRangeError, +from component_model.utils.analysis import extremum, extremum_series +from component_model.utils.transform import ( cartesian_to_spherical, + euler_rot_spherical, + normalized, + rot_from_spherical, + rot_from_vectors, spherical_to_cartesian, + spherical_unique, ) +from component_model.variable import Check, Variable logger = logging.getLogger(__name__) logging.basicConfig(level=logging.INFO) @@ -123,6 +128,110 @@ def test_spherical_cartesian(): arrays_equal(np.array(vec, dtype="float"), _vec) +def test_spherical_unique(): + def do_test(x0: tuple, x1: tuple): + if len(x0) == 3: + _unique = spherical_unique(np.append(x0[0], np.radians(x0[1:]))) + unique = np.append(_unique[0], np.degrees(_unique[1:])) + else: + unique = np.degrees(spherical_unique(np.radians(x0))) + assert np.allclose(unique, x1), f"{x0} -> {list(unique)} != {list(x1)}" + + do_test((0, 99), (0.0, 0.0)) + do_test((-1, 10, 20), (1, 180 - 10.0, 180 + 20)) + do_test((10, 300), (10, 300)) + do_test((190, 300), (170.0, 300.0 + 180 - 360)) + do_test((170, 300), (170.0, 300.0)) + do_test((170, 720), (170.0, 0.0)) + + +def test_rot_from_spherical(): + assert np.allclose(rot_from_spherical((0, 0)).as_matrix(), Rot.identity().as_matrix()) + assert np.allclose(rot_from_spherical((90, 0), True).apply((0, 0, 1)), (1.0, 0, 0)) + assert np.allclose(rot_from_spherical((0, 90), True).apply((0, 0, 1)), (0.0, 0, 1.0)) + assert np.allclose(rot_from_spherical((0, 90), True).apply((1.0, 0, 0)), (0.0, 1.0, 0.0)) + assert np.allclose(rot_from_spherical((1.0, 45, 45), True).apply((0.0, 0, 1.0)), (0.5, 0.5, np.sqrt(2) / 2)) + down = rot_from_spherical((180, 0), True) + print("down+2", down, down * rot_from_spherical((2, 0), True)) + + +def test_rot_from_vectors(): + def do_check(vec1: Sequence, vec2: Sequence): + v1 = np.array(vec1, float) + v2 = np.array(vec2, float) + r = rot_from_vectors(v1, v2) + v = r.apply(v1) + assert np.allclose(v2, v), f"{r.as_matrix} does not turn {v1} into {v2}" + + do_check((1, 0, 0), (-1, 0, 0)) + return + do_check((1, 0, 0), (0, 1, 0)) + rng = np.random.default_rng(12345) + for _i in range(100): + v1 = normalized(rng.random(3)) + do_check(v1, -v1) # opposite vectors + do_check(v1, v1) # identical vectors + for _i in range(1000): + do_check(normalized(rng.random(3)), normalized(rng.random(3))) + + +def test_euler_rot_spherical(): + """Test euler rotations. + Note: We use XYZ + (roll, pitch, yaw) convention Tait-Brian.""" + _re = Rot.from_euler("zyx", (20, 40, 60), degrees=True) # extrinsic rotation + _ri = Rot.from_euler("XYZ", (60, 40, 20), degrees=True) # intrinsic rotation + assert np.allclose(_re.as_matrix(), _ri.as_matrix()), "Rotation matrices for extrinsic == intrisic+reversed" + _re_inv = Rot.from_euler("xyz", (-60, -40, -20), degrees=True) + assert np.allclose(_re.as_matrix(), _re_inv.as_matrix().transpose()), "_re_inv is inverse to _re" + assert np.allclose(_re.as_matrix() @ _re_inv.as_matrix(), Rot.identity(3).as_matrix()), "_re_inv is the inverse." + + assert np.allclose(Rot.from_euler("XYZ", (90, 0, 0), degrees=True).apply((1, 0, 0)), (1, 0, 0)), "Roll invariant x" + assert np.allclose(Rot.from_euler("XYZ", (90, 0, 0), degrees=True).apply((0, 1, 0)), (0, 0, 1)), ( + "Roll y(SB) -> z(down)" + ) + assert np.allclose(Rot.from_euler("XYZ", (90, 0, 0), degrees=True).apply((0, 0, -1)), (0, 1, 0)), ( + "Roll -z(up) -> y(SB)" + ) + assert np.allclose(Rot.from_euler("XYZ", (0, 90, 0), degrees=True).apply((1, 0, 0)), (0, 0, -1)), ( + "Pitch x(FW) -> -z(up)" + ) + assert np.allclose(Rot.from_euler("XYZ", (0, 90, 0), degrees=True).apply((0, 1, 0)), (0, 1, 0)), "Pitch invariant y" + assert np.allclose(Rot.from_euler("XYZ", (0, 90, 0), degrees=True).apply((0, 0, 1)), (1, 0, 0)), ( + "Pitch z(down) -> x(FW)" + ) + assert np.allclose(Rot.from_euler("XYZ", (0, 0, 90), degrees=True).apply((1, 0, 0)), (0, 1, 0)), ( + "Yaw x(FW) -> y(SB)" + ) + assert np.allclose(Rot.from_euler("XYZ", (0, 0, 90), degrees=True).apply((0, 1, 0)), (-1, 0, 0)), ( + "Yaw y(SB) -> -x(BW)" + ) + assert np.allclose(Rot.from_euler("XYZ", (0, 0, 90), degrees=True).apply((0, 0, 1)), (0, 0, 1)), "Yaw invariant z" + assert np.allclose(np.degrees(euler_rot_spherical((90, 0, 0), (90, 90), degrees=True)), (0, 0)), "Roll y -> z" + assert np.allclose(np.degrees(euler_rot_spherical((0, 90, 0), (0, 0), degrees=True)), (90, 0)), "Pitch z -> x" + assert np.allclose(np.degrees(euler_rot_spherical((0, 0, 90), (90, 0), degrees=True)), (90, 90)), "Yaw x -> y" + + +def test_euler_rot(): + """Test general issues about 3D rotations.""" + _rot = Rot.from_euler("XYZ", (90, 0, 0), degrees=True) # roll 90 deg + assert np.allclose(_rot.apply((0, 0, 1)), (0, -1, 0)), "z -> -y" + _rot2 = Rot.from_euler("XYZ", (90, 0, 0), degrees=True) * _rot # another 90 deg in same direction + assert np.allclose(_rot2.apply((0, 0, 1)), (0, 0, -1)), "z -> -z" + assert np.allclose(_rot2.apply((0, 0, 1)), Rot.from_euler("XYZ", (180, 0, 0), degrees=True).apply((0, 0, 1))), ( + "Angles added" + ) + _rot2 = _rot.from_euler("XYZ", (0, 90, 0), degrees=True) * _rot # + pitch 90 deg + print(_rot2.as_euler(seq="XYZ", degrees=True)) + print(Rot.from_euler("XYZ", (90, 90, 0), degrees=True).as_euler(seq="XYZ", degrees=True)) + _rot3 = Rot.from_euler("XYZ", (0, 0, 90), degrees=True) * _rot2 # +yaw 90 deg + assert np.allclose(_rot3.apply((1, 0, 0)), (0, 0, -1)) + assert np.allclose(_rot3.apply((0, 1, 0)), (0, 1, 0)) + assert np.allclose(_rot3.apply((0, 0, 1)), (1, 0, 0)) + assert np.allclose(np.cross(_rot3.apply((1, 0, 0)), _rot3.apply((0, 1, 0))), _rot3.apply((0, 0, 1))), ( + "Still right-hand" + ) + + def init_model_variables(): """Define model and a few variables for various tests""" mod = DummyModel("MyModel") @@ -239,16 +348,14 @@ def test_init(): # internally packed into tuple: assert int1.start == (99,) assert int1.range == ((0, 100),) - assert int1.unit == ("dimensionless",) - assert int1.display == (None,) + assert int1.unit[0].u == "dimensionless" + assert int1.unit[0].du is None assert int1.check_range([50]) assert not int1.check_range([110]) assert mod.int1 == 99, "Value directly accessible as model variable" mod.int1 = 110 assert mod.int1 == 110, "Internal changes not range-checked!" - with pytest.raises(VariableRangeError) as err: # ... but getter() detects the range error - _ = int1.getter() - assert str(err.value) == "getter(): Value [110] outside range." + assert not int1.check_range(int1.getter(), -1), "Error detected during getter()" assert mod.int1 == 110, f"Value {mod.int1} should still be unchanged" int1.setter([50]) assert mod.int1 == 50, f"Value {mod.int1} changed back." @@ -263,19 +370,17 @@ def test_init(): # internally packed into tuple: assert float1.start == (0.99,) assert float1.range == ((0, 99.0),), f"Range: {float1.range} in display units." - assert float1.unit == ("dimensionless",) - assert float1.display[0][0] == "percent", f"Display: {float1.display[0][0]}" - assert float1.display[0][1](99) == 0.99, "Transform from dimensionless to percent" - assert float1.display[0][2](0.99) == 99, "... and back." + assert float1.unit[0].u == "dimensionless" + assert float1.unit[0].du == "percent", f"Display: {float1.unit[0].du}" + assert float1.unit[0].to_base(99) == 0.99, "Transform to dimensionless" + assert float1.unit[0].from_base(0.99) == 99, "... and back." assert float1.check_range([0.5]) assert not float1.check_range([1.0], disp=False), "Check as internal units" assert not float1.check_range([100.0]), "Check as display units" assert mod.float1 == 0.99, "Value directly accessible as model variable" mod.float1 = 1.0 assert mod.float1 == 1.0, "Internal changes not range-checked!" - with pytest.raises(VariableRangeError) as err: # ... but getter() detects the range error - _ = float1.getter() - assert str(err.value) == "getter(): Value [100.0] outside range." + assert not float1.check_range(float1.getter(), -1), "Range check during getter detects the range errror" assert mod.float1 == 1.0, f"Value {mod.float1} should still be unchanged" float1.setter([50]) assert mod.float1 == 0.5, f"Value {mod.float1} changed back." @@ -291,8 +396,8 @@ def test_init(): # internally packed into tuple: assert enum1.start == (Causality.parameter,) assert enum1.range == ((0, 4),), f"Range: {enum1.range}" - assert enum1.unit == ("dimensionless",) - assert enum1.display[0] is None, f"Display: {enum1.display[0]}" + assert enum1.unit[0].u == "dimensionless" + assert enum1.unit[0].du is None, f"Display: {enum1.unit[0].du}" assert enum1.check_range([1]) assert not enum1.check_range([7]) assert mod.enum1 == Causality.parameter, f"Value {mod.enum1} directly accessible as model variable" @@ -311,8 +416,8 @@ def test_init(): # internally packed into tuple: assert bool1.start == (True,) assert bool1.range == ((False, True),) - assert bool1.unit == ("dimensionless",) - assert bool1.display == (None,) + assert bool1.unit[0].u == "dimensionless" + assert bool1.unit[0].du is None assert bool1.check_range([True]) assert bool1.check_range([100.5]), "Any number will work" assert not bool1.check_range("Hei"), "But non-numbers are rejected" @@ -333,8 +438,8 @@ def test_init(): # internally packed into tuple: assert str1.start == ("Hello World!",) assert str1.range == (("", ""),), f"Range: {str1.range}. Basically irrelevant" - assert str1.unit == ("dimensionless",), f"Unit {str1.unit}" - assert str1.display[0] is None, f"Display: {str1.display[0]}" + assert str1.unit[0].u == "dimensionless", f"Unit {str1.unit}" + assert str1.unit[0].du is None, f"Display: {str1.unit[0].du}" assert str1.check_range([0.5]), "Everything is ok" assert mod.str1 == "Hello World!", f"Value {mod.str1} directly accessible as model variable" mod.str1 = 1.0 # type: ignore # intentional misuse @@ -358,19 +463,18 @@ def test_init(): tuples_nearly_equal(np1.range, ((0, 3), (1, 5), (float("-inf"), 5))) assert not np1.check_range([5.1], idx=1), "Checks performed on display units!" assert not np1.check_range([0.9], idx=1), "Checks performed on display units!" - assert np1.unit == ("meter", "radian", "radian"), f"Units: {np1.unit}" - assert isinstance(np1.display, tuple) and len(np1.display) == 3, "Tuple of length 3 expected" - assert np1.display[0] is None - assert np1.display[1][0] == "degree" - assert np1.display[2] is None + assert tuple(x.u for x in np1.unit) == ("meter", "radian", "radian"), f"Units: {np1.unit}" + disp = tuple(x.du for x in np1.unit) + assert isinstance(disp, tuple) and len(disp) == 3, "Tuple of length 3 expected" + assert disp[0] is None + assert disp[1] == "degree" + assert disp[2] is None assert np1.check_range((2, 3.5, 4.5)) assert not np1.check_range((2, 3.5, 6.3), -1), f"Range is {np1.range}" assert mod.np1[1] == math.radians(2), "Value directly accessible as model variable" mod.np1[1] = -1.0 assert mod.np1[1] == -1.0, "Internal changes not range-checked!" - with pytest.raises(VariableRangeError) as err: # ... but getter() detects the range error - _ = np1.getter() - assert str(err.value) == "getter(): Value [1.0, -57.29577951308233, 3.0] outside range." + assert not np1.check_range(np1.getter(), 1), "Error detected during getter()" assert mod.np1[1] == -1.0, f"Value {mod.np1} should still be unchanged" mod.np1 = np.array((1.5, 2.5, 3.5), float) assert np.linalg.norm(mod.np1) == math.sqrt(1.5**2 + 2.5**2 + 3.5**2), "np calculations are done on value" @@ -398,7 +502,7 @@ def test_init(): ) assert err2.value.args[0] == "Variable int1 already used as index 0 in model MyModel" - with pytest.raises(VariableInitError) as err3: + with pytest.raises(KeyError) as err3: int1 = Variable( mod, "bool1", @@ -410,7 +514,7 @@ def test_init(): annotations=None, typ=int, ) - assert err3.value.args[0].startswith("Range must be specified for int variable") + assert err3.value.args[0].startswith("Variable bool1 already used") assert float1.range[0][1] == 99.0 assert enum1.range[0] == (0, 4) assert enum1.check_range([Causality.parameter]) @@ -421,24 +525,18 @@ def test_init(): def test_range(): """Test the various ways of providing a range for a variable""" mod = DummyModel("MyModel2", instance_name="MyModel2") - with pytest.raises(VariableInitError) as err: - int1 = Variable(mod, "int1", start=1) - assert ( - str(err.value) - == "Range must be specified for int variable or use float." - ) - int1 = Variable(mod, "int1", start=1, rng=(0, 5)) # that works - mod.int1 = 6 - with pytest.raises(VariableRangeError) as err2: # causes an error - _ = int1.getter() - assert err2.value.args[0] == "getter(): Value [6.0] outside range." + int1 = Variable(mod, "int1", start=1) + assert int1.range == ((1, 1),), "Missing range. restricted to fixed start value." + int2 = Variable(mod, "int2", start=1, rng=(0, 5)) + assert int2.range == ((0, 5),), "That works" + mod.int2 = 6 + assert not int2.check_range(int2.getter(), -1), "Error detected in getter() function" float1 = Variable(mod, "float1", start=1, typ=float) # explicit type assert float1.range == ((float("-inf"), float("inf")),), "Auto_extreme. Same as rng=()" float2 = Variable(mod, "float2", start=1.0, rng=None) # implicit type through start value and no range assert float2.range == ((1.0, 1.0),), "No range." - with pytest.raises(VariableRangeError) as err3: - float2.setter([2.0]) - assert err3.value.args[0] == "set(): values [2.0] outside range." + mod.float2 = 99.9 # type: ignore[reportAttributeAccessIssue] ## values are accessible through model + assert not float2.check_range(float2.getter(), -1), "Error detected in getter() function" np1 = Variable(mod, "np1", start=("1.0m", 2, 3), rng=((0, "3m"), None, tuple())) assert np1.range == ((0.0, 3.0), (2.0, 2.0), (float("-inf"), float("inf"))) @@ -595,9 +693,9 @@ def test_xml(): assert len(lst) == 3 expected = '' assert ET.tostring(lst[0], encoding="unicode") == expected, ET.tostring(lst[0], encoding="unicode") - expected = '' + expected = '' assert ET.tostring(lst[1], encoding="unicode") == expected, ET.tostring(lst[1], encoding="unicode") - expected = '' + expected = '' assert ET.tostring(lst[2], encoding="unicode") == expected, ET.tostring(lst[2], encoding="unicode") int1 = Variable( @@ -639,6 +737,29 @@ def test_on_set(): arrays_equal(mod.np2, (0.9 * 0.9 * 4, 0.9 * 7, 0.9 * 8)) +def test_normalized(): + assert np.allclose(normalized(np.array((1, 0, 0), float)), (1, 0, 0)) + assert np.allclose(normalized(np.array((1, 1, 1), float)), np.array((1, 1, 1), float) / np.sqrt(3)) + + +def test_extremum(): + t = [np.radians(10 * x) for x in range(100)] + x = [np.cos(x) for x in t] + e, p = extremum(t[0:3], x[0:3], 2e-3) # allow a small error + assert e == 1 + assert p[0] > -2e-3 and p[1] < 1 + 1e-6, ( + "Top of parabola somewhat to the left due to cos not exactly equal to 2.order" + ) + # for i in range(100): + # print(i, t[i], x[i]) + e, p = extremum(t[17:20], x[17:20]) + assert e == -1 and abs(p[0] - np.pi) < 1e-10 and np.isclose(p[1], -1) + ex = extremum_series(t, x, "all") + assert len(ex) == 2 + assert np.allclose(ex[0], (12.566370614359142, 1.0)) + assert np.allclose(ex[1], (15.707963267948958, -1.0)) + + if __name__ == "__main__": retcode = pytest.main(["-rP -s -v", __file__]) assert retcode == 0, f"Return code {retcode}" @@ -654,3 +775,10 @@ def test_on_set(): # test_set() # test_on_set() # test_xml() + # test_spherical_unique() + # test_rot_from_spherical() + # test_rot_from_vectors() + # test_euler_rot_spherical() + # test_euler_rot() + # test_normalized() + # test_extremum() diff --git a/tests/test_variable_naming.py b/tests/test_variable_naming.py index 96515c2..652a967 100644 --- a/tests/test_variable_naming.py +++ b/tests/test_variable_naming.py @@ -8,33 +8,73 @@ logging.basicConfig(level=logging.INFO) +def test_single(): + which = -1 + if which == -1 or which == 1: + parsed = ParsedVariable("der(pos,1)", VariableNamingConvention.structured) + assert parsed.as_string() == "der(pos)" + assert parsed.as_tuple() == (None, "pos", [], 1) + assert parsed.var == "pos" + if which == -1 or which == 2: + parsed = ParsedVariable("der(pos,2)", VariableNamingConvention.structured) + assert parsed.as_string() == "der(pos,2)" + if which == -1 or which == 3: + parsed = ParsedVariable("der(der(pos,1))", VariableNamingConvention.structured) + assert parsed.as_tuple() == (None, "pos", [], 2), f"Found {parsed.as_tuple()}" + assert parsed.as_string(simplified=True, primitive=True) == "der(pos)" + if which == -1 or which == 4: + parsed = ParsedVariable("der(pipe[3,4].T[14])", VariableNamingConvention.structured) + assert parsed.as_string(primitive=True, index="13") == "pipe[3,4].T[13]" + + test_cases = [ - ("vehicle.engine.speed", ("vehicle.engine", "speed", [], 0)), - ("resistor12.u", ("resistor12", "u", [], 0)), - ("v_min", (None, "v_min", [], 0)), - ("robot.axis.'motor #234'", ("robot.axis", "'motor #234'", [], 0)), - ("der(pipe[3,4].T[14],2)", ("pipe[3,4]", "T", [14], 2)), - ("der(pipe[3,4].T[14])", ("pipe[3,4]", "T", [14], 1)), - ("pipe[3,4].T[14]", ("pipe[3,4]", "T", [14], 0)), - ("T[14].pipe[3,4]", ("T[14]", "pipe", [3, 4], 0)), - ("der(pos,1)", (None, "pos", [], 1)), - ("der(wheels[0].motor.rpm)", ("wheels[0].motor", "rpm", [], 1)), + # to-parse expected msg + ("vehicle.engine.speed", ("vehicle.engine", "speed", [], 0), ""), + ("resistor12.u", ("resistor12", "u", [], 0), ""), + ("v_min", (None, "v_min", [], 0), ""), + ("robot.axis.'motor #234'", ("robot.axis", "'motor #234'", [], 0), ""), + ("der(pipe[3,4].T[14],2)", ("pipe[3,4]", "T", [14], 2), ""), + ("der(pipe[3,4].T[14])", ("pipe[3,4]", "T", [14], 1), ""), + ("pipe[3,4].T[14]", ("pipe[3,4]", "T", [14], 0), ""), + ("T[14].pipe[3, 4]", ("T[14]", "pipe", [3, 4], 0), ""), + ("der(pos)", (None, "pos", [], 1), ""), + ("der(pos,1)", (None, "pos", [], 1), "der(pos)"), + ("der(wheels[0].motor.rpm)", ("wheels[0].motor", "rpm", [], 1), ""), + ("der(der(wheels[0].motor.rpm))", ("wheels[0].motor", "rpm", [], 2), "der(wheels[0].motor.rpm,2)"), + ("der(wheels[0].motor.rpm,2)", ("wheels[0].motor", "rpm", [], 2), ""), ] -@pytest.mark.parametrize("txt, expected", test_cases) -def test_basic_re_expressions(txt, expected): +@pytest.mark.parametrize("txt, expected, text", test_cases) +def test_basic_re_expressions(txt, expected, text): """Test the expressions used in variable_naming.""" parsed = ParsedVariable(txt, VariableNamingConvention.structured) tpl = (parsed.parent, parsed.var, parsed.indices, parsed.der) for i in range(4): assert tpl[i] == expected[i], f"Test:{txt}. Variable element {i} {tpl[i]} != {expected[i]}" - # print(tpl) + expected_txt = txt if text == "" else text + assert parsed.as_string() == expected_txt, f"as_string {parsed.as_tuple()}: {parsed.as_string()}. Expected:{txt}" + + +def _test_basic_re_expressions(): + for c, e, t in test_cases: + print(c, e, t) + test_basic_re_expressions(c, e, t) + + +def test_as_string(): + parsed = ParsedVariable("der(pipe[3,4].T[14], 2)", VariableNamingConvention.structured) + assert parsed.as_string(("parent", "var")) == "pipe[3,4].T" + assert parsed.as_string(("parent", "var", "indices")) == "pipe[3,4].T[14]" + parsed = ParsedVariable("der(wheels[0].motor.rpm)") + assert parsed.as_string(("parent", "var")) == "wheels[0].motor.rpm" + assert parsed.as_string(("parent", "var", "indices")) == "wheels[0].motor.rpm" + assert parsed.as_string(("parent", "var", "indices", "der"), simplified=False) == "der(wheels[0].motor.rpm,1)" if __name__ == "__main__": - retcode = 0 # pytest.main(["-rA", "-v", __file__]) + retcode = pytest.main(["-rA", "-v", __file__]) assert retcode == 0, f"Non-zero return code {retcode}" - for c, e in test_cases: - print(c, e) - test_basic_re_expressions(c, e) + # test_single() + # _test_basic_re_expressions() + # test_as_string() diff --git a/uv.lock b/uv.lock index 444e8ea..789044d 100644 --- a/uv.lock +++ b/uv.lock @@ -1,10 +1,5 @@ version = 1 -requires-python = ">=3.10" -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", - "python_full_version < '3.11'", -] +requires-python = ">=3.11" [[package]] name = "alabaster" @@ -15,36 +10,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/b3/6b4067be973ae96ba0d615946e314c5ae35f9f993eca561b356540bb0c2b/alabaster-1.0.0-py3-none-any.whl", hash = "sha256:fc6786402dc3fcb2de3cabd5fe455a2db534b371124f1f21de8731783dec828b", size = 13929 }, ] -[[package]] -name = "appdirs" -version = "1.4.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d7/d8/05696357e0311f5b5c316d7b95f46c669dd9c15aaeecbb48c7d0aeb88c40/appdirs-1.4.4.tar.gz", hash = "sha256:7d5d0167b2b1ba821647616af46a749d1c653740dd0d2415100fe26e27afdf41", size = 13470 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/00/2344469e2084fb287c2e0b57b72910309874c3245463acd6cf5e3db69324/appdirs-1.4.4-py2.py3-none-any.whl", hash = "sha256:a841dacd6b99318a741b166adb07e19ee71a274450e68237b4650ca1055ab128", size = 9566 }, -] - -[[package]] -name = "astroid" -version = "3.3.9" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/39/33/536530122a22a7504b159bccaf30a1f76aa19d23028bd8b5009eb9b2efea/astroid-3.3.9.tar.gz", hash = "sha256:622cc8e3048684aa42c820d9d218978021c3c3d174fb03a9f0d615921744f550", size = 398731 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/de/80/c749efbd8eef5ea77c7d6f1956e8fbfb51963b7f93ef79647afd4d9886e3/astroid-3.3.9-py3-none-any.whl", hash = "sha256:d05bfd0acba96a7bd43e222828b7d9bc1e138aaeb0649707908d3702a9831248", size = 275339 }, -] - -[[package]] -name = "asttokens" -version = "3.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918 }, -] - [[package]] name = "attrs" version = "25.3.0" @@ -100,19 +65,6 @@ version = "3.4.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/58/5580c1716040bc89206c77d8f74418caf82ce519aae06450393ca73475d1/charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de", size = 198013 }, - { url = "https://files.pythonhosted.org/packages/d0/11/00341177ae71c6f5159a08168bcb98c6e6d196d372c94511f9f6c9afe0c6/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176", size = 141285 }, - { url = "https://files.pythonhosted.org/packages/01/09/11d684ea5819e5a8f5100fb0b38cf8d02b514746607934134d31233e02c8/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e218488cd232553829be0664c2292d3af2eeeb94b32bea483cf79ac6a694e037", size = 151449 }, - { url = "https://files.pythonhosted.org/packages/08/06/9f5a12939db324d905dc1f70591ae7d7898d030d7662f0d426e2286f68c9/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80ed5e856eb7f30115aaf94e4a08114ccc8813e6ed1b5efa74f9f82e8509858f", size = 143892 }, - { url = "https://files.pythonhosted.org/packages/93/62/5e89cdfe04584cb7f4d36003ffa2936681b03ecc0754f8e969c2becb7e24/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b010a7a4fd316c3c484d482922d13044979e78d1861f0e0650423144c616a46a", size = 146123 }, - { url = "https://files.pythonhosted.org/packages/a9/ac/ab729a15c516da2ab70a05f8722ecfccc3f04ed7a18e45c75bbbaa347d61/charset_normalizer-3.4.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4532bff1b8421fd0a320463030c7520f56a79c9024a4e88f01c537316019005a", size = 147943 }, - { url = "https://files.pythonhosted.org/packages/03/d2/3f392f23f042615689456e9a274640c1d2e5dd1d52de36ab8f7955f8f050/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:d973f03c0cb71c5ed99037b870f2be986c3c05e63622c017ea9816881d2dd247", size = 142063 }, - { url = "https://files.pythonhosted.org/packages/f2/e3/e20aae5e1039a2cd9b08d9205f52142329f887f8cf70da3650326670bddf/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:3a3bd0dcd373514dcec91c411ddb9632c0d7d92aed7093b8c3bbb6d69ca74408", size = 150578 }, - { url = "https://files.pythonhosted.org/packages/8d/af/779ad72a4da0aed925e1139d458adc486e61076d7ecdcc09e610ea8678db/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d9c3cdf5390dcd29aa8056d13e8e99526cda0305acc038b96b30352aff5ff2bb", size = 153629 }, - { url = "https://files.pythonhosted.org/packages/c2/b6/7aa450b278e7aa92cf7732140bfd8be21f5f29d5bf334ae987c945276639/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:2bdfe3ac2e1bbe5b59a1a63721eb3b95fc9b6817ae4a46debbb4e11f6232428d", size = 150778 }, - { url = "https://files.pythonhosted.org/packages/39/f4/d9f4f712d0951dcbfd42920d3db81b00dd23b6ab520419626f4023334056/charset_normalizer-3.4.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:eab677309cdb30d047996b36d34caeda1dc91149e4fdca0b1a039b3f79d9a807", size = 146453 }, - { url = "https://files.pythonhosted.org/packages/49/2b/999d0314e4ee0cff3cb83e6bc9aeddd397eeed693edb4facb901eb8fbb69/charset_normalizer-3.4.1-cp310-cp310-win32.whl", hash = "sha256:c0429126cf75e16c4f0ad00ee0eae4242dc652290f940152ca8c75c3a4b6ee8f", size = 95479 }, - { url = "https://files.pythonhosted.org/packages/2d/ce/3cbed41cff67e455a386fb5e5dd8906cdda2ed92fbc6297921f2e4419309/charset_normalizer-3.4.1-cp310-cp310-win_amd64.whl", hash = "sha256:9f0b8b1c6d84c8034a44893aba5e767bf9c7a211e313a9605d9c617d7083829f", size = 102790 }, { url = "https://files.pythonhosted.org/packages/72/80/41ef5d5a7935d2d3a773e3eaebf0a9350542f2cab4eac59a7a4741fbbbbe/charset_normalizer-3.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8bfa33f4f2672964266e940dd22a195989ba31669bd84629f05fab3ef4e2d125", size = 194995 }, { url = "https://files.pythonhosted.org/packages/7a/28/0b9fefa7b8b080ec492110af6d88aa3dea91c464b17d53474b6e9ba5d2c5/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28bf57629c75e810b6ae989f03c0828d64d6b26a5e205535585f96093e405ed1", size = 139471 }, { url = "https://files.pythonhosted.org/packages/71/64/d24ab1a997efb06402e3fc07317e94da358e2585165930d9d59ad45fcae2/charset_normalizer-3.4.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f08ff5e948271dc7e18a35641d2f11a4cd8dfd5634f55228b691e62b37125eb3", size = 149831 }, @@ -171,27 +123,20 @@ source = { editable = "." } dependencies = [ { name = "flexparser" }, { name = "jsonpath-ng" }, - { name = "libcosimpy" }, - { name = "matplotlib" }, { name = "numpy" }, - { name = "pdfminer" }, { name = "pint" }, - { name = "pypdf2" }, { name = "pythonfmu" }, - { name = "scipy" }, - { name = "sympy" }, ] [package.optional-dependencies] -editor = [ - { name = "thonny" }, +rest = [ + { name = "docutils" }, ] -modeltest = [ +tests = [ { name = "fmpy" }, { name = "matplotlib" }, -] -rest = [ - { name = "docutils" }, + { name = "plotly" }, + { name = "scipy" }, ] [package.dev-dependencies] @@ -205,31 +150,24 @@ dev = [ { name = "pytest-cov" }, { name = "ruff" }, { name = "sourcery" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, { name = "sphinx-argparse-cli" }, - { name = "sphinx-autodoc-typehints", version = "3.0.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx-autodoc-typehints", version = "3.1.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx-autodoc-typehints" }, { name = "sphinxcontrib-mermaid" }, ] [package.metadata] requires-dist = [ { name = "docutils", marker = "extra == 'rest'", specifier = ">=0.21" }, - { name = "flexparser", specifier = "<0.4" }, - { name = "fmpy", marker = "extra == 'modeltest'", specifier = "==0.3.21" }, + { name = "flexparser", specifier = ">=0.4" }, + { name = "fmpy", marker = "extra == 'tests'", specifier = "==0.3.21" }, { name = "jsonpath-ng", specifier = ">=1.7.0" }, - { name = "libcosimpy", specifier = ">=0.0.2" }, - { name = "matplotlib", specifier = ">=3.9.1" }, - { name = "matplotlib", marker = "extra == 'modeltest'", specifier = ">=3.9.1" }, - { name = "numpy", specifier = ">=1.26,<2.0" }, - { name = "pdfminer", specifier = ">=20191125" }, - { name = "pint", specifier = ">=0.24" }, - { name = "pypdf2", specifier = ">=3.0.1" }, - { name = "pythonfmu", specifier = ">=0.6.6" }, - { name = "scipy", specifier = ">=1.15.1" }, - { name = "sympy", specifier = ">=1.13.3" }, - { name = "thonny", marker = "extra == 'editor'", specifier = ">=4.1" }, + { name = "matplotlib", marker = "extra == 'tests'", specifier = ">=3.9.1" }, + { name = "numpy", specifier = ">=2.0" }, + { name = "pint", specifier = ">=0.24.4" }, + { name = "plotly", marker = "extra == 'tests'", specifier = ">=6.0.1" }, + { name = "pythonfmu", specifier = "==0.6.9" }, + { name = "scipy", marker = "extra == 'tests'", specifier = ">=1.15.1" }, ] [package.metadata.requires-dev] @@ -258,16 +196,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/25/c2/fc7193cc5383637ff390a712e88e4ded0452c9fbcf84abe3de5ea3df1866/contourpy-1.3.1.tar.gz", hash = "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699", size = 13465753 } wheels = [ - { url = "https://files.pythonhosted.org/packages/b2/a3/80937fe3efe0edacf67c9a20b955139a1a622730042c1ea991956f2704ad/contourpy-1.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab", size = 268466 }, - { url = "https://files.pythonhosted.org/packages/82/1d/e3eaebb4aa2d7311528c048350ca8e99cdacfafd99da87bc0a5f8d81f2c2/contourpy-1.3.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124", size = 253314 }, - { url = "https://files.pythonhosted.org/packages/de/f3/d796b22d1a2b587acc8100ba8c07fb7b5e17fde265a7bb05ab967f4c935a/contourpy-1.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1", size = 312003 }, - { url = "https://files.pythonhosted.org/packages/bf/f5/0e67902bc4394daee8daa39c81d4f00b50e063ee1a46cb3938cc65585d36/contourpy-1.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b", size = 351896 }, - { url = "https://files.pythonhosted.org/packages/1f/d6/e766395723f6256d45d6e67c13bb638dd1fa9dc10ef912dc7dd3dcfc19de/contourpy-1.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453", size = 320814 }, - { url = "https://files.pythonhosted.org/packages/a9/57/86c500d63b3e26e5b73a28b8291a67c5608d4aa87ebd17bd15bb33c178bc/contourpy-1.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3", size = 324969 }, - { url = "https://files.pythonhosted.org/packages/b8/62/bb146d1289d6b3450bccc4642e7f4413b92ebffd9bf2e91b0404323704a7/contourpy-1.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277", size = 1265162 }, - { url = "https://files.pythonhosted.org/packages/18/04/9f7d132ce49a212c8e767042cc80ae390f728060d2eea47058f55b9eff1c/contourpy-1.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595", size = 1324328 }, - { url = "https://files.pythonhosted.org/packages/46/23/196813901be3f97c83ababdab1382e13e0edc0bb4e7b49a7bff15fcf754e/contourpy-1.3.1-cp310-cp310-win32.whl", hash = "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697", size = 173861 }, - { url = "https://files.pythonhosted.org/packages/e0/82/c372be3fc000a3b2005061ca623a0d1ecd2eaafb10d9e883a2fc8566e951/contourpy-1.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e", size = 218566 }, { url = "https://files.pythonhosted.org/packages/12/bb/11250d2906ee2e8b466b5f93e6b19d525f3e0254ac8b445b56e618527718/contourpy-1.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b", size = 269555 }, { url = "https://files.pythonhosted.org/packages/67/71/1e6e95aee21a500415f5d2dbf037bf4567529b6a4e986594d7026ec5ae90/contourpy-1.3.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc", size = 254549 }, { url = "https://files.pythonhosted.org/packages/31/2c/b88986e8d79ac45efe9d8801ae341525f38e087449b6c2f2e6050468a42c/contourpy-1.3.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86", size = 313000 }, @@ -308,9 +236,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f0/ed/92d86f183a8615f13f6b9cbfc5d4298a509d6ce433432e21da838b4b63f4/contourpy-1.3.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1", size = 1318403 }, { url = "https://files.pythonhosted.org/packages/b3/0e/c8e4950c77dcfc897c71d61e56690a0a9df39543d2164040301b5df8e67b/contourpy-1.3.1-cp313-cp313t-win32.whl", hash = "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1", size = 185117 }, { url = "https://files.pythonhosted.org/packages/c1/31/1ae946f11dfbd229222e6d6ad8e7bd1891d3d48bde5fbf7a0beb9491f8e3/contourpy-1.3.1-cp313-cp313t-win_amd64.whl", hash = "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546", size = 236668 }, - { url = "https://files.pythonhosted.org/packages/3e/4f/e56862e64b52b55b5ddcff4090085521fc228ceb09a88390a2b103dccd1b/contourpy-1.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6", size = 265605 }, - { url = "https://files.pythonhosted.org/packages/b0/2e/52bfeeaa4541889f23d8eadc6386b442ee2470bd3cff9baa67deb2dd5c57/contourpy-1.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750", size = 315040 }, - { url = "https://files.pythonhosted.org/packages/52/94/86bfae441707205634d80392e873295652fc313dfd93c233c52c4dc07874/contourpy-1.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53", size = 218221 }, ] [[package]] @@ -319,16 +244,6 @@ version = "7.8.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/19/4f/2251e65033ed2ce1e68f00f91a0294e0f80c80ae8c3ebbe2f12828c4cd53/coverage-7.8.0.tar.gz", hash = "sha256:7a3d62b3b03b4b6fd41a085f3574874cf946cb4604d2b4d3e8dca8cd570ca501", size = 811872 } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/01/1c5e6ee4ebaaa5e079db933a9a45f61172048c7efa06648445821a201084/coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe", size = 211379 }, - { url = "https://files.pythonhosted.org/packages/e9/16/a463389f5ff916963471f7c13585e5f38c6814607306b3cb4d6b4cf13384/coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28", size = 211814 }, - { url = "https://files.pythonhosted.org/packages/b8/b1/77062b0393f54d79064dfb72d2da402657d7c569cfbc724d56ac0f9c67ed/coverage-7.8.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5c8a5c139aae4c35cbd7cadca1df02ea8cf28a911534fc1b0456acb0b14234f3", size = 240937 }, - { url = "https://files.pythonhosted.org/packages/d7/54/c7b00a23150083c124e908c352db03bcd33375494a4beb0c6d79b35448b9/coverage-7.8.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5a26c0c795c3e0b63ec7da6efded5f0bc856d7c0b24b2ac84b4d1d7bc578d676", size = 238849 }, - { url = "https://files.pythonhosted.org/packages/f7/ec/a6b7cfebd34e7b49f844788fda94713035372b5200c23088e3bbafb30970/coverage-7.8.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:821f7bcbaa84318287115d54becb1915eece6918136c6f91045bb84e2f88739d", size = 239986 }, - { url = "https://files.pythonhosted.org/packages/21/8c/c965ecef8af54e6d9b11bfbba85d4f6a319399f5f724798498387f3209eb/coverage-7.8.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a321c61477ff8ee705b8a5fed370b5710c56b3a52d17b983d9215861e37b642a", size = 239896 }, - { url = "https://files.pythonhosted.org/packages/40/83/070550273fb4c480efa8381735969cb403fa8fd1626d74865bfaf9e4d903/coverage-7.8.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:ed2144b8a78f9d94d9515963ed273d620e07846acd5d4b0a642d4849e8d91a0c", size = 238613 }, - { url = "https://files.pythonhosted.org/packages/07/76/fbb2540495b01d996d38e9f8897b861afed356be01160ab4e25471f4fed1/coverage-7.8.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:042e7841a26498fff7a37d6fda770d17519982f5b7d8bf5278d140b67b61095f", size = 238909 }, - { url = "https://files.pythonhosted.org/packages/a3/7e/76d604db640b7d4a86e5dd730b73e96e12a8185f22b5d0799025121f4dcb/coverage-7.8.0-cp310-cp310-win32.whl", hash = "sha256:f9983d01d7705b2d1f7a95e10bbe4091fabc03a46881a256c2787637b087003f", size = 213948 }, - { url = "https://files.pythonhosted.org/packages/5c/a7/f8ce4aafb4a12ab475b56c76a71a40f427740cf496c14e943ade72e25023/coverage-7.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:5a570cd9bd20b85d1a0d7b009aaf6c110b52b5755c17be6962f8ccd65d1dbd23", size = 214844 }, { url = "https://files.pythonhosted.org/packages/2b/77/074d201adb8383addae5784cb8e2dac60bb62bfdf28b2b10f3a3af2fda47/coverage-7.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e7ac22a0bb2c7c49f441f7a6d46c9c80d96e56f5a8bc6972529ed43c8b694e27", size = 211493 }, { url = "https://files.pythonhosted.org/packages/a9/89/7a8efe585750fe59b48d09f871f0e0c028a7b10722b2172dfe021fa2fdd4/coverage-7.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf13d564d310c156d1c8e53877baf2993fb3073b2fc9f69790ca6a732eb4bfea", size = 211921 }, { url = "https://files.pythonhosted.org/packages/e9/ef/96a90c31d08a3f40c49dbe897df4f1fd51fb6583821a1a1c5ee30cc8f680/coverage-7.8.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5761c70c017c1b0d21b0815a920ffb94a670c8d5d409d9b38857874c21f70d7", size = 244556 }, @@ -387,15 +302,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321 }, ] -[[package]] -name = "dill" -version = "0.3.9" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/70/43/86fe3f9e130c4137b0f1b50784dd70a5087b911fe07fa81e53e0c4c47fea/dill-0.3.9.tar.gz", hash = "sha256:81aa267dddf68cbfe8029c42ca9ec6a4ab3b22371d1c450abc54422577b4512c", size = 187000 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/46/d1/e73b6ad76f0b1fb7f23c35c6d95dbc506a9c8804f43dda8cb5b0fa6331fd/dill-0.3.9-py3-none-any.whl", hash = "sha256:468dff3b89520b474c0397703366b7b95eebe6303f108adf9b19da1f702be87a", size = 119418 }, -] - [[package]] name = "distlib" version = "0.3.9" @@ -414,15 +320,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408 }, ] -[[package]] -name = "exceptiongroup" -version = "1.2.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/09/35/2495c4ac46b980e4ca1f6ad6db102322ef3ad2410b79fdde159a4b0f3b92/exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc", size = 28883 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/02/cc/b7e31358aac6ed1ef2bb790a9746ac2c69bcb3c8588b41616914eb106eaf/exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b", size = 16453 }, -] - [[package]] name = "filelock" version = "3.18.0" @@ -446,19 +343,19 @@ wheels = [ [[package]] name = "flexparser" -version = "0.3.1" +version = "0.4" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/dc/e4/a73612499d9c8c450c8f4878e8bb8b3b2dce4bf671b21dd8d5c6549525a7/flexparser-0.3.1.tar.gz", hash = "sha256:36f795d82e50f5c9ae2fde1c33f21f88922fdd67b7629550a3cc4d0b40a66856", size = 31422 } +sdist = { url = "https://files.pythonhosted.org/packages/82/99/b4de7e39e8eaf8207ba1a8fa2241dd98b2ba72ae6e16960d8351736d8702/flexparser-0.4.tar.gz", hash = "sha256:266d98905595be2ccc5da964fe0a2c3526fbbffdc45b65b3146d75db992ef6b2", size = 31799 } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/28/5ce78a4838bb9da1bd9f64bc79ba12ddbfcb4824a11ef41da6f05d3240ef/flexparser-0.3.1-py3-none-any.whl", hash = "sha256:2e3e2936bec1f9277f777ef77297522087d96adb09624d4fe4240fd56885c013", size = 27289 }, + { url = "https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl", hash = "sha256:3738b456192dcb3e15620f324c447721023c0293f6af9955b481e91d00179846", size = 27625 }, ] [[package]] name = "fmpy" -version = "0.3.22" +version = "0.3.21" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "attrs" }, @@ -470,7 +367,7 @@ dependencies = [ { name = "pywin32", marker = "sys_platform == 'win32'" }, ] wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/86/87a735aa1177be40e0fdc01ba87509eec6f5a80d7738cd84a732ba3bae23/FMPy-0.3.22-py3-none-any.whl", hash = "sha256:d280f63852cfc571341648c73653f2e4aec28d33f98189515cc34bf696feb88c", size = 4891796 }, + { url = "https://files.pythonhosted.org/packages/22/ee/974c39c5476ce20342a38483060450bd75f3cd5bb7b2c7ba712d63e763e3/FMPy-0.3.21-py3-none-any.whl", hash = "sha256:77d6896cbd4737ad8e11a185bd3eb54f1c71d3e22c6308b4e8ed55e39142fe80", size = 6721581 }, ] [[package]] @@ -479,14 +376,6 @@ version = "4.56.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/1c/8c/9ffa2a555af0e5e5d0e2ed7fdd8c9bef474ed676995bb4c57c9cd0014248/fonttools-4.56.0.tar.gz", hash = "sha256:a114d1567e1a1586b7e9e7fc2ff686ca542a82769a296cef131e4c4af51e58f4", size = 3462892 } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/5e/6ac30c2cc6a29454260f13c9c6422fc509b7982c13cd4597041260d8f482/fonttools-4.56.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000", size = 2752190 }, - { url = "https://files.pythonhosted.org/packages/92/3a/ac382a8396d1b420ee45eeb0f65b614a9ca7abbb23a1b17524054f0f2200/fonttools-4.56.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16", size = 2280624 }, - { url = "https://files.pythonhosted.org/packages/8a/ae/00b58bfe20e9ff7fbc3dda38f5d127913942b5e252288ea9583099a31bf5/fonttools-4.56.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:705837eae384fe21cee5e5746fd4f4b2f06f87544fa60f60740007e0aa600311", size = 4562074 }, - { url = "https://files.pythonhosted.org/packages/46/d0/0004ca8f6a200252e5bd6982ed99b5fe58c4c59efaf5f516621c4cd8f703/fonttools-4.56.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc871904a53a9d4d908673c6faa15689874af1c7c5ac403a8e12d967ebd0c0dc", size = 4604747 }, - { url = "https://files.pythonhosted.org/packages/45/ea/c8862bd3e09d143ef8ed8268ec8a7d477828f960954889e65288ac050b08/fonttools-4.56.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:38b947de71748bab150259ee05a775e8a0635891568e9fdb3cdd7d0e0004e62f", size = 4559025 }, - { url = "https://files.pythonhosted.org/packages/8f/75/bb88a9552ec1de31a414066257bfd9f40f4ada00074f7a3799ea39b5741f/fonttools-4.56.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:86b2a1013ef7a64d2e94606632683f07712045ed86d937c11ef4dde97319c086", size = 4728482 }, - { url = "https://files.pythonhosted.org/packages/2a/5f/80a2b640df1e1bb7d459d62c8b3f37fe83fd413897e549106d4ebe6371f5/fonttools-4.56.0-cp310-cp310-win32.whl", hash = "sha256:133bedb9a5c6376ad43e6518b7e2cd2f866a05b1998f14842631d5feb36b5786", size = 2155557 }, - { url = "https://files.pythonhosted.org/packages/8f/85/0904f9dbe51ac70d878d3242a8583b9453a09105c3ed19c6301247fd0d3a/fonttools-4.56.0-cp310-cp310-win_amd64.whl", hash = "sha256:17f39313b649037f6c800209984a11fc256a6137cbe5487091c6c7187cae4685", size = 2200017 }, { url = "https://files.pythonhosted.org/packages/35/56/a2f3e777d48fcae7ecd29de4d96352d84e5ea9871e5f3fc88241521572cf/fonttools-4.56.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7ef04bc7827adb7532be3d14462390dd71287644516af3f1e67f1e6ff9c6d6df", size = 2753325 }, { url = "https://files.pythonhosted.org/packages/71/85/d483e9c4e5ed586b183bf037a353e8d766366b54fd15519b30e6178a6a6e/fonttools-4.56.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ffda9b8cd9cb8b301cae2602ec62375b59e2e2108a117746f12215145e3f786c", size = 2281554 }, { url = "https://files.pythonhosted.org/packages/09/67/060473b832b2fade03c127019794df6dc02d9bc66fa4210b8e0d8a99d1e5/fonttools-4.56.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e2e993e8db36306cc3f1734edc8ea67906c55f98683d6fd34c3fc5593fdbba4c", size = 4869260 }, @@ -521,8 +410,7 @@ source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beautifulsoup4" }, { name = "pygments" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, { name = "sphinx-basic-ng" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a0/e2/d351d69a9a9e4badb4a5be062c2d0e87bd9e6c23b5e57337fef14bef34c8/furo-2024.8.6.tar.gz", hash = "sha256:b63e4cee8abfc3136d3bc03a3d45a76a850bada4d6374d24c1716b0e01394a01", size = 1661506 } @@ -566,27 +454,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050 }, ] -[[package]] -name = "isort" -version = "6.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b8/21/1e2a441f74a653a144224d7d21afe8f4169e6c7c20bb13aec3a2dc3815e0/isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450", size = 821955 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/11/114d0a5f4dabbdcedc1125dee0888514c3c3b16d3e9facad87ed96fad97c/isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615", size = 94186 }, -] - -[[package]] -name = "jedi" -version = "0.19.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "parso" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278 }, -] - [[package]] name = "jinja2" version = "3.1.6" @@ -617,21 +484,6 @@ version = "1.4.8" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538 } wheels = [ - { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623 }, - { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720 }, - { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413 }, - { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826 }, - { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231 }, - { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938 }, - { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799 }, - { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362 }, - { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695 }, - { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802 }, - { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646 }, - { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260 }, - { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633 }, - { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885 }, - { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175 }, { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635 }, { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717 }, { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413 }, @@ -690,12 +542,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661 }, { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710 }, { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213 }, - { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403 }, - { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657 }, - { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948 }, - { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186 }, - { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279 }, - { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762 }, ] [[package]] @@ -707,42 +553,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/00/d90b10b962b4277f5e64a78b6609968859ff86889f5b898c1a778c06ec00/lark-1.2.2-py3-none-any.whl", hash = "sha256:c2276486b02f0f1b90be155f2c8ba4a8e194d42775786db622faccd652d8e80c", size = 111036 }, ] -[[package]] -name = "libcosimpy" -version = "0.0.2" -source = { registry = "https://pypi.org/simple" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a4/b8/89b64b5920439bcaab95ce52686bb2bbc22c4cd72683e94f669761293619/libcosimpy-0.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb58d5fb9cf9ee8fa460e1cbce66ebe94cfb2719181333dc7435e80727509135", size = 23413889 }, - { url = "https://files.pythonhosted.org/packages/6c/9d/3a5004840737c1cc84874757b43a0cb5f259518200450faf8986addf4d18/libcosimpy-0.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:ba2e82245425066464297325c6074bb6829c8282dff1057f9ad28f2df97171e1", size = 4474674 }, - { url = "https://files.pythonhosted.org/packages/21/1d/74a1b078692b453022228d0a73695302d764b7d7e69b59ea1a478ab388c4/libcosimpy-0.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:439198d6ff92521df2fb705f1c0516886854ab7c9d53e2c86c2a8e37422f5dd0", size = 23413885 }, - { url = "https://files.pythonhosted.org/packages/05/99/93fd9c6b0b290a31e29287f3b0721171033f64d7636f7c7b1e9192fd2dbb/libcosimpy-0.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:93c5acf67f95a53e9ce9a27aafa863de9461a04a04933652b2991c0b0e52b3d2", size = 4474674 }, - { url = "https://files.pythonhosted.org/packages/06/e7/3dc745a7d41e563c5c50daad1198c86eb463cb522b9f90599dad4445e3b4/libcosimpy-0.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f94574843496e9692c86c955989cd411c749d6454b4fd891a3881282682b4bc7", size = 23413888 }, - { url = "https://files.pythonhosted.org/packages/69/c0/fb50b030b7b2af61b22bf21519b6f50f7e2c3b1b54472f7b8b23627394d4/libcosimpy-0.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:aeeef8d4fbcabf257a512ea2d626b00d281512204da19b9c07b83fe28ab4e9b2", size = 4474675 }, -] - [[package]] name = "lxml" version = "5.3.1" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/ef/f6/c15ca8e5646e937c148e147244817672cf920b56ac0bf2cc1512ae674be8/lxml-5.3.1.tar.gz", hash = "sha256:106b7b5d2977b339f1e97efe2778e2ab20e99994cbb0ec5e55771ed0795920c8", size = 3678591 } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/4b/73426192004a643c11a644ed2346dbe72da164c8e775ea2e70f60e63e516/lxml-5.3.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a4058f16cee694577f7e4dd410263cd0ef75644b43802a689c2b3c2a7e69453b", size = 8142766 }, - { url = "https://files.pythonhosted.org/packages/30/c2/3b28f642b43fdf9580d936e8fdd3ec43c01a97ecfe17fd67f76ce9099752/lxml-5.3.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:364de8f57d6eda0c16dcfb999af902da31396949efa0e583e12675d09709881b", size = 4422744 }, - { url = "https://files.pythonhosted.org/packages/1f/a5/45279e464174b99d72d25bc018b097f9211c0925a174ca582a415609f036/lxml-5.3.1-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:528f3a0498a8edc69af0559bdcf8a9f5a8bf7c00051a6ef3141fdcf27017bbf5", size = 5229609 }, - { url = "https://files.pythonhosted.org/packages/f0/e7/10cd8b9e27ffb6b3465b76604725b67b7c70d4e399750ff88de1b38ab9eb/lxml-5.3.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:db4743e30d6f5f92b6d2b7c86b3ad250e0bad8dee4b7ad8a0c44bfb276af89a3", size = 4943509 }, - { url = "https://files.pythonhosted.org/packages/ce/54/2d6f634924920b17122445136345d44c6d69178c9c49e161aa8f206739d6/lxml-5.3.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:17b5d7f8acf809465086d498d62a981fa6a56d2718135bb0e4aa48c502055f5c", size = 5561495 }, - { url = "https://files.pythonhosted.org/packages/a2/fe/7f5ae8fd1f357fcb21b0d4e20416fae870d654380b6487adbcaaf0df9b31/lxml-5.3.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:928e75a7200a4c09e6efc7482a1337919cc61fe1ba289f297827a5b76d8969c2", size = 4998970 }, - { url = "https://files.pythonhosted.org/packages/af/70/22fecb6f2ca8dc77d14ab6be3cef767ff8340040bc95dca384b5b1cb333a/lxml-5.3.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a997b784a639e05b9d4053ef3b20c7e447ea80814a762f25b8ed5a89d261eac", size = 5114205 }, - { url = "https://files.pythonhosted.org/packages/63/91/21619cc14f7fd1de3f1bdf86cc8106edacf4d685b540d658d84247a3a32a/lxml-5.3.1-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:7b82e67c5feb682dbb559c3e6b78355f234943053af61606af126df2183b9ef9", size = 4940823 }, - { url = "https://files.pythonhosted.org/packages/50/0f/27183248fa3cdd2040047ceccd320ff1ed1344167f38a4ac26aed092268b/lxml-5.3.1-cp310-cp310-manylinux_2_28_ppc64le.whl", hash = "sha256:f1de541a9893cf8a1b1db9bf0bf670a2decab42e3e82233d36a74eda7822b4c9", size = 5585725 }, - { url = "https://files.pythonhosted.org/packages/c6/8d/9b7388d5b23ed2f239a992a478cbd0ce313aaa2d008dd73c4042b190b6a9/lxml-5.3.1-cp310-cp310-manylinux_2_28_s390x.whl", hash = "sha256:de1fc314c3ad6bc2f6bd5b5a5b9357b8c6896333d27fdbb7049aea8bd5af2d79", size = 5082641 }, - { url = "https://files.pythonhosted.org/packages/65/8e/590e20833220eac55b6abcde71d3ae629d38ac1c3543bcc2bfe1f3c2f5d1/lxml-5.3.1-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:7c0536bd9178f754b277a3e53f90f9c9454a3bd108b1531ffff720e082d824f2", size = 5161219 }, - { url = "https://files.pythonhosted.org/packages/4e/77/cabdf5569fd0415a88ebd1d62d7f2814e71422439b8564aaa03e7eefc069/lxml-5.3.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:68018c4c67d7e89951a91fbd371e2e34cd8cfc71f0bb43b5332db38497025d51", size = 5019293 }, - { url = "https://files.pythonhosted.org/packages/49/bd/f0b6d50ea7b8b54aaa5df4410cb1d5ae6ffa016b8e0503cae08b86c24674/lxml-5.3.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:aa826340a609d0c954ba52fd831f0fba2a4165659ab0ee1a15e4aac21f302406", size = 5651232 }, - { url = "https://files.pythonhosted.org/packages/fa/69/1793d00a4e3da7f27349edb5a6f3da947ed921263cd9a243fab11c6cbc07/lxml-5.3.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:796520afa499732191e39fc95b56a3b07f95256f2d22b1c26e217fb69a9db5b5", size = 5489527 }, - { url = "https://files.pythonhosted.org/packages/d3/c9/e2449129b6cb2054c898df8d850ea4dadd75b4c33695a6c4b0f35082f1e7/lxml-5.3.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:3effe081b3135237da6e4c4530ff2a868d3f80be0bda027e118a5971285d42d0", size = 5227050 }, - { url = "https://files.pythonhosted.org/packages/ed/63/e5da540eba6ab9a0d4188eeaa5c85767b77cafa8efeb70da0593d6cd3b81/lxml-5.3.1-cp310-cp310-win32.whl", hash = "sha256:a22f66270bd6d0804b02cd49dae2b33d4341015545d17f8426f2c4e22f557a23", size = 3475345 }, - { url = "https://files.pythonhosted.org/packages/08/71/853a3ad812cd24c35b7776977cb0ae40c2b64ff79ad6d6c36c987daffc49/lxml-5.3.1-cp310-cp310-win_amd64.whl", hash = "sha256:0bcfadea3cdc68e678d2b20cb16a16716887dd00a881e16f7d806c2138b8ff0c", size = 3805093 }, { url = "https://files.pythonhosted.org/packages/57/bb/2faea15df82114fa27f2a86eec220506c532ee8ce211dff22f48881b353a/lxml-5.3.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e220f7b3e8656ab063d2eb0cd536fafef396829cafe04cb314e734f87649058f", size = 8161781 }, { url = "https://files.pythonhosted.org/packages/9f/d3/374114084abb1f96026eccb6cd48b070f85de82fdabae6c2f1e198fa64e5/lxml-5.3.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f2cfae0688fd01f7056a17367e3b84f37c545fb447d7282cf2c242b16262607", size = 4432571 }, { url = "https://files.pythonhosted.org/packages/0f/fb/44a46efdc235c2dd763c1e929611d8ff3b920c32b8fcd9051d38f4d04633/lxml-5.3.1-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:67d2f8ad9dcc3a9e826bdc7802ed541a44e124c29b7d95a679eeb58c1c14ade8", size = 5028919 }, @@ -794,12 +610,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/39/25/ad4ac8fac488505a2702656550e63c2a8db3a4fd63db82a20dad5689cecb/lxml-5.3.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dbf7bebc2275016cddf3c997bf8a0f7044160714c64a9b83975670a04e6d2252", size = 5050951 }, { url = "https://files.pythonhosted.org/packages/82/74/f7d223c704c87e44b3d27b5e0dde173a2fcf2e89c0524c8015c2b3554876/lxml-5.3.1-cp313-cp313-win32.whl", hash = "sha256:d0751528b97d2b19a388b302be2a0ee05817097bab46ff0ed76feeec24951f78", size = 3485357 }, { url = "https://files.pythonhosted.org/packages/80/83/8c54533b3576f4391eebea88454738978669a6cad0d8e23266224007939d/lxml-5.3.1-cp313-cp313-win_amd64.whl", hash = "sha256:91fb6a43d72b4f8863d21f347a9163eecbf36e76e2f51068d59cd004c506f332", size = 3814484 }, - { url = "https://files.pythonhosted.org/packages/d2/b4/89a68d05f267f05cc1b8b2f289a8242955705b1b0a9d246198227817ee46/lxml-5.3.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:afa578b6524ff85fb365f454cf61683771d0170470c48ad9d170c48075f86725", size = 3936118 }, - { url = "https://files.pythonhosted.org/packages/7f/0d/c034a541e7a1153527d7880c62493a74f2277f38e64de2480cadd0d4cf96/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:67f5e80adf0aafc7b5454f2c1cb0cde920c9b1f2cbd0485f07cc1d0497c35c5d", size = 4233690 }, - { url = "https://files.pythonhosted.org/packages/35/5c/38e183c2802f14fbdaa75c3266e11d0ca05c64d78e8cdab2ee84e954a565/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2dd0b80ac2d8f13ffc906123a6f20b459cb50a99222d0da492360512f3e50f84", size = 4349569 }, - { url = "https://files.pythonhosted.org/packages/18/5b/14f93b359b3c29673d5d282bc3a6edb3a629879854a77541841aba37607f/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:422c179022ecdedbe58b0e242607198580804253da220e9454ffe848daa1cfd2", size = 4236731 }, - { url = "https://files.pythonhosted.org/packages/f6/08/8471de65f3dee70a3a50e7082fd7409f0ac7a1ace777c13fca4aea1a5759/lxml-5.3.1-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:524ccfded8989a6595dbdda80d779fb977dbc9a7bc458864fc9a0c2fc15dc877", size = 4373119 }, - { url = "https://files.pythonhosted.org/packages/83/29/00b9b0322a473aee6cda87473401c9abb19506cd650cc69a8aa38277ea74/lxml-5.3.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:48fd46bf7155def2e15287c6f2b133a2f78e2d22cdf55647269977b873c65499", size = 3487718 }, ] [[package]] @@ -820,16 +630,6 @@ version = "3.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537 } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357 }, - { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393 }, - { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732 }, - { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866 }, - { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964 }, - { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977 }, - { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366 }, - { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091 }, - { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065 }, - { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514 }, { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353 }, { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392 }, { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984 }, @@ -889,12 +689,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/2f/08/b89867ecea2e305f408fbb417139a8dd941ecf7b23a2e02157c36da546f0/matplotlib-3.10.1.tar.gz", hash = "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba", size = 36743335 } wheels = [ - { url = "https://files.pythonhosted.org/packages/ee/b1/f70e27cf1cd76ce2a5e1aa5579d05afe3236052c6d9b9a96325bc823a17e/matplotlib-3.10.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16", size = 8163654 }, - { url = "https://files.pythonhosted.org/packages/26/af/5ec3d4636106718bb62503a03297125d4514f98fe818461bd9e6b9d116e4/matplotlib-3.10.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2", size = 8037943 }, - { url = "https://files.pythonhosted.org/packages/a1/3d/07f9003a71b698b848c9925d05979ffa94a75cd25d1a587202f0bb58aa81/matplotlib-3.10.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0673b4b8f131890eb3a1ad058d6e065fb3c6e71f160089b65f8515373394698", size = 8449510 }, - { url = "https://files.pythonhosted.org/packages/12/87/9472d4513ff83b7cd864311821793ab72234fa201ab77310ec1b585d27e2/matplotlib-3.10.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e875b95ac59a7908978fe307ecdbdd9a26af7fa0f33f474a27fcf8c99f64a19", size = 8586585 }, - { url = "https://files.pythonhosted.org/packages/31/9e/fe74d237d2963adae8608faeb21f778cf246dbbf4746cef87cffbc82c4b6/matplotlib-3.10.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2589659ea30726284c6c91037216f64a506a9822f8e50592d48ac16a2f29e044", size = 9397911 }, - { url = "https://files.pythonhosted.org/packages/b6/1b/025d3e59e8a4281ab463162ad7d072575354a1916aba81b6a11507dfc524/matplotlib-3.10.1-cp310-cp310-win_amd64.whl", hash = "sha256:a97ff127f295817bc34517255c9db6e71de8eddaab7f837b7d341dee9f2f587f", size = 8052998 }, { url = "https://files.pythonhosted.org/packages/a5/14/a1b840075be247bb1834b22c1e1d558740b0f618fe3a823740181ca557a1/matplotlib-3.10.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:057206ff2d6ab82ff3e94ebd94463d084760ca682ed5f150817b859372ec4401", size = 8174669 }, { url = "https://files.pythonhosted.org/packages/0a/e4/300b08e3e08f9c98b0d5635f42edabf2f7a1d634e64cb0318a71a44ff720/matplotlib-3.10.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a144867dd6bf8ba8cb5fc81a158b645037e11b3e5cf8a50bd5f9917cb863adfe", size = 8047996 }, { url = "https://files.pythonhosted.org/packages/75/f9/8d99ff5a2498a5f1ccf919fb46fb945109623c6108216f10f96428f388bc/matplotlib-3.10.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56c5d9fcd9879aa8040f196a235e2dcbdf7dd03ab5b07c0696f80bc6cf04bedd", size = 8461612 }, @@ -919,18 +713,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/db/b05bf463689134789b06dea85828f8ebe506fa1e37593f723b65b86c9582/matplotlib-3.10.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3", size = 8613864 }, { url = "https://files.pythonhosted.org/packages/c2/04/41ccec4409f3023a7576df3b5c025f1a8c8b81fbfe922ecfd837ac36e081/matplotlib-3.10.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f", size = 9409487 }, { url = "https://files.pythonhosted.org/packages/ac/c2/0d5aae823bdcc42cc99327ecdd4d28585e15ccd5218c453b7bcd827f3421/matplotlib-3.10.1-cp313-cp313t-win_amd64.whl", hash = "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9", size = 8134832 }, - { url = "https://files.pythonhosted.org/packages/c8/f6/10adb696d8cbeed2ab4c2e26ecf1c80dd3847bbf3891f4a0c362e0e08a5a/matplotlib-3.10.1-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:648406f1899f9a818cef8c0231b44dcfc4ff36f167101c3fd1c9151f24220fdc", size = 8158685 }, - { url = "https://files.pythonhosted.org/packages/3f/84/0603d917406072763e7f9bb37747d3d74d7ecd4b943a8c947cc3ae1cf7af/matplotlib-3.10.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:02582304e352f40520727984a5a18f37e8187861f954fea9be7ef06569cf85b4", size = 8035491 }, - { url = "https://files.pythonhosted.org/packages/fd/7d/6a8b31dd07ed856b3eae001c9129670ef75c4698fa1c2a6ac9f00a4a7054/matplotlib-3.10.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3809916157ba871bcdd33d3493acd7fe3037db5daa917ca6e77975a94cef779", size = 8590087 }, -] - -[[package]] -name = "mccabe" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/ff/0ffefdcac38932a54d2b5eed4e0ba8a408f215002cd178ad1df0f2806ff8/mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325", size = 9658 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/1a/1f68f9ba0c207934b35b86a8ca3aad8395a3d6dd7921c0686e23853ff5a9/mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e", size = 7350 }, ] [[package]] @@ -954,32 +736,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979 }, ] -[[package]] -name = "mpmath" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 }, -] - [[package]] name = "msgpack" version = "1.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/cb/d0/7555686ae7ff5731205df1012ede15dd9d927f6227ea151e901c7406af4f/msgpack-1.1.0.tar.gz", hash = "sha256:dd432ccc2c72b914e4cb77afce64aab761c1137cc698be3984eee260bcb2896e", size = 167260 } wheels = [ - { url = "https://files.pythonhosted.org/packages/4b/f9/a892a6038c861fa849b11a2bb0502c07bc698ab6ea53359e5771397d883b/msgpack-1.1.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7ad442d527a7e358a469faf43fda45aaf4ac3249c8310a82f0ccff9164e5dccd", size = 150428 }, - { url = "https://files.pythonhosted.org/packages/df/7a/d174cc6a3b6bb85556e6a046d3193294a92f9a8e583cdbd46dc8a1d7e7f4/msgpack-1.1.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:74bed8f63f8f14d75eec75cf3d04ad581da6b914001b474a5d3cd3372c8cc27d", size = 84131 }, - { url = "https://files.pythonhosted.org/packages/08/52/bf4fbf72f897a23a56b822997a72c16de07d8d56d7bf273242f884055682/msgpack-1.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:914571a2a5b4e7606997e169f64ce53a8b1e06f2cf2c3a7273aa106236d43dd5", size = 81215 }, - { url = "https://files.pythonhosted.org/packages/02/95/dc0044b439b518236aaf012da4677c1b8183ce388411ad1b1e63c32d8979/msgpack-1.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c921af52214dcbb75e6bdf6a661b23c3e6417f00c603dd2070bccb5c3ef499f5", size = 371229 }, - { url = "https://files.pythonhosted.org/packages/ff/75/09081792db60470bef19d9c2be89f024d366b1e1973c197bb59e6aabc647/msgpack-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8ce0b22b890be5d252de90d0e0d119f363012027cf256185fc3d474c44b1b9e", size = 378034 }, - { url = "https://files.pythonhosted.org/packages/32/d3/c152e0c55fead87dd948d4b29879b0f14feeeec92ef1fd2ec21b107c3f49/msgpack-1.1.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:73322a6cc57fcee3c0c57c4463d828e9428275fb85a27aa2aa1a92fdc42afd7b", size = 363070 }, - { url = "https://files.pythonhosted.org/packages/d9/2c/82e73506dd55f9e43ac8aa007c9dd088c6f0de2aa19e8f7330e6a65879fc/msgpack-1.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e1f3c3d21f7cf67bcf2da8e494d30a75e4cf60041d98b3f79875afb5b96f3a3f", size = 359863 }, - { url = "https://files.pythonhosted.org/packages/cb/a0/3d093b248837094220e1edc9ec4337de3443b1cfeeb6e0896af8ccc4cc7a/msgpack-1.1.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:64fc9068d701233effd61b19efb1485587560b66fe57b3e50d29c5d78e7fef68", size = 368166 }, - { url = "https://files.pythonhosted.org/packages/e4/13/7646f14f06838b406cf5a6ddbb7e8dc78b4996d891ab3b93c33d1ccc8678/msgpack-1.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:42f754515e0f683f9c79210a5d1cad631ec3d06cea5172214d2176a42e67e19b", size = 370105 }, - { url = "https://files.pythonhosted.org/packages/67/fa/dbbd2443e4578e165192dabbc6a22c0812cda2649261b1264ff515f19f15/msgpack-1.1.0-cp310-cp310-win32.whl", hash = "sha256:3df7e6b05571b3814361e8464f9304c42d2196808e0119f55d0d3e62cd5ea044", size = 68513 }, - { url = "https://files.pythonhosted.org/packages/24/ce/c2c8fbf0ded750cb63cbcbb61bc1f2dfd69e16dca30a8af8ba80ec182dcd/msgpack-1.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:685ec345eefc757a7c8af44a3032734a739f8c45d1b0ac45efc5d8977aa4720f", size = 74687 }, { url = "https://files.pythonhosted.org/packages/b7/5e/a4c7154ba65d93be91f2f1e55f90e76c5f91ccadc7efc4341e6f04c8647f/msgpack-1.1.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:3d364a55082fb2a7416f6c63ae383fbd903adb5a6cf78c5b96cc6316dc1cedc7", size = 150803 }, { url = "https://files.pythonhosted.org/packages/60/c2/687684164698f1d51c41778c838d854965dd284a4b9d3a44beba9265c931/msgpack-1.1.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:79ec007767b9b56860e0372085f8504db5d06bd6a327a335449508bbee9648fa", size = 84343 }, { url = "https://files.pythonhosted.org/packages/42/ae/d3adea9bb4a1342763556078b5765e666f8fdf242e00f3f6657380920972/msgpack-1.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6ad622bf7756d5a497d5b6836e7fc3752e2dd6f4c648e24b1803f6048596f701", size = 81408 }, @@ -1021,17 +783,10 @@ version = "1.15.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, { name = "typing-extensions" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/43/d5e49a86afa64bd3839ea0d5b9c7103487007d728e1293f52525d6d5486a/mypy-1.15.0.tar.gz", hash = "sha256:404534629d51d3efea5c800ee7c42b72a6554d6c400e6a79eafe15d11341fd43", size = 3239717 } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/f8/65a7ce8d0e09b6329ad0c8d40330d100ea343bd4dd04c4f8ae26462d0a17/mypy-1.15.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:979e4e1a006511dacf628e36fadfecbcc0160a8af6ca7dad2f5025529e082c13", size = 10738433 }, - { url = "https://files.pythonhosted.org/packages/b4/95/9c0ecb8eacfe048583706249439ff52105b3f552ea9c4024166c03224270/mypy-1.15.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c4bb0e1bd29f7d34efcccd71cf733580191e9a264a2202b0239da95984c5b559", size = 9861472 }, - { url = "https://files.pythonhosted.org/packages/84/09/9ec95e982e282e20c0d5407bc65031dfd0f0f8ecc66b69538296e06fcbee/mypy-1.15.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:be68172e9fd9ad8fb876c6389f16d1c1b5f100ffa779f77b1fb2176fcc9ab95b", size = 11611424 }, - { url = "https://files.pythonhosted.org/packages/78/13/f7d14e55865036a1e6a0a69580c240f43bc1f37407fe9235c0d4ef25ffb0/mypy-1.15.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7be1e46525adfa0d97681432ee9fcd61a3964c2446795714699a998d193f1a3", size = 12365450 }, - { url = "https://files.pythonhosted.org/packages/48/e1/301a73852d40c241e915ac6d7bcd7fedd47d519246db2d7b86b9d7e7a0cb/mypy-1.15.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2e2c2e6d3593f6451b18588848e66260ff62ccca522dd231cd4dd59b0160668b", size = 12551765 }, - { url = "https://files.pythonhosted.org/packages/77/ba/c37bc323ae5fe7f3f15a28e06ab012cd0b7552886118943e90b15af31195/mypy-1.15.0-cp310-cp310-win_amd64.whl", hash = "sha256:6983aae8b2f653e098edb77f893f7b6aca69f6cffb19b2cc7443f23cce5f4828", size = 9274701 }, { url = "https://files.pythonhosted.org/packages/03/bc/f6339726c627bd7ca1ce0fa56c9ae2d0144604a319e0e339bdadafbbb599/mypy-1.15.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2922d42e16d6de288022e5ca321cd0618b238cfc5570e0263e5ba0a77dbef56f", size = 10662338 }, { url = "https://files.pythonhosted.org/packages/e2/90/8dcf506ca1a09b0d17555cc00cd69aee402c203911410136cd716559efe7/mypy-1.15.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2ee2d57e01a7c35de00f4634ba1bbf015185b219e4dc5909e281016df43f5ee5", size = 9787540 }, { url = "https://files.pythonhosted.org/packages/05/05/a10f9479681e5da09ef2f9426f650d7b550d4bafbef683b69aad1ba87457/mypy-1.15.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:973500e0774b85d9689715feeffcc980193086551110fd678ebe1f4342fb7c5e", size = 11538051 }, @@ -1072,14 +827,22 @@ dependencies = [ { name = "markdown-it-py" }, { name = "mdit-py-plugins" }, { name = "pyyaml" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/a5/9626ba4f73555b3735ad86247a8077d4603aa8628537687c839ab08bfe44/myst_parser-4.0.1.tar.gz", hash = "sha256:5cfea715e4f3574138aecbf7d54132296bfd72bb614d31168f48c477a830a7c4", size = 93985 } wheels = [ { url = "https://files.pythonhosted.org/packages/5f/df/76d0321c3797b54b60fef9ec3bd6f4cfd124b9e422182156a1dd418722cf/myst_parser-4.0.1-py3-none-any.whl", hash = "sha256:9134e88959ec3b5780aedf8a99680ea242869d012e8821db3126d427edc9c95d", size = 84579 }, ] +[[package]] +name = "narwhals" +version = "1.45.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4c/9f/284886c5cea849b4ed1c55babcb48cb1084886139e8ac31e9849112ce6d0/narwhals-1.45.0.tar.gz", hash = "sha256:f9ecefb9d09cda6fefa8ead10dc37a79129b6c78b0ac7117d21b4d4486bdd0d1", size = 508812 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/a4/337a229d184b23ee63e6b730ac1588d77067af77c550dbf69cf1d74c3298/narwhals-1.45.0-py3-none-any.whl", hash = "sha256:0585612aa7ec89f9d061e78410b6fb8772794389d1a29d5799572d6b81999497", size = 371633 }, +] + [[package]] name = "nodeenv" version = "1.9.1" @@ -1091,34 +854,83 @@ wheels = [ [[package]] name = "numpy" -version = "1.26.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/65/6e/09db70a523a96d25e115e71cc56a6f9031e7b8cd166c1ac8438307c14058/numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010", size = 15786129 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a7/94/ace0fdea5241a27d13543ee117cbc65868e82213fb31a8eb7fe9ff23f313/numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0", size = 20631468 }, - { url = "https://files.pythonhosted.org/packages/20/f7/b24208eba89f9d1b58c1668bc6c8c4fd472b20c45573cb767f59d49fb0f6/numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a", size = 13966411 }, - { url = "https://files.pythonhosted.org/packages/fc/a5/4beee6488160798683eed5bdb7eead455892c3b4e1f78d79d8d3f3b084ac/numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4", size = 14219016 }, - { url = "https://files.pythonhosted.org/packages/4b/d7/ecf66c1cd12dc28b4040b15ab4d17b773b87fa9d29ca16125de01adb36cd/numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f", size = 18240889 }, - { url = "https://files.pythonhosted.org/packages/24/03/6f229fe3187546435c4f6f89f6d26c129d4f5bed40552899fcf1f0bf9e50/numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a", size = 13876746 }, - { url = "https://files.pythonhosted.org/packages/39/fe/39ada9b094f01f5a35486577c848fe274e374bbf8d8f472e1423a0bbd26d/numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2", size = 18078620 }, - { url = "https://files.pythonhosted.org/packages/d5/ef/6ad11d51197aad206a9ad2286dc1aac6a378059e06e8cf22cd08ed4f20dc/numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07", size = 5972659 }, - { url = "https://files.pythonhosted.org/packages/19/77/538f202862b9183f54108557bfda67e17603fc560c384559e769321c9d92/numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5", size = 15808905 }, - { url = "https://files.pythonhosted.org/packages/11/57/baae43d14fe163fa0e4c47f307b6b2511ab8d7d30177c491960504252053/numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71", size = 20630554 }, - { url = "https://files.pythonhosted.org/packages/1a/2e/151484f49fd03944c4a3ad9c418ed193cfd02724e138ac8a9505d056c582/numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef", size = 13997127 }, - { url = "https://files.pythonhosted.org/packages/79/ae/7e5b85136806f9dadf4878bf73cf223fe5c2636818ba3ab1c585d0403164/numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e", size = 14222994 }, - { url = "https://files.pythonhosted.org/packages/3a/d0/edc009c27b406c4f9cbc79274d6e46d634d139075492ad055e3d68445925/numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5", size = 18252005 }, - { url = "https://files.pythonhosted.org/packages/09/bf/2b1aaf8f525f2923ff6cfcf134ae5e750e279ac65ebf386c75a0cf6da06a/numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a", size = 13885297 }, - { url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567 }, - { url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812 }, - { url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913 }, - { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901 }, - { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868 }, - { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109 }, - { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613 }, - { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172 }, - { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643 }, - { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803 }, - { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754 }, +version = "2.3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/65/21b3bc86aac7b8f2862db1e808f1ea22b028e30a225a34a5ede9bf8678f2/numpy-2.3.5.tar.gz", hash = "sha256:784db1dcdab56bf0517743e746dfb0f885fc68d948aba86eeec2cba234bdf1c0", size = 20584950 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/77/84dd1d2e34d7e2792a236ba180b5e8fcc1e3e414e761ce0253f63d7f572e/numpy-2.3.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de5672f4a7b200c15a4127042170a694d4df43c992948f5e1af57f0174beed10", size = 17034641 }, + { url = "https://files.pythonhosted.org/packages/2a/ea/25e26fa5837106cde46ae7d0b667e20f69cbbc0efd64cba8221411ab26ae/numpy-2.3.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:acfd89508504a19ed06ef963ad544ec6664518c863436306153e13e94605c218", size = 12528324 }, + { url = "https://files.pythonhosted.org/packages/4d/1a/e85f0eea4cf03d6a0228f5c0256b53f2df4bc794706e7df019fc622e47f1/numpy-2.3.5-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:ffe22d2b05504f786c867c8395de703937f934272eb67586817b46188b4ded6d", size = 5356872 }, + { url = "https://files.pythonhosted.org/packages/5c/bb/35ef04afd567f4c989c2060cde39211e4ac5357155c1833bcd1166055c61/numpy-2.3.5-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:872a5cf366aec6bb1147336480fef14c9164b154aeb6542327de4970282cd2f5", size = 6893148 }, + { url = "https://files.pythonhosted.org/packages/f2/2b/05bbeb06e2dff5eab512dfc678b1cc5ee94d8ac5956a0885c64b6b26252b/numpy-2.3.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3095bdb8dd297e5920b010e96134ed91d852d81d490e787beca7e35ae1d89cf7", size = 14557282 }, + { url = "https://files.pythonhosted.org/packages/65/fb/2b23769462b34398d9326081fad5655198fcf18966fcb1f1e49db44fbf31/numpy-2.3.5-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8cba086a43d54ca804ce711b2a940b16e452807acebe7852ff327f1ecd49b0d4", size = 16897903 }, + { url = "https://files.pythonhosted.org/packages/ac/14/085f4cf05fc3f1e8aa95e85404e984ffca9b2275a5dc2b1aae18a67538b8/numpy-2.3.5-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6cf9b429b21df6b99f4dee7a1218b8b7ffbbe7df8764dc0bd60ce8a0708fed1e", size = 16341672 }, + { url = "https://files.pythonhosted.org/packages/6f/3b/1f73994904142b2aa290449b3bb99772477b5fd94d787093e4f24f5af763/numpy-2.3.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:396084a36abdb603546b119d96528c2f6263921c50df3c8fd7cb28873a237748", size = 18838896 }, + { url = "https://files.pythonhosted.org/packages/cd/b9/cf6649b2124f288309ffc353070792caf42ad69047dcc60da85ee85fea58/numpy-2.3.5-cp311-cp311-win32.whl", hash = "sha256:b0c7088a73aef3d687c4deef8452a3ac7c1be4e29ed8bf3b366c8111128ac60c", size = 6563608 }, + { url = "https://files.pythonhosted.org/packages/aa/44/9fe81ae1dcc29c531843852e2874080dc441338574ccc4306b39e2ff6e59/numpy-2.3.5-cp311-cp311-win_amd64.whl", hash = "sha256:a414504bef8945eae5f2d7cb7be2d4af77c5d1cb5e20b296c2c25b61dff2900c", size = 13078442 }, + { url = "https://files.pythonhosted.org/packages/6d/a7/f99a41553d2da82a20a2f22e93c94f928e4490bb447c9ff3c4ff230581d3/numpy-2.3.5-cp311-cp311-win_arm64.whl", hash = "sha256:0cd00b7b36e35398fa2d16af7b907b65304ef8bb4817a550e06e5012929830fa", size = 10458555 }, + { url = "https://files.pythonhosted.org/packages/44/37/e669fe6cbb2b96c62f6bbedc6a81c0f3b7362f6a59230b23caa673a85721/numpy-2.3.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:74ae7b798248fe62021dbf3c914245ad45d1a6b0cb4a29ecb4b31d0bfbc4cc3e", size = 16733873 }, + { url = "https://files.pythonhosted.org/packages/c5/65/df0db6c097892c9380851ab9e44b52d4f7ba576b833996e0080181c0c439/numpy-2.3.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee3888d9ff7c14604052b2ca5535a30216aa0a58e948cdd3eeb8d3415f638769", size = 12259838 }, + { url = "https://files.pythonhosted.org/packages/5b/e1/1ee06e70eb2136797abe847d386e7c0e830b67ad1d43f364dd04fa50d338/numpy-2.3.5-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:612a95a17655e213502f60cfb9bf9408efdc9eb1d5f50535cc6eb365d11b42b5", size = 5088378 }, + { url = "https://files.pythonhosted.org/packages/6d/9c/1ca85fb86708724275103b81ec4cf1ac1d08f465368acfc8da7ab545bdae/numpy-2.3.5-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3101e5177d114a593d79dd79658650fe28b5a0d8abeb8ce6f437c0e6df5be1a4", size = 6628559 }, + { url = "https://files.pythonhosted.org/packages/74/78/fcd41e5a0ce4f3f7b003da85825acddae6d7ecb60cf25194741b036ca7d6/numpy-2.3.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8b973c57ff8e184109db042c842423ff4f60446239bd585a5131cc47f06f789d", size = 14250702 }, + { url = "https://files.pythonhosted.org/packages/b6/23/2a1b231b8ff672b4c450dac27164a8b2ca7d9b7144f9c02d2396518352eb/numpy-2.3.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d8163f43acde9a73c2a33605353a4f1bc4798745a8b1d73183b28e5b435ae28", size = 16606086 }, + { url = "https://files.pythonhosted.org/packages/a0/c5/5ad26fbfbe2012e190cc7d5003e4d874b88bb18861d0829edc140a713021/numpy-2.3.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:51c1e14eb1e154ebd80e860722f9e6ed6ec89714ad2db2d3aa33c31d7c12179b", size = 16025985 }, + { url = "https://files.pythonhosted.org/packages/d2/fa/dd48e225c46c819288148d9d060b047fd2a6fb1eb37eae25112ee4cb4453/numpy-2.3.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b46b4ec24f7293f23adcd2d146960559aaf8020213de8ad1909dba6c013bf89c", size = 18542976 }, + { url = "https://files.pythonhosted.org/packages/05/79/ccbd23a75862d95af03d28b5c6901a1b7da4803181513d52f3b86ed9446e/numpy-2.3.5-cp312-cp312-win32.whl", hash = "sha256:3997b5b3c9a771e157f9aae01dd579ee35ad7109be18db0e85dbdbe1de06e952", size = 6285274 }, + { url = "https://files.pythonhosted.org/packages/2d/57/8aeaf160312f7f489dea47ab61e430b5cb051f59a98ae68b7133ce8fa06a/numpy-2.3.5-cp312-cp312-win_amd64.whl", hash = "sha256:86945f2ee6d10cdfd67bcb4069c1662dd711f7e2a4343db5cecec06b87cf31aa", size = 12782922 }, + { url = "https://files.pythonhosted.org/packages/78/a6/aae5cc2ca78c45e64b9ef22f089141d661516856cf7c8a54ba434576900d/numpy-2.3.5-cp312-cp312-win_arm64.whl", hash = "sha256:f28620fe26bee16243be2b7b874da327312240a7cdc38b769a697578d2100013", size = 10194667 }, + { url = "https://files.pythonhosted.org/packages/db/69/9cde09f36da4b5a505341180a3f2e6fadc352fd4d2b7096ce9778db83f1a/numpy-2.3.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d0f23b44f57077c1ede8c5f26b30f706498b4862d3ff0a7298b8411dd2f043ff", size = 16728251 }, + { url = "https://files.pythonhosted.org/packages/79/fb/f505c95ceddd7027347b067689db71ca80bd5ecc926f913f1a23e65cf09b/numpy-2.3.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:aa5bc7c5d59d831d9773d1170acac7893ce3a5e130540605770ade83280e7188", size = 12254652 }, + { url = "https://files.pythonhosted.org/packages/78/da/8c7738060ca9c31b30e9301ee0cf6c5ffdbf889d9593285a1cead337f9a5/numpy-2.3.5-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:ccc933afd4d20aad3c00bcef049cb40049f7f196e0397f1109dba6fed63267b0", size = 5083172 }, + { url = "https://files.pythonhosted.org/packages/a4/b4/ee5bb2537fb9430fd2ef30a616c3672b991a4129bb1c7dcc42aa0abbe5d7/numpy-2.3.5-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:afaffc4393205524af9dfa400fa250143a6c3bc646c08c9f5e25a9f4b4d6a903", size = 6622990 }, + { url = "https://files.pythonhosted.org/packages/95/03/dc0723a013c7d7c19de5ef29e932c3081df1c14ba582b8b86b5de9db7f0f/numpy-2.3.5-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c75442b2209b8470d6d5d8b1c25714270686f14c749028d2199c54e29f20b4d", size = 14248902 }, + { url = "https://files.pythonhosted.org/packages/f5/10/ca162f45a102738958dcec8023062dad0cbc17d1ab99d68c4e4a6c45fb2b/numpy-2.3.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11e06aa0af8c0f05104d56450d6093ee639e15f24ecf62d417329d06e522e017", size = 16597430 }, + { url = "https://files.pythonhosted.org/packages/2a/51/c1e29be863588db58175175f057286900b4b3327a1351e706d5e0f8dd679/numpy-2.3.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ed89927b86296067b4f81f108a2271d8926467a8868e554eaf370fc27fa3ccaf", size = 16024551 }, + { url = "https://files.pythonhosted.org/packages/83/68/8236589d4dbb87253d28259d04d9b814ec0ecce7cb1c7fed29729f4c3a78/numpy-2.3.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51c55fe3451421f3a6ef9a9c1439e82101c57a2c9eab9feb196a62b1a10b58ce", size = 18533275 }, + { url = "https://files.pythonhosted.org/packages/40/56/2932d75b6f13465239e3b7b7e511be27f1b8161ca2510854f0b6e521c395/numpy-2.3.5-cp313-cp313-win32.whl", hash = "sha256:1978155dd49972084bd6ef388d66ab70f0c323ddee6f693d539376498720fb7e", size = 6277637 }, + { url = "https://files.pythonhosted.org/packages/0c/88/e2eaa6cffb115b85ed7c7c87775cb8bcf0816816bc98ca8dbfa2ee33fe6e/numpy-2.3.5-cp313-cp313-win_amd64.whl", hash = "sha256:00dc4e846108a382c5869e77c6ed514394bdeb3403461d25a829711041217d5b", size = 12779090 }, + { url = "https://files.pythonhosted.org/packages/8f/88/3f41e13a44ebd4034ee17baa384acac29ba6a4fcc2aca95f6f08ca0447d1/numpy-2.3.5-cp313-cp313-win_arm64.whl", hash = "sha256:0472f11f6ec23a74a906a00b48a4dcf3849209696dff7c189714511268d103ae", size = 10194710 }, + { url = "https://files.pythonhosted.org/packages/13/cb/71744144e13389d577f867f745b7df2d8489463654a918eea2eeb166dfc9/numpy-2.3.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:414802f3b97f3c1eef41e530aaba3b3c1620649871d8cb38c6eaff034c2e16bd", size = 16827292 }, + { url = "https://files.pythonhosted.org/packages/71/80/ba9dc6f2a4398e7f42b708a7fdc841bb638d353be255655498edbf9a15a8/numpy-2.3.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5ee6609ac3604fa7780e30a03e5e241a7956f8e2fcfe547d51e3afa5247ac47f", size = 12378897 }, + { url = "https://files.pythonhosted.org/packages/2e/6d/db2151b9f64264bcceccd51741aa39b50150de9b602d98ecfe7e0c4bff39/numpy-2.3.5-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:86d835afea1eaa143012a2d7a3f45a3adce2d7adc8b4961f0b362214d800846a", size = 5207391 }, + { url = "https://files.pythonhosted.org/packages/80/ae/429bacace5ccad48a14c4ae5332f6aa8ab9f69524193511d60ccdfdc65fa/numpy-2.3.5-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:30bc11310e8153ca664b14c5f1b73e94bd0503681fcf136a163de856f3a50139", size = 6721275 }, + { url = "https://files.pythonhosted.org/packages/74/5b/1919abf32d8722646a38cd527bc3771eb229a32724ee6ba340ead9b92249/numpy-2.3.5-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1062fde1dcf469571705945b0f221b73928f34a20c904ffb45db101907c3454e", size = 14306855 }, + { url = "https://files.pythonhosted.org/packages/a5/87/6831980559434973bebc30cd9c1f21e541a0f2b0c280d43d3afd909b66d0/numpy-2.3.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ce581db493ea1a96c0556360ede6607496e8bf9b3a8efa66e06477267bc831e9", size = 16657359 }, + { url = "https://files.pythonhosted.org/packages/dd/91/c797f544491ee99fd00495f12ebb7802c440c1915811d72ac5b4479a3356/numpy-2.3.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:cc8920d2ec5fa99875b670bb86ddeb21e295cb07aa331810d9e486e0b969d946", size = 16093374 }, + { url = "https://files.pythonhosted.org/packages/74/a6/54da03253afcbe7a72785ec4da9c69fb7a17710141ff9ac5fcb2e32dbe64/numpy-2.3.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:9ee2197ef8c4f0dfe405d835f3b6a14f5fee7782b5de51ba06fb65fc9b36e9f1", size = 18594587 }, + { url = "https://files.pythonhosted.org/packages/80/e9/aff53abbdd41b0ecca94285f325aff42357c6b5abc482a3fcb4994290b18/numpy-2.3.5-cp313-cp313t-win32.whl", hash = "sha256:70b37199913c1bd300ff6e2693316c6f869c7ee16378faf10e4f5e3275b299c3", size = 6405940 }, + { url = "https://files.pythonhosted.org/packages/d5/81/50613fec9d4de5480de18d4f8ef59ad7e344d497edbef3cfd80f24f98461/numpy-2.3.5-cp313-cp313t-win_amd64.whl", hash = "sha256:b501b5fa195cc9e24fe102f21ec0a44dffc231d2af79950b451e0d99cea02234", size = 12920341 }, + { url = "https://files.pythonhosted.org/packages/bb/ab/08fd63b9a74303947f34f0bd7c5903b9c5532c2d287bead5bdf4c556c486/numpy-2.3.5-cp313-cp313t-win_arm64.whl", hash = "sha256:a80afd79f45f3c4a7d341f13acbe058d1ca8ac017c165d3fa0d3de6bc1a079d7", size = 10262507 }, + { url = "https://files.pythonhosted.org/packages/ba/97/1a914559c19e32d6b2e233cf9a6a114e67c856d35b1d6babca571a3e880f/numpy-2.3.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:bf06bc2af43fa8d32d30fae16ad965663e966b1a3202ed407b84c989c3221e82", size = 16735706 }, + { url = "https://files.pythonhosted.org/packages/57/d4/51233b1c1b13ecd796311216ae417796b88b0616cfd8a33ae4536330748a/numpy-2.3.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:052e8c42e0c49d2575621c158934920524f6c5da05a1d3b9bab5d8e259e045f0", size = 12264507 }, + { url = "https://files.pythonhosted.org/packages/45/98/2fe46c5c2675b8306d0b4a3ec3494273e93e1226a490f766e84298576956/numpy-2.3.5-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:1ed1ec893cff7040a02c8aa1c8611b94d395590d553f6b53629a4461dc7f7b63", size = 5093049 }, + { url = "https://files.pythonhosted.org/packages/ce/0e/0698378989bb0ac5f1660c81c78ab1fe5476c1a521ca9ee9d0710ce54099/numpy-2.3.5-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:2dcd0808a421a482a080f89859a18beb0b3d1e905b81e617a188bd80422d62e9", size = 6626603 }, + { url = "https://files.pythonhosted.org/packages/5e/a6/9ca0eecc489640615642a6cbc0ca9e10df70df38c4d43f5a928ff18d8827/numpy-2.3.5-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:727fd05b57df37dc0bcf1a27767a3d9a78cbbc92822445f32cc3436ba797337b", size = 14262696 }, + { url = "https://files.pythonhosted.org/packages/c8/f6/07ec185b90ec9d7217a00eeeed7383b73d7e709dae2a9a021b051542a708/numpy-2.3.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fffe29a1ef00883599d1dc2c51aa2e5d80afe49523c261a74933df395c15c520", size = 16597350 }, + { url = "https://files.pythonhosted.org/packages/75/37/164071d1dde6a1a84c9b8e5b414fa127981bad47adf3a6b7e23917e52190/numpy-2.3.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:8f7f0e05112916223d3f438f293abf0727e1181b5983f413dfa2fefc4098245c", size = 16040190 }, + { url = "https://files.pythonhosted.org/packages/08/3c/f18b82a406b04859eb026d204e4e1773eb41c5be58410f41ffa511d114ae/numpy-2.3.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2e2eb32ddb9ccb817d620ac1d8dae7c3f641c1e5f55f531a33e8ab97960a75b8", size = 18536749 }, + { url = "https://files.pythonhosted.org/packages/40/79/f82f572bf44cf0023a2fe8588768e23e1592585020d638999f15158609e1/numpy-2.3.5-cp314-cp314-win32.whl", hash = "sha256:66f85ce62c70b843bab1fb14a05d5737741e74e28c7b8b5a064de10142fad248", size = 6335432 }, + { url = "https://files.pythonhosted.org/packages/a3/2e/235b4d96619931192c91660805e5e49242389742a7a82c27665021db690c/numpy-2.3.5-cp314-cp314-win_amd64.whl", hash = "sha256:e6a0bc88393d65807d751a614207b7129a310ca4fe76a74e5c7da5fa5671417e", size = 12919388 }, + { url = "https://files.pythonhosted.org/packages/07/2b/29fd75ce45d22a39c61aad74f3d718e7ab67ccf839ca8b60866054eb15f8/numpy-2.3.5-cp314-cp314-win_arm64.whl", hash = "sha256:aeffcab3d4b43712bb7a60b65f6044d444e75e563ff6180af8f98dd4b905dfd2", size = 10476651 }, + { url = "https://files.pythonhosted.org/packages/17/e1/f6a721234ebd4d87084cfa68d081bcba2f5cfe1974f7de4e0e8b9b2a2ba1/numpy-2.3.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:17531366a2e3a9e30762c000f2c43a9aaa05728712e25c11ce1dbe700c53ad41", size = 16834503 }, + { url = "https://files.pythonhosted.org/packages/5c/1c/baf7ffdc3af9c356e1c135e57ab7cf8d247931b9554f55c467efe2c69eff/numpy-2.3.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d21644de1b609825ede2f48be98dfde4656aefc713654eeee280e37cadc4e0ad", size = 12381612 }, + { url = "https://files.pythonhosted.org/packages/74/91/f7f0295151407ddc9ba34e699013c32c3c91944f9b35fcf9281163dc1468/numpy-2.3.5-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:c804e3a5aba5460c73955c955bdbd5c08c354954e9270a2c1565f62e866bdc39", size = 5210042 }, + { url = "https://files.pythonhosted.org/packages/2e/3b/78aebf345104ec50dd50a4d06ddeb46a9ff5261c33bcc58b1c4f12f85ec2/numpy-2.3.5-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:cc0a57f895b96ec78969c34f682c602bf8da1a0270b09bc65673df2e7638ec20", size = 6724502 }, + { url = "https://files.pythonhosted.org/packages/02/c6/7c34b528740512e57ef1b7c8337ab0b4f0bddf34c723b8996c675bc2bc91/numpy-2.3.5-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:900218e456384ea676e24ea6a0417f030a3b07306d29d7ad843957b40a9d8d52", size = 14308962 }, + { url = "https://files.pythonhosted.org/packages/80/35/09d433c5262bc32d725bafc619e095b6a6651caf94027a03da624146f655/numpy-2.3.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:09a1bea522b25109bf8e6f3027bd810f7c1085c64a0c7ce050c1676ad0ba010b", size = 16655054 }, + { url = "https://files.pythonhosted.org/packages/7a/ab/6a7b259703c09a88804fa2430b43d6457b692378f6b74b356155283566ac/numpy-2.3.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:04822c00b5fd0323c8166d66c701dc31b7fbd252c100acd708c48f763968d6a3", size = 16091613 }, + { url = "https://files.pythonhosted.org/packages/c2/88/330da2071e8771e60d1038166ff9d73f29da37b01ec3eb43cb1427464e10/numpy-2.3.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:d6889ec4ec662a1a37eb4b4fb26b6100841804dac55bd9df579e326cdc146227", size = 18591147 }, + { url = "https://files.pythonhosted.org/packages/51/41/851c4b4082402d9ea860c3626db5d5df47164a712cb23b54be028b184c1c/numpy-2.3.5-cp314-cp314t-win32.whl", hash = "sha256:93eebbcf1aafdf7e2ddd44c2923e2672e1010bddc014138b229e49725b4d6be5", size = 6479806 }, + { url = "https://files.pythonhosted.org/packages/90/30/d48bde1dfd93332fa557cff1972fbc039e055a52021fbef4c2c4b1eefd17/numpy-2.3.5-cp314-cp314t-win_amd64.whl", hash = "sha256:c8a9958e88b65c3b27e22ca2a076311636850b612d6bbfb76e8d156aacde2aaf", size = 13105760 }, + { url = "https://files.pythonhosted.org/packages/2d/fd/4b5eb0b3e888d86aee4d198c23acec7d214baaf17ea93c1adec94c9518b9/numpy-2.3.5-cp314-cp314t-win_arm64.whl", hash = "sha256:6203fdf9f3dc5bdaed7319ad8698e685c7a3be10819f41d32a0723e611733b42", size = 10545459 }, + { url = "https://files.pythonhosted.org/packages/c6/65/f9dea8e109371ade9c782b4e4756a82edf9d3366bca495d84d79859a0b79/numpy-2.3.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f0963b55cdd70fad460fa4c1341f12f976bb26cb66021a5580329bd498988310", size = 16910689 }, + { url = "https://files.pythonhosted.org/packages/00/4f/edb00032a8fb92ec0a679d3830368355da91a69cab6f3e9c21b64d0bb986/numpy-2.3.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:f4255143f5160d0de972d28c8f9665d882b5f61309d8362fdd3e103cf7bf010c", size = 12457053 }, + { url = "https://files.pythonhosted.org/packages/16/a4/e8a53b5abd500a63836a29ebe145fc1ab1f2eefe1cfe59276020373ae0aa/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:a4b9159734b326535f4dd01d947f919c6eefd2d9827466a696c44ced82dfbc18", size = 5285635 }, + { url = "https://files.pythonhosted.org/packages/a3/2f/37eeb9014d9c8b3e9c55bc599c68263ca44fdbc12a93e45a21d1d56df737/numpy-2.3.5-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2feae0d2c91d46e59fcd62784a3a83b3fb677fead592ce51b5a6fbb4f95965ff", size = 6801770 }, + { url = "https://files.pythonhosted.org/packages/7d/e4/68d2f474df2cb671b2b6c2986a02e520671295647dad82484cde80ca427b/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ffac52f28a7849ad7576293c0cb7b9f08304e8f7d738a8cb8a90ec4c55a998eb", size = 14391768 }, + { url = "https://files.pythonhosted.org/packages/b8/50/94ccd8a2b141cb50651fddd4f6a48874acb3c91c8f0842b08a6afc4b0b21/numpy-2.3.5-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63c0e9e7eea69588479ebf4a8a270d5ac22763cc5854e9a7eae952a3908103f7", size = 16729263 }, + { url = "https://files.pythonhosted.org/packages/2d/ee/346fa473e666fe14c52fcdd19ec2424157290a032d4c41f98127bfb31ac7/numpy-2.3.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f16417ec91f12f814b10bafe79ef77e70113a2f5f7018640e7425ff979253425", size = 12967213 }, ] [[package]] @@ -1130,41 +942,12 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, ] -[[package]] -name = "parso" -version = "0.8.4" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650 }, -] - -[[package]] -name = "pdfminer" -version = "20191125" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pycryptodome" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/71/a3/155c5cde5f9c0b1069043b2946a93f54a41fd72cc19c6c100f6f2f5bdc15/pdfminer-20191125.tar.gz", hash = "sha256:9e700bc731300ed5c8936343c1dd4529638184198e54e91dd2b59b64a755dc01", size = 4173248 } - [[package]] name = "pillow" version = "11.1.0" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/f3/af/c097e544e7bd278333db77933e535098c259609c4eb3b85381109602fb5b/pillow-11.1.0.tar.gz", hash = "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20", size = 46742715 } wheels = [ - { url = "https://files.pythonhosted.org/packages/50/1c/2dcea34ac3d7bc96a1fd1bd0a6e06a57c67167fec2cff8d95d88229a8817/pillow-11.1.0-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8", size = 3229983 }, - { url = "https://files.pythonhosted.org/packages/14/ca/6bec3df25e4c88432681de94a3531cc738bd85dea6c7aa6ab6f81ad8bd11/pillow-11.1.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192", size = 3101831 }, - { url = "https://files.pythonhosted.org/packages/d4/2c/668e18e5521e46eb9667b09e501d8e07049eb5bfe39d56be0724a43117e6/pillow-11.1.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2", size = 4314074 }, - { url = "https://files.pythonhosted.org/packages/02/80/79f99b714f0fc25f6a8499ecfd1f810df12aec170ea1e32a4f75746051ce/pillow-11.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26", size = 4394933 }, - { url = "https://files.pythonhosted.org/packages/81/aa/8d4ad25dc11fd10a2001d5b8a80fdc0e564ac33b293bdfe04ed387e0fd95/pillow-11.1.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07", size = 4353349 }, - { url = "https://files.pythonhosted.org/packages/84/7a/cd0c3eaf4a28cb2a74bdd19129f7726277a7f30c4f8424cd27a62987d864/pillow-11.1.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482", size = 4476532 }, - { url = "https://files.pythonhosted.org/packages/8f/8b/a907fdd3ae8f01c7670dfb1499c53c28e217c338b47a813af8d815e7ce97/pillow-11.1.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e", size = 4279789 }, - { url = "https://files.pythonhosted.org/packages/6f/9a/9f139d9e8cccd661c3efbf6898967a9a337eb2e9be2b454ba0a09533100d/pillow-11.1.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269", size = 4413131 }, - { url = "https://files.pythonhosted.org/packages/a8/68/0d8d461f42a3f37432203c8e6df94da10ac8081b6d35af1c203bf3111088/pillow-11.1.0-cp310-cp310-win32.whl", hash = "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49", size = 2291213 }, - { url = "https://files.pythonhosted.org/packages/14/81/d0dff759a74ba87715509af9f6cb21fa21d93b02b3316ed43bda83664db9/pillow-11.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a", size = 2625725 }, - { url = "https://files.pythonhosted.org/packages/ce/1f/8d50c096a1d58ef0584ddc37e6f602828515219e9d2428e14ce50f5ecad1/pillow-11.1.0-cp310-cp310-win_arm64.whl", hash = "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65", size = 2375213 }, { url = "https://files.pythonhosted.org/packages/dd/d6/2000bfd8d5414fb70cbbe52c8332f2283ff30ed66a9cde42716c8ecbe22c/pillow-11.1.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457", size = 3229968 }, { url = "https://files.pythonhosted.org/packages/d9/45/3fe487010dd9ce0a06adf9b8ff4f273cc0a44536e234b0fad3532a42c15b/pillow-11.1.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35", size = 3101806 }, { url = "https://files.pythonhosted.org/packages/e3/72/776b3629c47d9d5f1c160113158a7a7ad177688d3a1159cd3b62ded5a33a/pillow-11.1.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2", size = 4322283 }, @@ -1206,28 +989,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/fb/a7960e838bc5df57a2ce23183bfd2290d97c33028b96bde332a9057834d3/pillow-11.1.0-cp313-cp313t-win32.whl", hash = "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9", size = 2295494 }, { url = "https://files.pythonhosted.org/packages/d7/6c/6ec83ee2f6f0fda8d4cf89045c6be4b0373ebfc363ba8538f8c999f63fcd/pillow-11.1.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe", size = 2631595 }, { url = "https://files.pythonhosted.org/packages/cf/6c/41c21c6c8af92b9fea313aa47c75de49e2f9a467964ee33eb0135d47eb64/pillow-11.1.0-cp313-cp313t-win_arm64.whl", hash = "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756", size = 2377651 }, - { url = "https://files.pythonhosted.org/packages/fa/c5/389961578fb677b8b3244fcd934f720ed25a148b9a5cc81c91bdf59d8588/pillow-11.1.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90", size = 3198345 }, - { url = "https://files.pythonhosted.org/packages/c4/fa/803c0e50ffee74d4b965229e816af55276eac1d5806712de86f9371858fd/pillow-11.1.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb", size = 3072938 }, - { url = "https://files.pythonhosted.org/packages/dc/67/2a3a5f8012b5d8c63fe53958ba906c1b1d0482ebed5618057ef4d22f8076/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442", size = 3400049 }, - { url = "https://files.pythonhosted.org/packages/e5/a0/514f0d317446c98c478d1872497eb92e7cde67003fed74f696441e647446/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83", size = 3422431 }, - { url = "https://files.pythonhosted.org/packages/cd/00/20f40a935514037b7d3f87adfc87d2c538430ea625b63b3af8c3f5578e72/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f", size = 3446208 }, - { url = "https://files.pythonhosted.org/packages/28/3c/7de681727963043e093c72e6c3348411b0185eab3263100d4490234ba2f6/pillow-11.1.0-pp310-pypy310_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73", size = 3509746 }, - { url = "https://files.pythonhosted.org/packages/41/67/936f9814bdd74b2dfd4822f1f7725ab5d8ff4103919a1664eb4874c58b2f/pillow-11.1.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0", size = 2626353 }, ] [[package]] name = "pint" -version = "0.24.3" +version = "0.25.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "appdirs" }, { name = "flexcache" }, { name = "flexparser" }, + { name = "platformdirs" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/53/7d/30178ff193a076e35521592260915f74049bfa77dccb43ac8aa5abe1414b/pint-0.24.3.tar.gz", hash = "sha256:d54771093e8b94c4e0a35ac638c2444ddf3ef685652bab7675ffecfa0c5c5cdf", size = 341664 } +sdist = { url = "https://files.pythonhosted.org/packages/5f/74/bc3f671997158aef171194c3c4041e549946f4784b8690baa0626a0a164b/pint-0.25.2.tar.gz", hash = "sha256:85a45d1da8fe9c9f7477fed8aef59ad2b939af3d6611507e1a9cbdacdcd3450a", size = 254467 } wheels = [ - { url = "https://files.pythonhosted.org/packages/3f/2b/abe15c62ef1aece41d0799f31ba97d298aad9c76bc31dd655c387c29f17a/Pint-0.24.3-py3-none-any.whl", hash = "sha256:d98667e46fd03a1b94694fbfa104ec30858684d8ab26952e2a348b48059089bb", size = 301758 }, + { url = "https://files.pythonhosted.org/packages/ab/88/550d41e81e6d43335603a960cd9c75c1d88f9cf01bc9d4ee8e86290aba7d/pint-0.25.2-py3-none-any.whl", hash = "sha256:ca35ab1d8eeeb6f7d9942b3cb5f34ca42b61cdd5fb3eae79531553dcca04dda7", size = 306762 }, ] [[package]] @@ -1239,6 +1015,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6d/45/59578566b3275b8fd9157885918fcd0c4d74162928a5310926887b856a51/platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94", size = 18499 }, ] +[[package]] +name = "plotly" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "narwhals" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6e/5c/0efc297df362b88b74957a230af61cd6929f531f72f48063e8408702ffba/plotly-6.2.0.tar.gz", hash = "sha256:9dfa23c328000f16c928beb68927444c1ab9eae837d1fe648dbcda5360c7953d", size = 6801941 } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/20/f2b7ac96a91cc5f70d81320adad24cc41bf52013508d649b1481db225780/plotly-6.2.0-py3-none-any.whl", hash = "sha256:32c444d4c940887219cb80738317040363deefdfee4f354498cc0b6dab8978bd", size = 9635469 }, +] + [[package]] name = "pluggy" version = "1.5.0" @@ -1273,29 +1062,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, ] -[[package]] -name = "pycryptodome" -version = "3.22.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/44/e6/099310419df5ada522ff34ffc2f1a48a11b37fc6a76f51a6854c182dbd3e/pycryptodome-3.22.0.tar.gz", hash = "sha256:fd7ab568b3ad7b77c908d7c3f7e167ec5a8f035c64ff74f10d47a4edd043d723", size = 4917300 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/65/a05831c3e4bcd1bf6c2a034e399f74b3d6f30bb4e37e36b9c310c09dc8c0/pycryptodome-3.22.0-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:009e1c80eea42401a5bd5983c4bab8d516aef22e014a4705622e24e6d9d703c6", size = 2490637 }, - { url = "https://files.pythonhosted.org/packages/5c/76/ff3c2e7a60d17c080c4c6120ebaf60f38717cd387e77f84da4dcf7f64ff0/pycryptodome-3.22.0-cp37-abi3-macosx_10_9_x86_64.whl", hash = "sha256:3b76fa80daeff9519d7e9f6d9e40708f2fce36b9295a847f00624a08293f4f00", size = 1635372 }, - { url = "https://files.pythonhosted.org/packages/cc/7f/cc5d6da0dbc36acd978d80a72b228e33aadaec9c4f91c93221166d8bdc05/pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a31fa5914b255ab62aac9265654292ce0404f6b66540a065f538466474baedbc", size = 2177456 }, - { url = "https://files.pythonhosted.org/packages/92/65/35f5063e68790602d892ad36e35ac723147232a9084d1999630045c34593/pycryptodome-3.22.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a0092fd476701eeeb04df5cc509d8b739fa381583cda6a46ff0a60639b7cd70d", size = 2263744 }, - { url = "https://files.pythonhosted.org/packages/cc/67/46acdd35b1081c3dbc72dc466b1b95b80d2f64cad3520f994a9b6c5c7d00/pycryptodome-3.22.0-cp37-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:18d5b0ddc7cf69231736d778bd3ae2b3efb681ae33b64b0c92fb4626bb48bb89", size = 2303356 }, - { url = "https://files.pythonhosted.org/packages/3d/f9/a4f8a83384626098e3f55664519bec113002b9ef751887086ae63a53135a/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f6cf6aa36fcf463e622d2165a5ad9963b2762bebae2f632d719dfb8544903cf5", size = 2176714 }, - { url = "https://files.pythonhosted.org/packages/88/65/e5f8c3a885f70a6e05c84844cd5542120576f4369158946e8cfc623a464d/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_i686.whl", hash = "sha256:aec7b40a7ea5af7c40f8837adf20a137d5e11a6eb202cde7e588a48fb2d871a8", size = 2337329 }, - { url = "https://files.pythonhosted.org/packages/b8/2a/25e0be2b509c28375c7f75c7e8d8d060773f2cce4856a1654276e3202339/pycryptodome-3.22.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:d21c1eda2f42211f18a25db4eaf8056c94a8563cd39da3683f89fe0d881fb772", size = 2262255 }, - { url = "https://files.pythonhosted.org/packages/41/58/60917bc4bbd91712e53ce04daf237a74a0ad731383a01288130672994328/pycryptodome-3.22.0-cp37-abi3-win32.whl", hash = "sha256:f02baa9f5e35934c6e8dcec91fcde96612bdefef6e442813b8ea34e82c84bbfb", size = 1763403 }, - { url = "https://files.pythonhosted.org/packages/55/f4/244c621afcf7867e23f63cfd7a9630f14cfe946c9be7e566af6c3915bcde/pycryptodome-3.22.0-cp37-abi3-win_amd64.whl", hash = "sha256:d086aed307e96d40c23c42418cbbca22ecc0ab4a8a0e24f87932eeab26c08627", size = 1794568 }, - { url = "https://files.pythonhosted.org/packages/37/c3/e3423e72669ca09f141aae493e1feaa8b8475859898b04f57078280a61c4/pycryptodome-3.22.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b4bdce34af16c1dcc7f8c66185684be15f5818afd2a82b75a4ce6b55f9783e13", size = 1618698 }, - { url = "https://files.pythonhosted.org/packages/f9/b7/35eec0b3919cafea362dcb68bb0654d9cb3cde6da6b7a9d8480ce0bf203a/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2988ffcd5137dc2d27eb51cd18c0f0f68e5b009d5fec56fbccb638f90934f333", size = 1666957 }, - { url = "https://files.pythonhosted.org/packages/b0/1f/f49bccdd8d61f1da4278eb0d6aee7f988f1a6ec4056b0c2dc51eda45ae27/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e653519dedcd1532788547f00eeb6108cc7ce9efdf5cc9996abce0d53f95d5a9", size = 1659242 }, - { url = "https://files.pythonhosted.org/packages/95/43/a01dcf1ed39c9a9e9c9d3f9d98040deeceedaa7cdf043e175251f2e13f44/pycryptodome-3.22.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f5810bc7494e4ac12a4afef5a32218129e7d3890ce3f2b5ec520cc69eb1102ad", size = 1697246 }, - { url = "https://files.pythonhosted.org/packages/3b/49/195842931f9ee6f14cd63ef85e06b93073463ed59601fb283ba9b813cd53/pycryptodome-3.22.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:e7514a1aebee8e85802d154fdb261381f1cb9b7c5a54594545145b8ec3056ae6", size = 1797436 }, -] - [[package]] name = "pygments" version = "2.19.1" @@ -1305,25 +1071,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293 }, ] -[[package]] -name = "pylint" -version = "3.3.6" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "astroid" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "dill" }, - { name = "isort" }, - { name = "mccabe" }, - { name = "platformdirs" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, - { name = "tomlkit" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/69/a7/113d02340afb9dcbb0c8b25454e9538cd08f0ebf3e510df4ed916caa1a89/pylint-3.3.6.tar.gz", hash = "sha256:b634a041aac33706d56a0d217e6587228c66427e20ec21a019bc4cdee48c040a", size = 1519586 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/31/21/9537fc94aee9ec7316a230a49895266cf02d78aa29b0a2efbc39566e0935/pylint-3.3.6-py3-none-any.whl", hash = "sha256:8b7c2d3e86ae3f94fb27703d521dd0b9b6b378775991f504d7c3a6275aa0a6a6", size = 522462 }, -] - [[package]] name = "pyparsing" version = "3.2.3" @@ -1333,15 +1080,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120 }, ] -[[package]] -name = "pypdf2" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9f/bb/18dc3062d37db6c491392007dfd1a7f524bb95886eb956569ac38a23a784/PyPDF2-3.0.1.tar.gz", hash = "sha256:a74408f69ba6271f71b9352ef4ed03dc53a31aa404d29b5d31f53bfecfee1440", size = 227419 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8e/5e/c86a5643653825d3c913719e788e41386bee415c2b87b4f955432f2de6b2/pypdf2-3.0.1-py3-none-any.whl", hash = "sha256:d16e4205cfee272fbdc0568b68d82be796540b1537508cef59388f839c191928", size = 232572 }, -] - [[package]] name = "pyright" version = "1.1.398" @@ -1355,26 +1093,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/58/e0/5283593f61b3c525d6d7e94cfb6b3ded20b3df66e953acaf7bb4f23b3f6e/pyright-1.1.398-py3-none-any.whl", hash = "sha256:0a70bfd007d9ea7de1cf9740e1ad1a40a122592cfe22a3f6791b06162ad08753", size = 5780235 }, ] -[[package]] -name = "pyserial" -version = "3.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1e/7d/ae3f0a63f41e4d2f6cb66a5b57197850f919f59e558159a4dd3a818f5082/pyserial-3.5.tar.gz", hash = "sha256:3c77e014170dfffbd816e6ffc205e9842efb10be9f58ec16d3e8675b4925cddb", size = 159125 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/bc/587a445451b253b285629263eb51c2d8e9bcea4fc97826266d186f96f558/pyserial-3.5-py2.py3-none-any.whl", hash = "sha256:c4451db6ba391ca6ca299fb3ec7bae67a5c55dde170964c7a14ceefec02f2cf0", size = 90585 }, -] - [[package]] name = "pytest" version = "8.3.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, { name = "iniconfig" }, { name = "packaging" }, { name = "pluggy" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } wheels = [ @@ -1408,11 +1135,11 @@ wheels = [ [[package]] name = "pythonfmu" -version = "0.6.6" +version = "0.6.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ef/f2/0f12b4fce8f88e68b87c88d497d51fb141760c7968c93e120e2b9b97e948/pythonfmu-0.6.6.tar.gz", hash = "sha256:a2d0043cfd843c75842085aa1273cc5f6cba3f6e3fcab23cfb1f9928d109c0f5", size = 342602 } +sdist = { url = "https://files.pythonhosted.org/packages/7f/22/08cdfef2fdc9a451ddaa35aaba05643184d3e709f67ded6d142aaf469e8c/pythonfmu-0.6.9.tar.gz", hash = "sha256:13eab981d3c86704ab19b35ebcee9347b2dcc378bbe8d0b43c9ae1fe35bc9fa7", size = 244885 } wheels = [ - { url = "https://files.pythonhosted.org/packages/99/45/cdc2ca486d272fa6fbb4637ab0482c13e013988f93a07bc383ad99f11b3a/pythonfmu-0.6.6-py3-none-any.whl", hash = "sha256:32b0f5384002aea8dc24ee55e85f9f2916436158edfbf7bdba4f503c2d5f90bf", size = 357936 }, + { url = "https://files.pythonhosted.org/packages/1b/11/73c37bfb38f0feaab0e0054de5afd9518da21095f371b7beb58183f5bf2b/pythonfmu-0.6.9-py3-none-any.whl", hash = "sha256:9b196f1e4b76ec67197cbf70e93452f1595b2459ed735fa4488ada913d713949", size = 259448 }, ] [[package]] @@ -1420,9 +1147,6 @@ name = "pywin32" version = "310" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/da/a5f38fffbba2fb99aa4aa905480ac4b8e83ca486659ac8c95bce47fb5276/pywin32-310-cp310-cp310-win32.whl", hash = "sha256:6dd97011efc8bf51d6793a82292419eba2c71cf8e7250cfac03bba284454abc1", size = 8848240 }, - { url = "https://files.pythonhosted.org/packages/aa/fe/d873a773324fa565619ba555a82c9dabd677301720f3660a731a5d07e49a/pywin32-310-cp310-cp310-win_amd64.whl", hash = "sha256:c3e78706e4229b915a0821941a84e7ef420bf2b77e08c9dae3c76fd03fd2ae3d", size = 9601854 }, - { url = "https://files.pythonhosted.org/packages/3c/84/1a8e3d7a15490d28a5d816efa229ecb4999cdc51a7c30dd8914f669093b8/pywin32-310-cp310-cp310-win_arm64.whl", hash = "sha256:33babed0cf0c92a6f94cc6cc13546ab24ee13e3e800e61ed87609ab91e4c8213", size = 8522963 }, { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284 }, { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748 }, { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941 }, @@ -1440,15 +1164,6 @@ version = "6.0.2" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } wheels = [ - { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, - { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, - { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, - { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, - { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, - { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, - { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, - { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, - { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, @@ -1536,15 +1251,6 @@ dependencies = [ ] sdist = { url = "https://files.pythonhosted.org/packages/b7/b9/31ba9cd990e626574baf93fbc1ac61cf9ed54faafd04c479117517661637/scipy-1.15.2.tar.gz", hash = "sha256:cd58a314d92838f7e6f755c8a2167ead4f27e1fd5c1251fd54289569ef3495ec", size = 59417316 } wheels = [ - { url = "https://files.pythonhosted.org/packages/95/df/ef233fff6838fe6f7840d69b5ef9f20d2b5c912a8727b21ebf876cb15d54/scipy-1.15.2-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a2ec871edaa863e8213ea5df811cd600734f6400b4af272e1c011e69401218e9", size = 38692502 }, - { url = "https://files.pythonhosted.org/packages/5c/20/acdd4efb8a68b842968f7bc5611b1aeb819794508771ad104de418701422/scipy-1.15.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:6f223753c6ea76983af380787611ae1291e3ceb23917393079dcc746ba60cfb5", size = 30085508 }, - { url = "https://files.pythonhosted.org/packages/42/55/39cf96ca7126f1e78ee72a6344ebdc6702fc47d037319ad93221063e6cf4/scipy-1.15.2-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:ecf797d2d798cf7c838c6d98321061eb3e72a74710e6c40540f0e8087e3b499e", size = 22359166 }, - { url = "https://files.pythonhosted.org/packages/51/48/708d26a4ab8a1441536bf2dfcad1df0ca14a69f010fba3ccbdfc02df7185/scipy-1.15.2-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:9b18aa747da280664642997e65aab1dd19d0c3d17068a04b3fe34e2559196cb9", size = 25112047 }, - { url = "https://files.pythonhosted.org/packages/dd/65/f9c5755b995ad892020381b8ae11f16d18616208e388621dfacc11df6de6/scipy-1.15.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:87994da02e73549dfecaed9e09a4f9d58a045a053865679aeb8d6d43747d4df3", size = 35536214 }, - { url = "https://files.pythonhosted.org/packages/de/3c/c96d904b9892beec978562f64d8cc43f9cca0842e65bd3cd1b7f7389b0ba/scipy-1.15.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:69ea6e56d00977f355c0f84eba69877b6df084516c602d93a33812aa04d90a3d", size = 37646981 }, - { url = "https://files.pythonhosted.org/packages/3d/74/c2d8a24d18acdeae69ed02e132b9bc1bb67b7bee90feee1afe05a68f9d67/scipy-1.15.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:888307125ea0c4466287191e5606a2c910963405ce9671448ff9c81c53f85f58", size = 37230048 }, - { url = "https://files.pythonhosted.org/packages/42/19/0aa4ce80eca82d487987eff0bc754f014dec10d20de2f66754fa4ea70204/scipy-1.15.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9412f5e408b397ff5641080ed1e798623dbe1ec0d78e72c9eca8992976fa65aa", size = 40010322 }, - { url = "https://files.pythonhosted.org/packages/d0/d2/f0683b7e992be44d1475cc144d1f1eeae63c73a14f862974b4db64af635e/scipy-1.15.2-cp310-cp310-win_amd64.whl", hash = "sha256:b5e025e903b4f166ea03b109bb241355b9c42c279ea694d8864d033727205e65", size = 41233385 }, { url = "https://files.pythonhosted.org/packages/40/1f/bf0a5f338bda7c35c08b4ed0df797e7bafe8a78a97275e9f439aceb46193/scipy-1.15.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:92233b2df6938147be6fa8824b8136f29a18f016ecde986666be5f4d686a91a4", size = 38703651 }, { url = "https://files.pythonhosted.org/packages/de/54/db126aad3874601048c2c20ae3d8a433dbfd7ba8381551e6f62606d9bd8e/scipy-1.15.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:62ca1ff3eb513e09ed17a5736929429189adf16d2d740f44e53270cc800ecff1", size = 30102038 }, { url = "https://files.pythonhosted.org/packages/61/d8/84da3fffefb6c7d5a16968fe5b9f24c98606b165bb801bb0b8bc3985200f/scipy-1.15.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:4c6676490ad76d1c2894d77f976144b41bd1a4052107902238047fb6a473e971", size = 22375518 }, @@ -1583,24 +1289,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0a/c8/b3f566db71461cabd4b2d5b39bcc24a7e1c119535c8361f81426be39bb47/scipy-1.15.2-cp313-cp313t-win_amd64.whl", hash = "sha256:fe8a9eb875d430d81755472c5ba75e84acc980e4a8f6204d402849234d3017db", size = 40477705 }, ] -[[package]] -name = "send2trash" -version = "1.8.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fd/3a/aec9b02217bb79b87bbc1a21bc6abc51e3d5dcf65c30487ac96c0908c722/Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf", size = 17394 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9", size = 18072 }, -] - -[[package]] -name = "setuptools" -version = "78.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a9/5a/0db4da3bc908df06e5efae42b44e75c81dd52716e10192ff36d0c1c8e379/setuptools-78.1.0.tar.gz", hash = "sha256:18fd474d4a82a5f83dac888df697af65afa82dec7323d09c3e37d1f14288da54", size = 1367827 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/54/21/f43f0a1fa8b06b32812e0975981f4677d28e0f3271601dc88ac5a5b83220/setuptools-78.1.0-py3-none-any.whl", hash = "sha256:3e386e96793c8702ae83d17b853fb93d3e09ef82ec62722e61da5cd22376dcd8", size = 1256108 }, -] - [[package]] name = "six" version = "1.17.0" @@ -1638,63 +1326,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/35/32/c2e9dbce03a3c64c9cf8b5c667368e9b6d9f236a43659cf112b6a86446e1/sourcery-1.35.0-py2.py3-none-win_amd64.whl", hash = "sha256:61db72f1183abd231bc448a4d03a74738f3b1013ad4396dc2935c080c3863c36", size = 78356050 }, ] -[[package]] -name = "sphinx" -version = "8.1.3" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "alabaster", marker = "python_full_version < '3.11'" }, - { name = "babel", marker = "python_full_version < '3.11'" }, - { name = "colorama", marker = "python_full_version < '3.11' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version < '3.11'" }, - { name = "imagesize", marker = "python_full_version < '3.11'" }, - { name = "jinja2", marker = "python_full_version < '3.11'" }, - { name = "packaging", marker = "python_full_version < '3.11'" }, - { name = "pygments", marker = "python_full_version < '3.11'" }, - { name = "requests", marker = "python_full_version < '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version < '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version < '3.11'" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/6f/6d/be0b61178fe2cdcb67e2a92fc9ebb488e3c51c4f74a36a7824c0adf23425/sphinx-8.1.3.tar.gz", hash = "sha256:43c1911eecb0d3e161ad78611bc905d1ad0e523e4ddc202a58a821773dc4c927", size = 8184611 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/60/1ddff83a56d33aaf6f10ec8ce84b4c007d9368b21008876fceda7e7381ef/sphinx-8.1.3-py3-none-any.whl", hash = "sha256:09719015511837b76bf6e03e42eb7595ac8c2e41eeb9c29c5b755c6b677992a2", size = 3487125 }, -] - [[package]] name = "sphinx" version = "8.2.3" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] dependencies = [ - { name = "alabaster", marker = "python_full_version >= '3.11'" }, - { name = "babel", marker = "python_full_version >= '3.11'" }, - { name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" }, - { name = "docutils", marker = "python_full_version >= '3.11'" }, - { name = "imagesize", marker = "python_full_version >= '3.11'" }, - { name = "jinja2", marker = "python_full_version >= '3.11'" }, - { name = "packaging", marker = "python_full_version >= '3.11'" }, - { name = "pygments", marker = "python_full_version >= '3.11'" }, - { name = "requests", marker = "python_full_version >= '3.11'" }, - { name = "roman-numerals-py", marker = "python_full_version >= '3.11'" }, - { name = "snowballstemmer", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-applehelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-devhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-htmlhelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-jsmath", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-qthelp", marker = "python_full_version >= '3.11'" }, - { name = "sphinxcontrib-serializinghtml", marker = "python_full_version >= '3.11'" }, + { name = "alabaster" }, + { name = "babel" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "docutils" }, + { name = "imagesize" }, + { name = "jinja2" }, + { name = "packaging" }, + { name = "pygments" }, + { name = "requests" }, + { name = "roman-numerals-py" }, + { name = "snowballstemmer" }, + { name = "sphinxcontrib-applehelp" }, + { name = "sphinxcontrib-devhelp" }, + { name = "sphinxcontrib-htmlhelp" }, + { name = "sphinxcontrib-jsmath" }, + { name = "sphinxcontrib-qthelp" }, + { name = "sphinxcontrib-serializinghtml" }, ] sdist = { url = "https://files.pythonhosted.org/packages/38/ad/4360e50ed56cb483667b8e6dadf2d3fda62359593faabbe749a27c4eaca6/sphinx-8.2.3.tar.gz", hash = "sha256:398ad29dee7f63a75888314e9424d40f52ce5a6a87ae88e7071e80af296ec348", size = 8321876 } wheels = [ @@ -1706,39 +1359,19 @@ name = "sphinx-argparse-cli" version = "1.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/5f/ae6043738a48408ce1371303958fa02943d29619dd5d202e147b1dc8552d/sphinx_argparse_cli-1.19.0.tar.gz", hash = "sha256:0374b23560fd4246234c0ef2d8bb97703088c8ae721bf63a84b9fd0d32f87a78", size = 12667 } wheels = [ { url = "https://files.pythonhosted.org/packages/00/1f/7d40169591a70fb1a8ba57345e22c3f6dca5b06ee6efddafe5ccee91336c/sphinx_argparse_cli-1.19.0-py3-none-any.whl", hash = "sha256:c0e069deed1db44d289f90469c04320f1c2249b3dcbd3c3e093bf9a66e4bd8e4", size = 9941 }, ] -[[package]] -name = "sphinx-autodoc-typehints" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/26/f0/43c6a5ff3e7b08a8c3b32f81b859f1b518ccc31e45f22e2b41ced38be7b9/sphinx_autodoc_typehints-3.0.1.tar.gz", hash = "sha256:b9b40dd15dee54f6f810c924f863f9cf1c54f9f3265c495140ea01be7f44fa55", size = 36282 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3c/dc/dc46c5c7c566b7ec5e8f860f9c89533bf03c0e6aadc96fb9b337867e4460/sphinx_autodoc_typehints-3.0.1-py3-none-any.whl", hash = "sha256:4b64b676a14b5b79cefb6628a6dc8070e320d4963e8ff640a2f3e9390ae9045a", size = 20245 }, -] - [[package]] name = "sphinx-autodoc-typehints" version = "3.1.0" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.12'", - "python_full_version == '3.11.*'", -] dependencies = [ - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cb/cc/d38e7260b1bd3af0c84ad8285dfd78236584b74544510584e07963e000ec/sphinx_autodoc_typehints-3.1.0.tar.gz", hash = "sha256:a6b7b0b6df0a380783ce5b29150c2d30352746f027a3e294d37183995d3f23ed", size = 36528 } wheels = [ @@ -1750,8 +1383,7 @@ name = "sphinx-basic-ng" version = "1.0.0b2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/98/0b/a866924ded68efec7a1759587a4e478aec7559d8165fac8b2ad1c0e774d6/sphinx_basic_ng-1.0.0b2.tar.gz", hash = "sha256:9ec55a47c90c8c002b5960c57492ec3021f5193cb26cebc2dc4ea226848651c9", size = 20736 } wheels = [ @@ -1800,8 +1432,7 @@ version = "1.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pyyaml" }, - { name = "sphinx", version = "8.1.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "sphinx", version = "8.2.3", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "sphinx" }, ] sdist = { url = "https://files.pythonhosted.org/packages/97/69/bf039237ad260073e8c02f820b3e00dc34f3a2de20aff7861e6b19d2f8c5/sphinxcontrib_mermaid-1.0.0.tar.gz", hash = "sha256:2e8ab67d3e1e2816663f9347d026a8dee4a858acdd4ad32dd1c808893db88146", size = 15153 } wheels = [ @@ -1826,38 +1457,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072 }, ] -[[package]] -name = "sympy" -version = "1.13.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "mpmath" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/11/8a/5a7fd6284fa8caac23a26c9ddf9c30485a48169344b4bd3b0f02fef1890f/sympy-1.13.3.tar.gz", hash = "sha256:b27fd2c6530e0ab39e275fc9b683895367e51d5da91baa8d3d64db2565fec4d9", size = 7533196 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/99/ff/c87e0622b1dadea79d2fb0b25ade9ed98954c9033722eb707053d310d4f3/sympy-1.13.3-py3-none-any.whl", hash = "sha256:54612cf55a62755ee71824ce692986f23c88ffa77207b30c1368eda4a7060f73", size = 6189483 }, -] - -[[package]] -name = "thonny" -version = "4.1.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "asttokens" }, - { name = "docutils" }, - { name = "jedi" }, - { name = "mypy" }, - { name = "pylint" }, - { name = "pyserial" }, - { name = "send2trash" }, - { name = "setuptools" }, - { name = "wheel" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/9d/4f51f79865f293024d601f9d6e8de367164f59d6892e4635bec0f9692a4e/thonny-4.1.7.tar.gz", hash = "sha256:6b3b5605f524c12462830e232c8eaf01235645820dd13c24d720acab44a58123", size = 2177370 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/96/59/f9d2960539042303dcc7a3fce00d2ee4f24ab117c4ebf8ef8115de2f160e/thonny-4.1.7-py3-none-any.whl", hash = "sha256:1364d16f71071cc1dc56ed844f77b1981f1ef0b87f787aabaec2d228bea1c3e3", size = 2528987 }, -] - [[package]] name = "tomli" version = "2.2.1" @@ -1897,15 +1496,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257 }, ] -[[package]] -name = "tomlkit" -version = "0.13.2" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b1/09/a439bec5888f00a54b8b9f05fa94d7f901d6735ef4e55dcec9bc37b5d8fa/tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79", size = 192885 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/f9/b6/a447b5e4ec71e13871be01ba81f5dfc9d0af7e473da256ff46bc0e24026f/tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde", size = 37955 }, -] - [[package]] name = "typing-extensions" version = "4.13.0" @@ -1937,12 +1527,3 @@ sdist = { url = "https://files.pythonhosted.org/packages/c7/9c/57d19fa093bcf5ac6 wheels = [ { url = "https://files.pythonhosted.org/packages/c2/eb/c6db6e3001d58c6a9e67c74bb7b4206767caa3ccc28c6b9eaf4c23fb4e34/virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170", size = 4301458 }, ] - -[[package]] -name = "wheel" -version = "0.45.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/8a/98/2d9906746cdc6a6ef809ae6338005b3f21bb568bea3165cfc6a243fdc25c/wheel-0.45.1.tar.gz", hash = "sha256:661e1abd9198507b1409a20c02106d9670b2576e916d58f520316666abca6729", size = 107545 } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/2c/87f3254fd8ffd29e4c02732eee68a83a1d3c346ae39bc6822dcbcb697f2b/wheel-0.45.1-py3-none-any.whl", hash = "sha256:708e7481cc80179af0e556bbf0cc00b8444c7321e2700b8d8580231d13017248", size = 72494 }, -]