diff --git a/.github/workflows/pypi_deployment.yml b/.github/workflows/pypi_deployment.yml index be5f6826..f5657f81 100644 --- a/.github/workflows/pypi_deployment.yml +++ b/.github/workflows/pypi_deployment.yml @@ -17,7 +17,7 @@ jobs: fetch-depth: 0 - name: Set up Python - uses: actions/setup-python@v3 + uses: actions/setup-python@v5 with: python-version: '3.x' diff --git a/.github/workflows/tests_codecov.yml b/.github/workflows/tests_codecov.yml index 20ed87ae..d47b0e3d 100644 --- a/.github/workflows/tests_codecov.yml +++ b/.github/workflows/tests_codecov.yml @@ -6,7 +6,7 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up Python - uses: actions/setup-python@v4 + uses: actions/setup-python@v5 with: python-version: '3.x' - name: Install dependencies @@ -20,4 +20,6 @@ jobs: uses: codecov/codecov-action@v5 with: token: ${{ secrets.CODECOV_TOKEN }} - slug: pathsim/pathsim \ No newline at end of file + slug: pathsim/pathsim + fail_ci_if_error: true + files: ./coverage.xml \ No newline at end of file diff --git a/.gitignore b/.gitignore index abad3771..4e160397 100644 --- a/.gitignore +++ b/.gitignore @@ -45,6 +45,7 @@ htmlcov/ .cache nosetests.xml coverage.xml +coverage.json *.cover .hypothesis/ test_reports/ diff --git a/README.md b/README.md index a67764b9..0c25ee5f 100644 --- a/README.md +++ b/README.md @@ -1,313 +1,112 @@ - -
+ A block-based time-domain system simulation framework in Python +
-## Contributing and Future + -If you want to contribute to **PathSim**s development, check out the [community guidelines](https://pathsim.readthedocs.io/en/latest/contributing.html). If you are curious about what the future holds for **PathSim**, check out the [roadmap](https://pathsim.readthedocs.io/en/latest/roadmap.html)! ++ Homepage • + Documentation • + PathView Editor • + Sponsor +
+--- -## Graphical Interface +PathSim lets you model and simulate complex dynamical systems using an intuitive block diagram approach. Connect sources, integrators, functions, and scopes to build continuous-time, discrete-time, or hybrid systems. -As of Jan 2026, after a massive rewrite, [PathView](https://github.com/pathsim/pathview) is now the official graphical user interface for PathSim. +Minimal dependencies: just `numpy`, `scipy`, and `matplotlib`. +## Features -## Installation +- **Hot-swappable** — modify blocks and solvers during simulation +- **Stiff solvers** — implicit methods (BDF, ESDIRK) for challenging systems +- **Event handling** — zero-crossing detection for hybrid systems +- **Hierarchical** — nest subsystems for modular designs +- **Extensible** — subclass `Block` to create custom components -The latest release version of PathSim is available on [PyPi](https://pypi.org/project/pathsim/) and installable via pip: +## Install -```console +```bash pip install pathsim ``` -Additionally, PathSim is available on [conda-forge](https://anaconda.org/channels/conda-forge/packages/pathsim/overview) and installable via conda: +or with conda: -```console +```bash conda install conda-forge::pathsim ``` - -## Example - Harmonic Oscillator - -There are lots of [examples](https://github.com/pathsim/pathsim/tree/master/examples) of dynamical system simulations in the GitHub repository that showcase PathSim's capabilities. - -But first, lets have a look at how we can simulate the harmonic oscillator (a spring mass damper 2nd order system) using PathSim. The system and its corresponding equivalent block diagram are shown in the figure below: - - - - - -The equation of motion that defines the harmonic oscillator it is give by - -$$ -\ddot{x} + \frac{c}{m} \dot{x} + \frac{k}{m} x = 0 -$$ - -where $c$ is the damping, $k$ the spring constant and $m$ the mass together with the initial conditions $x_0$ and $v_0$ for position and velocity. - -The topology of the block diagram above can be directly defined as blocks and connections in the PathSim framework. First we initialize the blocks needed to represent the dynamical systems with their respective arguments such as initial conditions and gain values, then the blocks are connected using `Connection` objects, forming two feedback loops. - -The `Simulation` instance manages the blocks and connections and advances the system in time with the timestep (`dt`). The `log` flag for logging the simulation progress is also set. Finally, we run the simulation for some number of seconds and plot the results using the `plot()` method of the scope block. - +## Quick Example ```python from pathsim import Simulation, Connection - -# import the blocks we need for the harmonic oscillator from pathsim.blocks import Integrator, Amplifier, Adder, Scope -# initial position and velocity -x0, v0 = 2, 5 - -# parameters (mass, damping, spring constant) -m, c, k = 0.8, 0.2, 1.5 - -# define the blocks -I1 = Integrator(v0) # integrator for velocity -I2 = Integrator(x0) # integrator for position -A1 = Amplifier(-c/m) -A2 = Amplifier(-k/m) -P1 = Adder() -Sc = Scope(labels=["v(t)", "x(t)"]) - -blocks = [I1, I2, A1, A2, P1, Sc] - -# define the connections between the blocks -connections = [ - Connection(I1, I2, A1, Sc), # one to many connection - Connection(I2, A2, Sc[1]), - Connection(A1, P1), # default connection to port 0 - Connection(A2, P1[1]), # specific connection to port 1 - Connection(P1, I1), - ] - -# create a simulation instance from the blocks and connections -Sim = Simulation(blocks, connections, dt=0.05) - -# run the simulation for 30 seconds -Sim.run(duration=30.0) - -# plot the results directly from the scope -Sc.plot() - -# read the results from the scope for further processing -time, data = Sc.read() +# Damped harmonic oscillator: x'' + 0.5x' + 2x = 0 +int_v = Integrator(5) # velocity, v0=5 +int_x = Integrator(2) # position, x0=2 +amp_c = Amplifier(-0.5) # damping +amp_k = Amplifier(-2) # spring +add = Adder() +scp = Scope() + +sim = Simulation( + blocks=[int_v, int_x, amp_c, amp_k, add, scp], + connections=[ + Connection(int_v, int_x, amp_c), + Connection(int_x, amp_k, scp), + Connection(amp_c, add), + Connection(amp_k, add[1]), + Connection(add, int_v), + ], + dt=0.05 +) + +sim.run(30) +scp.plot() ``` - - - -## Stiff Systems - -PathSim implements a large variety of implicit integrators such as diagonally implicit runge-kutta (`DIRK2`, `ESDIRK43`, etc.) and multistep (`BDF2`, `GEAR52A`, etc.) methods. This enables the simulation of very stiff systems where the timestep is limited by stability and not accuracy of the method. - -A common example for a stiff system is the Van der Pol oscillator where the parameter $\mu$ "controls" the severity of the stiffness. It is defined by the following second order ODE: - -$$ -\ddot{x} + \mu (1 - x^2) \dot{x} + x = 0 -$$ - -The Van der Pol ODE can be translated into a block diagram like the one below, where the two states are handled by two distinct integrators. - - - - +## PathView -Lets translate it to PathSim using two `Integrator` blocks and a `Function` block. The parameter is set to $\mu = 1000$ which means severe stiffness. - - -```python -from pathsim import Simulation, Connection -from pathsim.blocks import Integrator, Scope, Function - -# implicit adaptive timestep solver -from pathsim.solvers import ESDIRK54 - -# initial conditions -x1, x2 = 2, 0 - -# van der Pol parameter (1000 is very stiff) -mu = 1000 - -# blocks that define the system -Sc = Scope(labels=["$x_1(t)$"]) -I1 = Integrator(x1) -I2 = Integrator(x2) -Fn = Function(lambda x1, x2: mu*(1 - x1**2)*x2 - x1) - -blocks = [I1, I2, Fn, Sc] - -# the connections between the blocks -connections = [ - Connection(I2, I1, Fn[1]), - Connection(I1, Fn, Sc), - Connection(Fn, I2) - ] - -# initialize simulation with the blocks, connections, timestep and logging enabled -Sim = Simulation( - blocks, - connections, - dt=0.05, - Solver=ESDIRK54, - tolerance_lte_abs=1e-5, - tolerance_lte_rel=1e-3, - ) - -# run simulation for some number of seconds -Sim.run(3*mu) - -# plot the results directly (steps highlighted) -Sc.plot(".-") -``` - - - - -## Event Detection - -PathSim has an event handling system that monitors the simulation state and can find and locate discrete events by evaluating an event function and trigger callbacks or state transformations. Multiple event types are supported such as `ZeroCrossing` or `Schedule`. - -This enables the simulation of hybrid continuous time systems with discrete events. - - - - - -Probably the most popular example for this is the bouncing ball (see figure above) where discrete events occur whenever the ball touches the floor. The event in this case is a zero-crossing. - -The dynamics of this system can be translated into a block diagramm in the following way: - - - - - -And built and simulated with `PathSim` like this: - -```python -from pathsim import Simulation, Connection -from pathsim.blocks import Integrator, Constant, Scope -from pathsim.solvers import RKBS32 - -# event library -from pathsim.events import ZeroCrossing - -# initial values -x0, v0 = 1, 10 - -# blocks that define the system -Ix = Integrator(x0) # v -> x -Iv = Integrator(v0) # a -> v -Cn = Constant(-9.81) # gravitational acceleration -Sc = Scope(labels=["x", "v"]) - -blocks = [Ix, Iv, Cn, Sc] - -# the connections between the blocks -connections = [ - Connection(Cn, Iv), - Connection(Iv, Ix), - Connection(Ix, Sc) - ] - -# event function for zero crossing detection -def func_evt(t): - i, o, s = Ix() # get block inputs, outputs and states - return s - -# action function for state transformation -def func_act(t): - i1, o1, s1 = Ix() - i2, o2, s2 = Iv() - Ix.engine.set(abs(s1)) - Iv.engine.set(-0.9*s2) - -# event (zero-crossing) -> ball makes contact -E1 = ZeroCrossing( - func_evt=func_evt, - func_act=func_act, - tolerance=1e-4, - ) - -events = [E1] - -# initialize simulation with the blocks, connections, timestep -Sim = Simulation( - blocks, - connections, - events, - dt=0.1, - Solver=RKBS32, - dt_max=0.1, - ) - -# run the simulation -Sim.run(20) - -# plot the recordings from the scope -Sc.plot() -``` - - - -During the event handling, the simulator approaches the event until the specified tolerance is met. You can see this by analyzing the timesteps taken by the adaptive integrator `RKBS32`. - - -```python -import numpy as np -import matplotlib.pyplot as plt +[PathView](https://view.pathsim.org) is the graphical editor for PathSim — design systems visually and export to Python. -fig, ax = plt.subplots(figsize=(8,4), tight_layout=True, dpi=120) +## Learn More -time, _ = Sc.read() +- [Documentation](https://docs.pathsim.org) — tutorials, examples, and API reference +- [Homepage](https://pathsim.org) — overview and getting started +- [Contributing](https://docs.pathsim.org/pathsim/latest/contributing) — how to contribute -# add detected events -for t in E1: ax.axvline(t, ls="--", c="k") +## Citation -# plot the timesteps -ax.plot(time[:-1], np.diff(time)) +If you use PathSim in research, please cite: -ax.set_yscale("log") -ax.set_ylabel("dt [s]") -ax.set_xlabel("time [s]") -ax.grid(True) +```bibtex +@article{Rother2025, + author = {Rother, Milan}, + title = {PathSim - A System Simulation Framework}, + journal = {Journal of Open Source Software}, + year = {2025}, + volume = {10}, + number = {109}, + pages = {8158}, + doi = {10.21105/joss.08158} +} ``` +## License - +MIT diff --git a/conftest.py b/conftest.py new file mode 100644 index 00000000..b251a0d6 --- /dev/null +++ b/conftest.py @@ -0,0 +1,13 @@ +def pytest_addoption(parser): + parser.addoption( + "--run-all", + action="store_true", + default=False, + help="Run all tests including slow eval tests.", + ) + + +def pytest_configure(config): + if config.getoption("--run-all"): + # Override the default marker filter + config.option.markexpr = "" diff --git a/coverage.json b/coverage.json deleted file mode 100644 index 63a13c38..00000000 --- a/coverage.json +++ /dev/null @@ -1 +0,0 @@ -{"meta": {"format": 3, "version": "7.6.9", "timestamp": "2025-10-17T09:26:24.757195", "branch_coverage": false, "show_contexts": false}, "files": {"src\\pathsim\\__init__.py": {"executed_lines": [1, 3, 4, 8, 9, 10], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [5, 6], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 3, 4, 8, 9, 10], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [5, 6], "excluded_lines": []}}, "classes": {"": {"executed_lines": [1, 3, 4, 8, 9, 10], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [5, 6], "excluded_lines": []}}}, "src\\pathsim\\_constants.py": {"executed_lines": [12, 17, 18, 19, 20, 21, 26, 27, 28, 29, 30, 31, 32, 37, 38, 43, 48, 49, 50, 55, 56, 57, 58, 59, 60], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [12, 17, 18, 19, 20, 21, 26, 27, 28, 29, 30, 31, 32, 37, 38, 43, 48, 49, 50, 55, 56, 57, 58, 59, 60], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [12, 17, 18, 19, 20, 21, 26, 27, 28, 29, 30, 31, 32, 37, 38, 43, 48, 49, 50, 55, 56, 57, 58, 59, 60], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\_version.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 18, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 18, "excluded_lines": 0}, "missing_lines": [4, 13, 14, 15, 16, 18, 19, 21, 22, 24, 25, 26, 27, 28, 29, 31, 32, 34], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 18, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 18, "excluded_lines": 0}, "missing_lines": [4, 13, 14, 15, 16, 18, 19, 21, 22, 24, 25, 26, 27, 28, 29, 31, 32, 34], "excluded_lines": []}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 18, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 18, "excluded_lines": 0}, "missing_lines": [4, 13, 14, 15, 16, 18, 19, 21, 22, 24, 25, 26, 27, 28, 29, 31, 32, 34], "excluded_lines": []}}}, "src\\pathsim\\blocks\\__init__.py": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28], "summary": {"covered_lines": 28, "num_statements": 28, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\_block.py": {"executed_lines": [13, 14, 15, 20, 21, 88, 89, 92, 93, 95, 98, 99, 102, 103, 106, 109, 112, 115, 116, 119, 135, 138, 154, 157, 158, 161, 162, 165, 166, 168, 182, 185, 188, 191, 193, 196, 197, 202, 203, 213, 214, 216, 217, 230, 249, 253, 254, 258, 262, 263, 265, 268, 273, 274, 277, 280, 281, 284, 299, 302, 304, 311, 319, 320, 325, 341, 344, 351, 354, 366, 371, 383, 388, 404, 405, 406, 407, 412, 433, 434, 437, 440, 441, 442, 447, 450, 474, 477, 506], "summary": {"covered_lines": 88, "num_statements": 101, "percent_covered": 87.12871287128714, "percent_covered_display": "87", "missing_lines": 13, "excluded_lines": 0}, "missing_lines": [170, 173, 174, 177, 178, 180, 225, 244, 255, 264, 307, 308, 444], "excluded_lines": [], "functions": {"Block.__init__": {"executed_lines": [98, 99, 102, 103, 106, 109, 112, 115, 116], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.__len__": {"executed_lines": [135], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.__getitem__": {"executed_lines": [154, 157, 158, 161, 162, 165, 166, 168, 182, 185, 188], "summary": {"covered_lines": 11, "num_statements": 17, "percent_covered": 64.70588235294117, "percent_covered_display": "65", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [170, 173, 174, 177, 178, 180], "excluded_lines": []}, "Block.__call__": {"executed_lines": [193], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.__bool__": {"executed_lines": [197], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.size": {"executed_lines": [213, 214], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.shape": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [225], "excluded_lines": []}, "Block.plot": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [244], "excluded_lines": []}, "Block.on": {"executed_lines": [253, 254], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [255], "excluded_lines": []}, "Block.off": {"executed_lines": [262, 263, 265], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [264], "excluded_lines": []}, "Block.reset": {"executed_lines": [273, 274, 277, 280, 281], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.linearize": {"executed_lines": [299, 302, 304], "summary": {"covered_lines": 3, "num_statements": 5, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [307, 308], "excluded_lines": []}, "Block.delinearize": {"executed_lines": [319, 320], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.set_solver": {"executed_lines": [341], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.revert": {"executed_lines": [351], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.buffer": {"executed_lines": [366], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.sample": {"executed_lines": [383], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.get_all": {"executed_lines": [404, 405, 406, 407], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.update": {"executed_lines": [433, 434, 437, 440, 441, 442, 447], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [444], "excluded_lines": []}, "Block.solve": {"executed_lines": [474], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Block.step": {"executed_lines": [506], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [13, 14, 15, 20, 21, 88, 89, 92, 93, 95, 119, 138, 191, 196, 202, 203, 216, 217, 230, 249, 258, 268, 284, 311, 325, 344, 354, 371, 388, 412, 450, 477], "summary": {"covered_lines": 31, "num_statements": 31, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Block": {"executed_lines": [98, 99, 102, 103, 106, 109, 112, 115, 116, 135, 154, 157, 158, 161, 162, 165, 166, 168, 182, 185, 188, 193, 197, 213, 214, 253, 254, 262, 263, 265, 273, 274, 277, 280, 281, 299, 302, 304, 319, 320, 341, 351, 366, 383, 404, 405, 406, 407, 433, 434, 437, 440, 441, 442, 447, 474, 506], "summary": {"covered_lines": 57, "num_statements": 70, "percent_covered": 81.42857142857143, "percent_covered_display": "81", "missing_lines": 13, "excluded_lines": 0}, "missing_lines": [170, 173, 174, 177, 178, 180, 225, 244, 255, 264, 307, 308, 444], "excluded_lines": []}, "": {"executed_lines": [13, 14, 15, 20, 21, 88, 89, 92, 93, 95, 119, 138, 191, 196, 202, 203, 216, 217, 230, 249, 258, 268, 284, 311, 325, 344, 354, 371, 388, 412, 450, 477], "summary": {"covered_lines": 31, "num_statements": 31, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\adder.py": {"executed_lines": [12, 14, 16, 21, 22, 79, 80, 83, 85, 86, 89, 90, 93, 96, 104, 105, 106, 107, 108, 111, 113, 114, 115, 116, 117, 118, 121, 127, 129, 132, 141, 142, 143], "summary": {"covered_lines": 32, "num_statements": 32, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"Adder.__init__": {"executed_lines": [86, 89, 90, 93, 96, 104, 105, 106, 107, 108, 111, 113, 114, 115, 116, 117, 118, 121], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Adder.__len__": {"executed_lines": [129], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Adder.update": {"executed_lines": [141, 142, 143], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 79, 80, 83, 85, 127, 132], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Adder": {"executed_lines": [86, 89, 90, 93, 96, 104, 105, 106, 107, 108, 111, 113, 114, 115, 116, 117, 118, 121, 129, 141, 142, 143], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 79, 80, 83, 85, 127, 132], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\amplifier.py": {"executed_lines": [10, 12, 17, 18, 56, 57, 60, 61, 63, 64, 65, 67, 73, 81], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"Amplifier.__init__": {"executed_lines": [64, 65, 67], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Amplifier.update": {"executed_lines": [81], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 17, 18, 56, 57, 60, 61, 63, 73], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Amplifier": {"executed_lines": [64, 65, 67, 81], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 17, 18, 56, 57, 60, 61, 63, 73], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\comparator.py": {"executed_lines": [12, 14, 15, 20, 21, 48, 49, 52, 53, 55, 74], "summary": {"covered_lines": 10, "num_statements": 20, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 10, "excluded_lines": 0}, "missing_lines": [56, 58, 59, 60, 62, 63, 66, 95, 96, 98], "excluded_lines": [], "functions": {"Comparator.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [56, 58, 59, 60, 62, 66], "excluded_lines": []}, "Comparator.__init__.func_evt": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [63], "excluded_lines": []}, "Comparator.update": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [95, 96, 98], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 20, 21, 48, 49, 52, 53, 55, 74], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Comparator": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 10, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 10, "excluded_lines": 0}, "missing_lines": [56, 58, 59, 60, 62, 63, 66, 95, 96, 98], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 20, 21, 48, 49, 52, 53, 55, 74], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\converters.py": {"executed_lines": [12, 14, 15, 16, 21, 22, 65, 66, 69, 71, 72, 74, 75, 76, 77, 80, 83, 85, 104, 113, 115, 118, 119, 163, 164, 167, 169, 170, 172, 173, 174, 175, 178, 181, 183, 196, 205, 207], "summary": {"covered_lines": 36, "num_statements": 50, "percent_covered": 72.0, "percent_covered_display": "72", "missing_lines": 14, "excluded_lines": 0}, "missing_lines": [88, 89, 91, 92, 93, 94, 97, 100, 101, 186, 189, 190, 192, 193], "excluded_lines": [], "functions": {"ADC.__init__": {"executed_lines": [72, 74, 75, 76, 77, 80, 83, 85, 104], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ADC.__init__._sample": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 9, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 9, "excluded_lines": 0}, "missing_lines": [88, 89, 91, 92, 93, 94, 97, 100, 101], "excluded_lines": []}, "ADC.__len__": {"executed_lines": [115], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DAC.__init__": {"executed_lines": [170, 172, 173, 174, 175, 178, 181, 183, 196], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DAC.__init__._sample": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [186, 189, 190, 192, 193], "excluded_lines": []}, "DAC.__len__": {"executed_lines": [207], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 16, 21, 22, 65, 66, 69, 71, 113, 118, 119, 163, 164, 167, 169, 205], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ADC": {"executed_lines": [72, 74, 75, 76, 77, 80, 83, 85, 104, 115], "summary": {"covered_lines": 10, "num_statements": 19, "percent_covered": 52.63157894736842, "percent_covered_display": "53", "missing_lines": 9, "excluded_lines": 0}, "missing_lines": [88, 89, 91, 92, 93, 94, 97, 100, 101], "excluded_lines": []}, "DAC": {"executed_lines": [170, 172, 173, 174, 175, 178, 181, 183, 196, 207], "summary": {"covered_lines": 10, "num_statements": 15, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [186, 189, 190, 192, 193], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 16, 21, 22, 65, 66, 69, 71, 113, 118, 119, 163, 164, 167, 169, 205], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\counter.py": {"executed_lines": [12, 14, 15, 20, 21, 40, 41, 44, 45, 47, 62, 67, 86, 87, 110, 122, 123, 146], "summary": {"covered_lines": 15, "num_statements": 28, "percent_covered": 53.57142857142857, "percent_covered_display": "54", "missing_lines": 13, "excluded_lines": 0}, "missing_lines": [48, 50, 51, 54, 59, 64, 83, 111, 114, 119, 147, 150, 155], "excluded_lines": [], "functions": {"Counter.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [48, 50, 51, 54, 59], "excluded_lines": []}, "Counter.__len__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [64], "excluded_lines": []}, "Counter.update": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [83], "excluded_lines": []}, "CounterUp.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [111, 114, 119], "excluded_lines": []}, "CounterDown.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [147, 150, 155], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 20, 21, 40, 41, 44, 45, 47, 62, 67, 86, 87, 110, 122, 123, 146], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Counter": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 0}, "missing_lines": [48, 50, 51, 54, 59, 64, 83], "excluded_lines": []}, "CounterUp": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [111, 114, 119], "excluded_lines": []}, "CounterDown": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [147, 150, 155], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 20, 21, 40, 41, 44, 45, 47, 62, 67, 86, 87, 110, 122, 123, 146], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\ctrl.py": {"executed_lines": [12, 14, 16, 21, 22, 75, 76, 79, 80, 82, 83, 86, 87, 88, 91, 93, 94, 95, 96, 97, 98, 100, 103, 106, 107, 108, 109, 112, 115, 122, 123, 126, 139, 141, 144, 152, 153, 154, 157, 172, 173, 174, 177, 196, 197, 198, 201, 202, 279, 280, 283, 284, 286, 287, 288, 289, 290, 291, 293, 296, 299, 300, 303, 306, 307, 308, 310, 313, 316], "summary": {"covered_lines": 67, "num_statements": 71, "percent_covered": 94.36619718309859, "percent_covered_display": "94", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [101, 104, 294, 297], "excluded_lines": [], "functions": {"PID.__init__": {"executed_lines": [83, 86, 87, 88, 91, 93, 100, 103, 106, 112, 115], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PID.__init__._g_pid": {"executed_lines": [94, 95, 96, 97, 98], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PID.__init__._jac_x_g_pid": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [101], "excluded_lines": []}, "PID.__init__._jac_u_g_pid": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [104], "excluded_lines": []}, "PID.__init__._f_pid": {"executed_lines": [107, 108, 109], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PID.__len__": {"executed_lines": [123], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PID.set_solver": {"executed_lines": [139, 141], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PID.update": {"executed_lines": [152, 153, 154], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PID.solve": {"executed_lines": [172, 173, 174], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PID.step": {"executed_lines": [196, 197, 198], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AntiWindupPID.__init__": {"executed_lines": [280, 283, 284, 286, 293, 296, 299, 313, 316], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AntiWindupPID.__init__._g_pid": {"executed_lines": [287, 288, 289, 290, 291], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AntiWindupPID.__init__._jac_x_g_pid": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [294], "excluded_lines": []}, "AntiWindupPID.__init__._jac_u_g_pid": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [297], "excluded_lines": []}, "AntiWindupPID.__init__._f_pid": {"executed_lines": [300, 303, 306, 307, 308, 310], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 75, 76, 79, 80, 82, 122, 126, 144, 157, 177, 201, 202, 279], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"PID": {"executed_lines": [83, 86, 87, 88, 91, 93, 94, 95, 96, 97, 98, 100, 103, 106, 107, 108, 109, 112, 115, 123, 139, 141, 152, 153, 154, 172, 173, 174, 196, 197, 198], "summary": {"covered_lines": 31, "num_statements": 33, "percent_covered": 93.93939393939394, "percent_covered_display": "94", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [101, 104], "excluded_lines": []}, "AntiWindupPID": {"executed_lines": [280, 283, 284, 286, 287, 288, 289, 290, 291, 293, 296, 299, 300, 303, 306, 307, 308, 310, 313, 316], "summary": {"covered_lines": 20, "num_statements": 22, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [294, 297], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 75, 76, 79, 80, 82, 122, 126, 144, 157, 177, 201, 202, 279], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\delay.py": {"executed_lines": [12, 14, 16, 21, 22, 63, 64, 67, 68, 70, 71, 74, 77, 80, 82, 85, 86, 89, 92, 103, 104, 107, 118], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"Delay.__init__": {"executed_lines": [71, 74, 77], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Delay.__len__": {"executed_lines": [82], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Delay.reset": {"executed_lines": [86, 89], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Delay.update": {"executed_lines": [103, 104], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Delay.sample": {"executed_lines": [118], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 63, 64, 67, 68, 70, 80, 85, 92, 107], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Delay": {"executed_lines": [71, 74, 77, 82, 86, 89, 103, 104, 118], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 63, 64, 67, 68, 70, 80, 85, 92, 107], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\differentiator.py": {"executed_lines": [12, 14, 16, 21, 22, 66, 67, 70, 71, 73, 74, 77, 79, 83, 89, 90, 93, 107, 109, 112, 121, 122, 123, 126, 146, 165, 166, 167], "summary": {"covered_lines": 27, "num_statements": 30, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [141, 142, 143], "excluded_lines": [], "functions": {"Differentiator.__init__": {"executed_lines": [74, 77, 79, 83], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Differentiator.__len__": {"executed_lines": [90], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Differentiator.set_solver": {"executed_lines": [107, 109], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Differentiator.update": {"executed_lines": [121, 122, 123], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Differentiator.solve": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [141, 142, 143], "excluded_lines": []}, "Differentiator.step": {"executed_lines": [165, 166, 167], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 66, 67, 70, 71, 73, 89, 93, 112, 126, 146], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Differentiator": {"executed_lines": [74, 77, 79, 83, 90, 107, 109, 121, 122, 123, 165, 166, 167], "summary": {"covered_lines": 13, "num_statements": 16, "percent_covered": 81.25, "percent_covered_display": "81", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [141, 142, 143], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 66, 67, 70, 71, 73, 89, 93, 112, 126, 146], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\dynsys.py": {"executed_lines": [10, 12, 14, 19, 20, 54, 62, 65, 66, 69, 72, 75, 79, 84, 96, 97, 98, 101, 113, 115, 118, 121, 130, 131, 134, 154], "summary": {"covered_lines": 25, "num_statements": 31, "percent_covered": 80.64516129032258, "percent_covered_display": "81", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [149, 150, 151, 173, 174, 175], "excluded_lines": [], "functions": {"DynamicalSystem.__init__": {"executed_lines": [62, 65, 66, 69, 72, 75, 79], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicalSystem.__len__": {"executed_lines": [96, 97, 98], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicalSystem.set_solver": {"executed_lines": [113, 115, 118], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicalSystem.update": {"executed_lines": [130, 131], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicalSystem.solve": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [149, 150, 151], "excluded_lines": []}, "DynamicalSystem.step": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [173, 174, 175], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 19, 20, 54, 84, 101, 121, 134, 154], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"DynamicalSystem": {"executed_lines": [62, 65, 66, 69, 72, 75, 79, 96, 97, 98, 113, 115, 118, 130, 131], "summary": {"covered_lines": 15, "num_statements": 21, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [149, 150, 151, 173, 174, 175], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 19, 20, 54, 84, 101, 121, 134, 154], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\filters.py": {"executed_lines": [11, 13, 15, 17, 22, 23, 39, 40, 43, 44, 46, 49, 50, 53, 54, 57, 58, 61, 62, 78, 79, 82, 83, 85, 88, 89, 92, 93, 96, 97, 100, 101, 117, 118, 121, 122, 124, 127, 128, 130, 131, 134, 137, 140, 141, 157, 158, 161, 162, 164, 180, 181, 199, 200, 203, 204, 206, 209, 210, 213, 214, 217, 222, 225, 228], "summary": {"covered_lines": 60, "num_statements": 68, "percent_covered": 88.23529411764706, "percent_covered_display": "88", "missing_lines": 8, "excluded_lines": 0}, "missing_lines": [167, 168, 170, 171, 174, 177, 218, 219], "excluded_lines": [], "functions": {"ButterworthLowpassFilter.__init__": {"executed_lines": [49, 50, 53, 54, 57, 58], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ButterworthHighpassFilter.__init__": {"executed_lines": [88, 89, 92, 93, 96, 97], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ButterworthBandpassFilter.__init__": {"executed_lines": [127, 128, 130, 131, 134, 137], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ButterworthBandstopFilter.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [167, 168, 170, 171, 174, 177], "excluded_lines": []}, "AllpassFilter.__init__": {"executed_lines": [209, 210, 213, 214, 217, 222, 225, 228], "summary": {"covered_lines": 8, "num_statements": 10, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [218, 219], "excluded_lines": []}, "": {"executed_lines": [11, 13, 15, 17, 22, 23, 39, 40, 43, 44, 46, 61, 62, 78, 79, 82, 83, 85, 100, 101, 117, 118, 121, 122, 124, 140, 141, 157, 158, 161, 162, 164, 180, 181, 199, 200, 203, 204, 206], "summary": {"covered_lines": 34, "num_statements": 34, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ButterworthLowpassFilter": {"executed_lines": [49, 50, 53, 54, 57, 58], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ButterworthHighpassFilter": {"executed_lines": [88, 89, 92, 93, 96, 97], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ButterworthBandpassFilter": {"executed_lines": [127, 128, 130, 131, 134, 137], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ButterworthBandstopFilter": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [167, 168, 170, 171, 174, 177], "excluded_lines": []}, "AllpassFilter": {"executed_lines": [209, 210, 213, 214, 217, 222, 225, 228], "summary": {"covered_lines": 8, "num_statements": 10, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [218, 219], "excluded_lines": []}, "": {"executed_lines": [11, 13, 15, 17, 22, 23, 39, 40, 43, 44, 46, 61, 62, 78, 79, 82, 83, 85, 100, 101, 117, 118, 121, 122, 124, 140, 141, 157, 158, 161, 162, 164, 180, 181, 199, 200, 203, 204, 206], "summary": {"covered_lines": 34, "num_statements": 34, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\fir.py": {"executed_lines": [12, 13, 15, 16, 21, 22, 73, 74, 77, 78, 80, 112, 119], "summary": {"covered_lines": 12, "num_statements": 27, "percent_covered": 44.44444444444444, "percent_covered_display": "44", "missing_lines": 15, "excluded_lines": 0}, "missing_lines": [81, 83, 84, 85, 88, 89, 91, 94, 97, 100, 103, 114, 115, 116, 121], "excluded_lines": [], "functions": {"FIR.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 8, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 8, "excluded_lines": 0}, "missing_lines": [81, 83, 84, 85, 88, 89, 91, 103], "excluded_lines": []}, "FIR.__init__._update_fir": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [94, 97, 100], "excluded_lines": []}, "FIR.reset": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [114, 115, 116], "excluded_lines": []}, "FIR.__len__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [121], "excluded_lines": []}, "": {"executed_lines": [12, 13, 15, 16, 21, 22, 73, 74, 77, 78, 80, 112, 119], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"FIR": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 15, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 15, "excluded_lines": 0}, "missing_lines": [81, 83, 84, 85, 88, 89, 91, 94, 97, 100, 103, 114, 115, 116, 121], "excluded_lines": []}, "": {"executed_lines": [12, 13, 15, 16, 21, 22, 73, 74, 77, 78, 80, 112, 119], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\fmu.py": {"executed_lines": [10, 12, 13, 14, 19, 20, 70, 71, 74, 75, 77, 81, 82, 83, 84, 88, 89, 90, 91, 94, 97, 100, 103, 106, 112, 115, 116, 118, 119, 120, 121, 122, 123, 124, 127, 130, 131, 148, 151, 158, 159, 162, 165, 168, 177, 180, 183, 185, 189, 190, 191, 194, 199, 200, 201, 204, 206, 207, 208, 209, 210, 218, 219, 220, 221, 222, 223, 226, 228, 239, 241, 244, 249, 252, 254, 255, 256, 259, 261, 262, 263, 266, 268, 273, 291, 293, 296], "summary": {"covered_lines": 86, "num_statements": 125, "percent_covered": 68.8, "percent_covered_display": "69", "missing_lines": 39, "excluded_lines": 0}, "missing_lines": [85, 86, 107, 108, 110, 137, 138, 145, 152, 186, 195, 196, 197, 212, 213, 214, 215, 229, 230, 231, 232, 233, 234, 235, 236, 269, 270, 275, 276, 278, 279, 285, 287, 288, 298, 299, 300, 301, 302], "excluded_lines": [], "functions": {"CoSimulationFMU.__init__": {"executed_lines": [81, 82, 83, 84, 88, 89, 90, 91, 94, 97, 100, 103, 106, 112, 115, 116, 118, 119, 120, 121, 122, 123, 124, 127, 130, 131, 148, 151, 158, 159, 162, 165, 168, 177], "summary": {"covered_lines": 34, "num_statements": 43, "percent_covered": 79.06976744186046, "percent_covered_display": "79", "missing_lines": 9, "excluded_lines": 0}, "missing_lines": [85, 86, 107, 108, 110, 137, 138, 145, 152], "excluded_lines": []}, "CoSimulationFMU._extract_fmu_metadata": {"executed_lines": [183, 185, 189, 190, 191, 194, 199, 200, 201, 204, 206, 207, 208, 209, 210, 218, 219, 220, 221, 222, 223], "summary": {"covered_lines": 21, "num_statements": 29, "percent_covered": 72.41379310344827, "percent_covered_display": "72", "missing_lines": 8, "excluded_lines": 0}, "missing_lines": [186, 195, 196, 197, 212, 213, 214, 215], "excluded_lines": []}, "CoSimulationFMU._set_start_values": {"executed_lines": [228], "summary": {"covered_lines": 1, "num_statements": 9, "percent_covered": 11.11111111111111, "percent_covered_display": "11", "missing_lines": 8, "excluded_lines": 0}, "missing_lines": [229, 230, 231, 232, 233, 234, 235, 236], "excluded_lines": []}, "CoSimulationFMU._step_fmu": {"executed_lines": [241, 244, 249], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "CoSimulationFMU._update_fmu_from_inputs": {"executed_lines": [254, 255, 256], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "CoSimulationFMU._update_outputs_from_fmu": {"executed_lines": [261, 262, 263], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "CoSimulationFMU.update": {"executed_lines": [268], "summary": {"covered_lines": 1, "num_statements": 3, "percent_covered": 33.333333333333336, "percent_covered_display": "33", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [269, 270], "excluded_lines": []}, "CoSimulationFMU.reset": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 0}, "missing_lines": [275, 276, 278, 279, 285, 287, 288], "excluded_lines": []}, "CoSimulationFMU.__len__": {"executed_lines": [293], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "CoSimulationFMU.__del__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [298, 299, 300, 301, 302], "excluded_lines": []}, "": {"executed_lines": [10, 12, 13, 14, 19, 20, 70, 71, 74, 75, 77, 180, 226, 239, 252, 259, 266, 273, 291, 296], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"CoSimulationFMU": {"executed_lines": [81, 82, 83, 84, 88, 89, 90, 91, 94, 97, 100, 103, 106, 112, 115, 116, 118, 119, 120, 121, 122, 123, 124, 127, 130, 131, 148, 151, 158, 159, 162, 165, 168, 177, 183, 185, 189, 190, 191, 194, 199, 200, 201, 204, 206, 207, 208, 209, 210, 218, 219, 220, 221, 222, 223, 228, 241, 244, 249, 254, 255, 256, 261, 262, 263, 268, 293], "summary": {"covered_lines": 67, "num_statements": 106, "percent_covered": 63.20754716981132, "percent_covered_display": "63", "missing_lines": 39, "excluded_lines": 0}, "missing_lines": [85, 86, 107, 108, 110, 137, 138, 145, 152, 186, 195, 196, 197, 212, 213, 214, 215, 229, 230, 231, 232, 233, 234, 235, 236, 269, 270, 275, 276, 278, 279, 285, 287, 288, 298, 299, 300, 301, 302], "excluded_lines": []}, "": {"executed_lines": [10, 12, 13, 14, 19, 20, 70, 71, 74, 75, 77, 180, 226, 239, 252, 259, 266, 273, 291, 296], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\function.py": {"executed_lines": [10, 12, 14, 19, 20, 117, 118, 121, 122, 125, 126, 129, 140, 141, 145, 146, 215, 216, 219, 223, 224, 227, 238, 239], "summary": {"covered_lines": 22, "num_statements": 23, "percent_covered": 95.65217391304348, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [220], "excluded_lines": [], "functions": {"Function.__init__": {"executed_lines": [118, 121, 122, 125, 126], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Function.update": {"executed_lines": [140, 141], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicalFunction.__init__": {"executed_lines": [216, 219, 223, 224], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [220], "excluded_lines": []}, "DynamicalFunction.update": {"executed_lines": [238, 239], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 19, 20, 117, 129, 145, 146, 215, 227], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Function": {"executed_lines": [118, 121, 122, 125, 126, 140, 141], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicalFunction": {"executed_lines": [216, 219, 223, 224, 238, 239], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [220], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 19, 20, 117, 129, 145, 146, 215, 227], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\integrator.py": {"executed_lines": [12, 14, 16, 21, 22, 57, 58, 61, 64, 65, 68, 81, 83, 87, 90, 103, 106, 121, 122, 125, 144, 145], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"Integrator.__init__": {"executed_lines": [58, 61], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Integrator.__len__": {"executed_lines": [65], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Integrator.set_solver": {"executed_lines": [81, 83, 87], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Integrator.update": {"executed_lines": [103], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Integrator.solve": {"executed_lines": [121, 122], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Integrator.step": {"executed_lines": [144, 145], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 57, 64, 68, 90, 106, 125], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Integrator": {"executed_lines": [58, 61, 65, 81, 83, 87, 103, 121, 122, 144, 145], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 57, 64, 68, 90, 106, 125], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\lti.py": {"executed_lines": [13, 15, 16, 18, 20, 21, 23, 28, 29, 84, 87, 90, 91, 92, 93, 96, 97, 98, 99, 100, 103, 104, 107, 108, 110, 113, 118, 125, 127, 129, 142, 144, 148, 151, 171, 190, 191, 192, 195, 196, 236, 237, 240, 241, 243, 246, 249, 252, 255, 256, 264, 265, 267, 268, 273, 274, 312, 315, 318, 321, 324, 325, 361, 362, 365, 366, 368, 371, 374, 377], "summary": {"covered_lines": 65, "num_statements": 68, "percent_covered": 95.58823529411765, "percent_covered_display": "96", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [166, 167, 168], "excluded_lines": [], "functions": {"StateSpace.__init__": {"executed_lines": [87, 90, 91, 92, 93, 96, 97, 98, 99, 100, 103, 104, 107, 108, 110, 113, 118], "summary": {"covered_lines": 17, "num_statements": 17, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "StateSpace.__len__": {"executed_lines": [127], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "StateSpace.set_solver": {"executed_lines": [142, 144, 148], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "StateSpace.solve": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [166, 167, 168], "excluded_lines": []}, "StateSpace.step": {"executed_lines": [190, 191, 192], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TransferFunctionPRC.__init__": {"executed_lines": [246, 249, 252], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TransferFunction.__init__": {"executed_lines": [265, 267, 268], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TransferFunctionZPG.__init__": {"executed_lines": [315, 318, 321], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TransferFunctionNumDen.__init__": {"executed_lines": [371, 374, 377], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [13, 15, 16, 18, 20, 21, 23, 28, 29, 84, 125, 129, 151, 171, 195, 196, 236, 237, 240, 241, 243, 255, 256, 264, 273, 274, 312, 324, 325, 361, 362, 365, 366, 368], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"StateSpace": {"executed_lines": [87, 90, 91, 92, 93, 96, 97, 98, 99, 100, 103, 104, 107, 108, 110, 113, 118, 127, 142, 144, 148, 190, 191, 192], "summary": {"covered_lines": 24, "num_statements": 27, "percent_covered": 88.88888888888889, "percent_covered_display": "89", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [166, 167, 168], "excluded_lines": []}, "TransferFunctionPRC": {"executed_lines": [246, 249, 252], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TransferFunction": {"executed_lines": [265, 267, 268], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TransferFunctionZPG": {"executed_lines": [315, 318, 321], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TransferFunctionNumDen": {"executed_lines": [371, 374, 377], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [13, 15, 16, 18, 20, 21, 23, 28, 29, 84, 125, 129, 151, 171, 195, 196, 236, 237, 240, 241, 243, 255, 256, 264, 273, 274, 312, 324, 325, 361, 362, 365, 366, 368], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\math.py": {"executed_lines": [12, 14, 16, 21, 22, 32, 37, 45, 46, 47, 52, 53, 68, 69, 72, 78, 79, 93, 94, 97, 103, 104, 118, 119, 122, 128, 129, 143, 144, 147, 153, 154, 173, 174, 176, 179, 185, 186, 206, 207, 209, 211, 230, 236, 237, 251, 252, 255, 261, 262, 276, 277, 280, 286, 287, 301, 302, 305, 311, 312, 326, 327, 330, 336, 337, 351, 352, 355, 361, 362, 376, 377, 380, 386, 387, 401, 402, 405, 411, 412, 426, 427, 430, 436, 437, 451, 452, 455, 461, 462, 486, 487, 489, 492, 498, 499, 520, 521, 523, 524, 527, 532], "summary": {"covered_lines": 84, "num_statements": 97, "percent_covered": 86.5979381443299, "percent_covered_display": "87", "missing_lines": 13, "excluded_lines": 0}, "missing_lines": [34, 212, 213, 215, 217, 220, 221, 222, 223, 225, 227, 529, 530], "excluded_lines": [], "functions": {"Math.__len__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [34], "excluded_lines": []}, "Math.update": {"executed_lines": [45, 46, 47], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Sin.__init__": {"executed_lines": [69, 72], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Cos.__init__": {"executed_lines": [94, 97], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Sqrt.__init__": {"executed_lines": [119, 122], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Abs.__init__": {"executed_lines": [144, 147], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Pow.__init__": {"executed_lines": [174, 176, 179], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PowProd.__init__": {"executed_lines": [207, 209, 211, 230], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PowProd.__init__._jac": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 10, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 10, "excluded_lines": 0}, "missing_lines": [212, 213, 215, 217, 220, 221, 222, 223, 225, 227], "excluded_lines": []}, "Exp.__init__": {"executed_lines": [252, 255], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Log.__init__": {"executed_lines": [277, 280], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Log10.__init__": {"executed_lines": [302, 305], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Tan.__init__": {"executed_lines": [327, 330], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Sinh.__init__": {"executed_lines": [352, 355], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Cosh.__init__": {"executed_lines": [377, 380], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Tanh.__init__": {"executed_lines": [402, 405], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Atan.__init__": {"executed_lines": [427, 430], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Norm.__init__": {"executed_lines": [452, 455], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Mod.__init__": {"executed_lines": [487, 489, 492], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Clip.__init__": {"executed_lines": [521, 523, 524, 527, 532], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Clip.__init__._clip_jac": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [529, 530], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 32, 37, 52, 53, 68, 78, 79, 93, 103, 104, 118, 128, 129, 143, 153, 154, 173, 185, 186, 206, 236, 237, 251, 261, 262, 276, 286, 287, 301, 311, 312, 326, 336, 337, 351, 361, 362, 376, 386, 387, 401, 411, 412, 426, 436, 437, 451, 461, 462, 486, 498, 499, 520], "summary": {"covered_lines": 40, "num_statements": 40, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Math": {"executed_lines": [45, 46, 47], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [34], "excluded_lines": []}, "Sin": {"executed_lines": [69, 72], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Cos": {"executed_lines": [94, 97], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Sqrt": {"executed_lines": [119, 122], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Abs": {"executed_lines": [144, 147], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Pow": {"executed_lines": [174, 176, 179], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PowProd": {"executed_lines": [207, 209, 211, 230], "summary": {"covered_lines": 4, "num_statements": 14, "percent_covered": 28.571428571428573, "percent_covered_display": "29", "missing_lines": 10, "excluded_lines": 0}, "missing_lines": [212, 213, 215, 217, 220, 221, 222, 223, 225, 227], "excluded_lines": []}, "Exp": {"executed_lines": [252, 255], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Log": {"executed_lines": [277, 280], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Log10": {"executed_lines": [302, 305], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Tan": {"executed_lines": [327, 330], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Sinh": {"executed_lines": [352, 355], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Cosh": {"executed_lines": [377, 380], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Tanh": {"executed_lines": [402, 405], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Atan": {"executed_lines": [427, 430], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Norm": {"executed_lines": [452, 455], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Mod": {"executed_lines": [487, 489, 492], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Clip": {"executed_lines": [521, 523, 524, 527, 532], "summary": {"covered_lines": 5, "num_statements": 7, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [529, 530], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 32, 37, 52, 53, 68, 78, 79, 93, 103, 104, 118, 128, 129, 143, 153, 154, 173, 185, 186, 206, 236, 237, 251, 261, 262, 276, 286, 287, 301, 311, 312, 326, 336, 337, 351, 361, 362, 376, 386, 387, 401, 411, 412, 426, 436, 437, 451, 461, 462, 486, 498, 499, 520], "summary": {"covered_lines": 40, "num_statements": 40, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\multiplier.py": {"executed_lines": [13, 15, 17, 18, 23, 24, 44, 45, 48, 50, 51, 53, 61, 69, 70], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"Multiplier.__init__": {"executed_lines": [51, 53], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Multiplier.update": {"executed_lines": [69, 70], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [13, 15, 17, 18, 23, 24, 44, 45, 48, 50, 61], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Multiplier": {"executed_lines": [51, 53, 69, 70], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [13, 15, 17, 18, 23, 24, 44, 45, 48, 50, 61], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\noise.py": {"executed_lines": [12, 14, 19, 20, 43, 44, 47, 49, 50, 53, 55, 56, 57, 58, 59, 62, 63, 66, 74, 82, 84, 85, 88, 102, 105, 106, 133, 134, 137, 139, 140, 143, 145, 146, 147, 148, 149, 152, 155, 158, 159, 162, 171, 180, 184, 187, 188, 189, 190, 193, 194, 197, 200, 203, 217], "summary": {"covered_lines": 53, "num_statements": 60, "percent_covered": 88.33333333333333, "percent_covered_display": "88", "missing_lines": 7, "excluded_lines": 0}, "missing_lines": [67, 70, 71, 163, 166, 167, 168], "excluded_lines": [], "functions": {"WhiteNoise.__init__": {"executed_lines": [50, 53, 55, 56, 57, 58, 59], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "WhiteNoise.__len__": {"executed_lines": [63], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "WhiteNoise.reset": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [67, 70, 71], "excluded_lines": []}, "WhiteNoise.sample": {"executed_lines": [82, 84, 85], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "WhiteNoise.update": {"executed_lines": [102], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PinkNoise.__init__": {"executed_lines": [140, 143, 145, 146, 147, 148, 149, 152, 155], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PinkNoise.__len__": {"executed_lines": [159], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PinkNoise.reset": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [163, 166, 167, 168], "excluded_lines": []}, "PinkNoise.sample": {"executed_lines": [180, 184, 187, 188, 189, 190, 193, 194, 197, 200], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PinkNoise.update": {"executed_lines": [217], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 19, 20, 43, 44, 47, 49, 62, 66, 74, 88, 105, 106, 133, 134, 137, 139, 158, 162, 171, 203], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"WhiteNoise": {"executed_lines": [50, 53, 55, 56, 57, 58, 59, 63, 82, 84, 85, 102], "summary": {"covered_lines": 12, "num_statements": 15, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [67, 70, 71], "excluded_lines": []}, "PinkNoise": {"executed_lines": [140, 143, 145, 146, 147, 148, 149, 152, 155, 159, 180, 184, 187, 188, 189, 190, 193, 194, 197, 200, 217], "summary": {"covered_lines": 21, "num_statements": 25, "percent_covered": 84.0, "percent_covered_display": "84", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [163, 166, 167, 168], "excluded_lines": []}, "": {"executed_lines": [12, 14, 19, 20, 43, 44, 47, 49, 62, 66, 74, 88, 105, 106, 133, 134, 137, 139, 158, 162, 171, 203], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\ode.py": {"executed_lines": [12, 14, 16, 21, 22, 89, 96, 99, 102, 105, 108, 114, 115, 118, 130, 132, 135, 138, 152, 155, 170, 171, 172, 175, 194, 195, 196], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"ODE.__init__": {"executed_lines": [96, 99, 102, 105, 108], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ODE.__len__": {"executed_lines": [115], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ODE.set_solver": {"executed_lines": [130, 132, 135], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ODE.update": {"executed_lines": [152], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ODE.solve": {"executed_lines": [170, 171, 172], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ODE.step": {"executed_lines": [194, 195, 196], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 89, 114, 118, 138, 155, 175], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ODE": {"executed_lines": [96, 99, 102, 105, 108, 115, 130, 132, 135, 152, 170, 171, 172, 194, 195, 196], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 89, 114, 118, 138, 155, 175], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\rf.py": {"executed_lines": [19, 21, 22, 23, 27, 28, 30, 35, 36, 65, 69, 73, 74, 77, 78, 80, 81, 83, 84, 85, 87, 88, 90, 93, 107], "summary": {"covered_lines": 24, "num_statements": 28, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [24, 25, 70, 71], "excluded_lines": [], "functions": {"RFNetwork.__init__": {"executed_lines": [69, 73, 74, 77, 78, 80, 81, 83, 84, 85, 87, 88, 90], "summary": {"covered_lines": 13, "num_statements": 15, "percent_covered": 86.66666666666667, "percent_covered_display": "87", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [70, 71], "excluded_lines": []}, "RFNetwork.s": {"executed_lines": [107], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [19, 21, 22, 23, 27, 28, 30, 35, 36, 65, 93], "summary": {"covered_lines": 10, "num_statements": 12, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [24, 25], "excluded_lines": []}}, "classes": {"RFNetwork": {"executed_lines": [69, 73, 74, 77, 78, 80, 81, 83, 84, 85, 87, 88, 90, 107], "summary": {"covered_lines": 14, "num_statements": 16, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [70, 71], "excluded_lines": []}, "": {"executed_lines": [19, 21, 22, 23, 27, 28, 30, 35, 36, 65, 93], "summary": {"covered_lines": 10, "num_statements": 12, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [24, 25], "excluded_lines": []}}}, "src\\pathsim\\blocks\\rng.py": {"executed_lines": [10, 12, 13, 18, 19, 41, 42, 45, 47, 48, 50, 53, 56, 61, 64, 69, 72, 81, 82, 85, 94, 95, 98, 100, 103, 105], "summary": {"covered_lines": 25, "num_statements": 29, "percent_covered": 86.20689655172414, "percent_covered_display": "86", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [62, 106, 108, 109], "excluded_lines": [], "functions": {"RandomNumberGenerator.__init__": {"executed_lines": [48, 50, 53, 56, 61, 64, 69], "summary": {"covered_lines": 7, "num_statements": 8, "percent_covered": 87.5, "percent_covered_display": "88", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [62], "excluded_lines": []}, "RandomNumberGenerator.update": {"executed_lines": [81, 82], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RandomNumberGenerator.sample": {"executed_lines": [94, 95], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RandomNumberGenerator.__len__": {"executed_lines": [100], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RNG.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [106, 108, 109], "excluded_lines": []}, "": {"executed_lines": [10, 12, 13, 18, 19, 41, 42, 45, 47, 72, 85, 98, 103, 105], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"RandomNumberGenerator": {"executed_lines": [48, 50, 53, 56, 61, 64, 69, 81, 82, 94, 95, 100], "summary": {"covered_lines": 12, "num_statements": 13, "percent_covered": 92.3076923076923, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [62], "excluded_lines": []}, "RNG": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [106, 108, 109], "excluded_lines": []}, "": {"executed_lines": [10, 12, 13, 18, 19, 41, 42, 45, 47, 72, 85, 98, 103, 105], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\samplehold.py": {"executed_lines": [12, 13, 18, 19, 36, 56], "summary": {"covered_lines": 5, "num_statements": 12, "percent_covered": 41.666666666666664, "percent_covered_display": "42", "missing_lines": 7, "excluded_lines": 0}, "missing_lines": [37, 39, 40, 42, 43, 48, 57], "excluded_lines": [], "functions": {"SampleHold.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [37, 39, 40, 42, 48], "excluded_lines": []}, "SampleHold.__init__._sample": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [43], "excluded_lines": []}, "SampleHold.__len__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [57], "excluded_lines": []}, "": {"executed_lines": [12, 13, 18, 19, 36, 56], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"SampleHold": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 7, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 7, "excluded_lines": 0}, "missing_lines": [37, 39, 40, 42, 43, 48, 57], "excluded_lines": []}, "": {"executed_lines": [12, 13, 18, 19, 36, 56], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\scope.py": {"executed_lines": [12, 14, 16, 17, 19, 20, 21, 23, 29, 30, 57, 58, 60, 61, 64, 67, 70, 73, 76, 79, 82, 91, 92, 95, 96, 99, 102, 115, 118, 119, 120, 123, 135, 136, 139, 213, 280, 350, 384, 404, 409, 410, 438, 453], "summary": {"covered_lines": 42, "num_statements": 138, "percent_covered": 30.434782608695652, "percent_covered_display": "30", "missing_lines": 96, "excluded_lines": 0}, "missing_lines": [80, 158, 159, 160, 163, 166, 169, 172, 173, 174, 177, 180, 181, 184, 185, 188, 189, 191, 192, 194, 195, 196, 197, 198, 199, 201, 204, 207, 210, 234, 235, 236, 239, 242, 243, 244, 247, 248, 249, 250, 253, 256, 259, 260, 263, 266, 267, 268, 269, 271, 274, 277, 301, 302, 303, 306, 309, 310, 311, 314, 315, 318, 319, 322, 323, 326, 329, 330, 331, 334, 337, 338, 339, 340, 341, 342, 345, 347, 360, 361, 364, 367, 370, 373, 374, 377, 380, 381, 439, 442, 450, 462, 463, 464, 465, 466], "excluded_lines": [], "functions": {"Scope.__init__": {"executed_lines": [61, 64, 67, 70, 73, 76, 79, 82], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Scope.__init__._sample": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [80], "excluded_lines": []}, "Scope.__len__": {"executed_lines": [92], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Scope.reset": {"executed_lines": [96, 99], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Scope.read": {"executed_lines": [115, 118, 119, 120], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Scope.sample": {"executed_lines": [135, 136], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Scope.plot": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 22, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 22, "excluded_lines": 0}, "missing_lines": [158, 159, 160, 163, 166, 169, 172, 173, 174, 177, 180, 181, 184, 185, 188, 189, 191, 192, 194, 204, 207, 210], "excluded_lines": []}, "Scope.plot.on_pick": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [195, 196, 197, 198, 199, 201], "excluded_lines": []}, "Scope.plot2D": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 23, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 23, "excluded_lines": 0}, "missing_lines": [234, 235, 236, 239, 242, 243, 244, 247, 248, 249, 250, 253, 256, 259, 260, 263, 266, 267, 268, 269, 271, 274, 277], "excluded_lines": []}, "Scope.plot3D": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 26, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 26, "excluded_lines": 0}, "missing_lines": [301, 302, 303, 306, 309, 310, 311, 314, 315, 318, 319, 322, 323, 326, 329, 330, 331, 334, 337, 338, 339, 340, 341, 342, 345, 347], "excluded_lines": []}, "Scope.save": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 10, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 10, "excluded_lines": 0}, "missing_lines": [360, 361, 364, 367, 370, 373, 374, 377, 380, 381], "excluded_lines": []}, "Scope.update": {"executed_lines": [404], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RealtimeScope.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [439, 442, 450], "excluded_lines": []}, "RealtimeScope.sample": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [462, 463, 464, 465, 466], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 17, 19, 20, 21, 23, 29, 30, 57, 58, 60, 91, 95, 102, 123, 139, 213, 280, 350, 384, 409, 410, 438, 453], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Scope": {"executed_lines": [61, 64, 67, 70, 73, 76, 79, 82, 92, 96, 99, 115, 118, 119, 120, 135, 136, 404], "summary": {"covered_lines": 18, "num_statements": 106, "percent_covered": 16.9811320754717, "percent_covered_display": "17", "missing_lines": 88, "excluded_lines": 0}, "missing_lines": [80, 158, 159, 160, 163, 166, 169, 172, 173, 174, 177, 180, 181, 184, 185, 188, 189, 191, 192, 194, 195, 196, 197, 198, 199, 201, 204, 207, 210, 234, 235, 236, 239, 242, 243, 244, 247, 248, 249, 250, 253, 256, 259, 260, 263, 266, 267, 268, 269, 271, 274, 277, 301, 302, 303, 306, 309, 310, 311, 314, 315, 318, 319, 322, 323, 326, 329, 330, 331, 334, 337, 338, 339, 340, 341, 342, 345, 347, 360, 361, 364, 367, 370, 373, 374, 377, 380, 381], "excluded_lines": []}, "RealtimeScope": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 8, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 8, "excluded_lines": 0}, "missing_lines": [439, 442, 450, 462, 463, 464, 465, 466], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 17, 19, 20, 21, 23, 29, 30, 57, 58, 60, 91, 95, 102, 123, 139, 213, 280, 350, 384, 409, 410, 438, 453], "summary": {"covered_lines": 24, "num_statements": 24, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\sources.py": {"executed_lines": [12, 14, 15, 16, 21, 22, 31, 32, 35, 37, 38, 39, 42, 44, 47, 61, 62, 65, 66, 134, 135, 138, 140, 141, 143, 144, 146, 149, 151, 154, 168, 173, 174, 187, 188, 191, 193, 194, 196, 197, 198, 201, 202, 205, 220, 223, 224, 225, 228, 229, 242, 243, 246, 248, 249, 251, 252, 253, 256, 257, 260, 261, 262, 265, 266, 279, 280, 283, 285, 286, 288, 289, 290, 293, 294, 297, 312, 313, 316, 317, 320, 321, 353, 354, 357, 359, 368, 370, 371, 372, 374, 376, 379, 380, 383, 384, 387, 388, 391, 392, 395, 397, 402, 403, 406, 407, 410, 426, 429, 432, 443, 450, 460, 461, 508, 509, 512, 514, 525, 528, 529, 530, 531, 532, 535, 536, 537, 540, 541, 544, 545, 548, 549, 552, 567, 570, 578, 579, 581, 587, 591, 593, 594, 595, 598, 600, 601, 604, 614, 617, 618, 621, 624, 626, 636, 638, 639, 645, 646, 681, 682, 685, 687, 696, 699, 700, 701, 702, 703, 704, 705, 706, 709, 710, 711, 714, 715, 716, 717, 718, 719, 722, 723, 726, 727, 728, 729, 732, 737, 742, 747, 753, 760, 767, 774, 781, 784, 800, 801, 804, 805, 806, 807, 808, 810, 811, 812, 813, 815, 816, 818, 819, 820, 822, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 845, 847, 850, 852, 853, 855, 856, 859, 860, 879, 880, 883, 885, 886, 888, 889, 891, 894, 898, 911, 913, 916, 918, 919, 921, 922, 926, 927, 948, 949, 952, 954, 955, 957, 958, 959, 961, 962, 964, 965, 968, 981, 983, 986, 987, 1052, 1053, 1056, 1058, 1059, 1062, 1063, 1064, 1065, 1067, 1068, 1071, 1072, 1075, 1079, 1083, 1085, 1087, 1090, 1092, 1093, 1095, 1096], "summary": {"covered_lines": 281, "num_statements": 315, "percent_covered": 89.2063492063492, "percent_covered_display": "89", "missing_lines": 34, "excluded_lines": 0}, "missing_lines": [399, 436, 438, 439, 440, 445, 446, 447, 452, 453, 456, 571, 574, 575, 584, 607, 608, 611, 733, 734, 735, 738, 739, 740, 743, 744, 745, 748, 749, 750, 892, 895, 1076, 1077], "excluded_lines": [], "functions": {"Constant.__init__": {"executed_lines": [38, 39], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Constant.__len__": {"executed_lines": [44], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Constant.update": {"executed_lines": [61, 62], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Source.__init__": {"executed_lines": [141, 143, 144, 146], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Source.__len__": {"executed_lines": [151], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Source.update": {"executed_lines": [168], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TriangleWaveSource.__init__": {"executed_lines": [194, 196, 197, 198], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TriangleWaveSource.__len__": {"executed_lines": [202], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TriangleWaveSource._triangle_wave": {"executed_lines": [220], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TriangleWaveSource.update": {"executed_lines": [224, 225], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SinusoidalSource.__init__": {"executed_lines": [249, 251, 252, 253], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SinusoidalSource.__len__": {"executed_lines": [257], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SinusoidalSource.update": {"executed_lines": [261, 262], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GaussianPulseSource.__init__": {"executed_lines": [286, 288, 289, 290], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GaussianPulseSource.__len__": {"executed_lines": [294], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GaussianPulseSource._gaussian": {"executed_lines": [312, 313], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GaussianPulseSource.update": {"executed_lines": [317], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SinusoidalPhaseNoiseSource.__init__": {"executed_lines": [368, 370, 371, 372, 374, 376, 379, 380, 383, 384, 387, 388], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SinusoidalPhaseNoiseSource.__len__": {"executed_lines": [392], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SinusoidalPhaseNoiseSource.set_solver": {"executed_lines": [397], "summary": {"covered_lines": 1, "num_statements": 2, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [399], "excluded_lines": []}, "SinusoidalPhaseNoiseSource.reset": {"executed_lines": [403, 406, 407], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SinusoidalPhaseNoiseSource.update": {"executed_lines": [426, 429], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SinusoidalPhaseNoiseSource.sample": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [436, 438, 439, 440], "excluded_lines": []}, "SinusoidalPhaseNoiseSource.solve": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [445, 446, 447], "excluded_lines": []}, "SinusoidalPhaseNoiseSource.step": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [452, 453, 456], "excluded_lines": []}, "ChirpPhaseNoiseSource.__init__": {"executed_lines": [525, 528, 529, 530, 531, 532, 535, 536, 537, 540, 541, 544, 545], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ChirpPhaseNoiseSource.__len__": {"executed_lines": [549], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ChirpPhaseNoiseSource._triangle_wave": {"executed_lines": [567], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ChirpPhaseNoiseSource.reset": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [571, 574, 575], "excluded_lines": []}, "ChirpPhaseNoiseSource.set_solver": {"executed_lines": [579, 581], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [584], "excluded_lines": []}, "ChirpPhaseNoiseSource.sample": {"executed_lines": [591, 593, 594, 595], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ChirpPhaseNoiseSource.update": {"executed_lines": [600, 601], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ChirpPhaseNoiseSource.solve": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [607, 608, 611], "excluded_lines": []}, "ChirpPhaseNoiseSource.step": {"executed_lines": [617, 618, 621], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ChirpSource.__init__": {"executed_lines": [636, 638, 639], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PulseSource.__init__": {"executed_lines": [696, 699, 700, 701, 702, 703, 704, 705, 706, 709, 710, 711, 714, 715, 716, 717, 718, 719, 722, 723, 726, 727, 728, 729, 732, 737, 742, 747, 753, 760, 767, 774, 781], "summary": {"covered_lines": 33, "num_statements": 33, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PulseSource.__init__._set_phase_rising": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [733, 734, 735], "excluded_lines": []}, "PulseSource.__init__._set_phase_high": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [738, 739, 740], "excluded_lines": []}, "PulseSource.__init__._set_phase_falling": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [743, 744, 745], "excluded_lines": []}, "PulseSource.__init__._set_phase_low": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [748, 749, 750], "excluded_lines": []}, "PulseSource.reset": {"executed_lines": [800, 801, 804, 805, 806, 807, 808, 810, 811, 812, 813, 815, 816, 818, 819, 820], "summary": {"covered_lines": 16, "num_statements": 16, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PulseSource.update": {"executed_lines": [833, 834, 835, 836, 837, 838, 839, 840, 841, 842], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PulseSource.__len__": {"executed_lines": [847], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Pulse.__init__": {"executed_lines": [853, 855, 856], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ClockSource.__init__": {"executed_lines": [886, 888, 889, 891, 894, 898], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ClockSource.__init__.clk_up": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [892], "excluded_lines": []}, "ClockSource.__init__.clk_down": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [895], "excluded_lines": []}, "ClockSource.__len__": {"executed_lines": [913], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Clock.__init__": {"executed_lines": [919, 921, 922], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SquareWaveSource.__init__": {"executed_lines": [955, 957, 958, 959, 961, 964, 968], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SquareWaveSource.__init__.sqw_up": {"executed_lines": [962], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SquareWaveSource.__init__.sqw_down": {"executed_lines": [965], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SquareWaveSource.__len__": {"executed_lines": [983], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "StepSource.__init__": {"executed_lines": [1059, 1062, 1063, 1064, 1065, 1067, 1068, 1071, 1072, 1075, 1079, 1083], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "StepSource.__init__.stp_set": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [1076, 1077], "excluded_lines": []}, "StepSource.__len__": {"executed_lines": [1087], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Step.__init__": {"executed_lines": [1093, 1095, 1096], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 16, 21, 22, 31, 32, 35, 37, 42, 47, 65, 66, 134, 135, 138, 140, 149, 154, 173, 174, 187, 188, 191, 193, 201, 205, 223, 228, 229, 242, 243, 246, 248, 256, 260, 265, 266, 279, 280, 283, 285, 293, 297, 316, 320, 321, 353, 354, 357, 359, 391, 395, 402, 410, 432, 443, 450, 460, 461, 508, 509, 512, 514, 548, 552, 570, 578, 587, 598, 604, 614, 624, 626, 645, 646, 681, 682, 685, 687, 784, 822, 845, 850, 852, 859, 860, 879, 880, 883, 885, 911, 916, 918, 926, 927, 948, 949, 952, 954, 981, 986, 987, 1052, 1053, 1056, 1058, 1085, 1090, 1092], "summary": {"covered_lines": 100, "num_statements": 100, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Constant": {"executed_lines": [38, 39, 44, 61, 62], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Source": {"executed_lines": [141, 143, 144, 146, 151, 168], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "TriangleWaveSource": {"executed_lines": [194, 196, 197, 198, 202, 220, 224, 225], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SinusoidalSource": {"executed_lines": [249, 251, 252, 253, 257, 261, 262], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GaussianPulseSource": {"executed_lines": [286, 288, 289, 290, 294, 312, 313, 317], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SinusoidalPhaseNoiseSource": {"executed_lines": [368, 370, 371, 372, 374, 376, 379, 380, 383, 384, 387, 388, 392, 397, 403, 406, 407, 426, 429], "summary": {"covered_lines": 19, "num_statements": 30, "percent_covered": 63.333333333333336, "percent_covered_display": "63", "missing_lines": 11, "excluded_lines": 0}, "missing_lines": [399, 436, 438, 439, 440, 445, 446, 447, 452, 453, 456], "excluded_lines": []}, "ChirpPhaseNoiseSource": {"executed_lines": [525, 528, 529, 530, 531, 532, 535, 536, 537, 540, 541, 544, 545, 549, 567, 579, 581, 591, 593, 594, 595, 600, 601, 617, 618, 621], "summary": {"covered_lines": 26, "num_statements": 33, "percent_covered": 78.78787878787878, "percent_covered_display": "79", "missing_lines": 7, "excluded_lines": 0}, "missing_lines": [571, 574, 575, 584, 607, 608, 611], "excluded_lines": []}, "ChirpSource": {"executed_lines": [636, 638, 639], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PulseSource": {"executed_lines": [696, 699, 700, 701, 702, 703, 704, 705, 706, 709, 710, 711, 714, 715, 716, 717, 718, 719, 722, 723, 726, 727, 728, 729, 732, 737, 742, 747, 753, 760, 767, 774, 781, 800, 801, 804, 805, 806, 807, 808, 810, 811, 812, 813, 815, 816, 818, 819, 820, 833, 834, 835, 836, 837, 838, 839, 840, 841, 842, 847], "summary": {"covered_lines": 60, "num_statements": 72, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 12, "excluded_lines": 0}, "missing_lines": [733, 734, 735, 738, 739, 740, 743, 744, 745, 748, 749, 750], "excluded_lines": []}, "Pulse": {"executed_lines": [853, 855, 856], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ClockSource": {"executed_lines": [886, 888, 889, 891, 894, 898, 913], "summary": {"covered_lines": 7, "num_statements": 9, "percent_covered": 77.77777777777777, "percent_covered_display": "78", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [892, 895], "excluded_lines": []}, "Clock": {"executed_lines": [919, 921, 922], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SquareWaveSource": {"executed_lines": [955, 957, 958, 959, 961, 962, 964, 965, 968, 983], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "StepSource": {"executed_lines": [1059, 1062, 1063, 1064, 1065, 1067, 1068, 1071, 1072, 1075, 1079, 1083, 1087], "summary": {"covered_lines": 13, "num_statements": 15, "percent_covered": 86.66666666666667, "percent_covered_display": "87", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [1076, 1077], "excluded_lines": []}, "Step": {"executed_lines": [1093, 1095, 1096], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 16, 21, 22, 31, 32, 35, 37, 42, 47, 65, 66, 134, 135, 138, 140, 149, 154, 173, 174, 187, 188, 191, 193, 201, 205, 223, 228, 229, 242, 243, 246, 248, 256, 260, 265, 266, 279, 280, 283, 285, 293, 297, 316, 320, 321, 353, 354, 357, 359, 391, 395, 402, 410, 432, 443, 450, 460, 461, 508, 509, 512, 514, 548, 552, 570, 578, 587, 598, 604, 614, 624, 626, 645, 646, 681, 682, 685, 687, 784, 822, 845, 850, 852, 859, 860, 879, 880, 883, 885, 911, 916, 918, 926, 927, 948, 949, 952, 954, 981, 986, 987, 1052, 1053, 1056, 1058, 1085, 1090, 1092], "summary": {"covered_lines": 100, "num_statements": 100, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\spectrum.py": {"executed_lines": [11, 13, 15, 16, 18, 20, 22, 27, 28, 105, 106, 108, 109, 112, 115, 118, 121, 124, 127, 128, 131, 132, 135, 139, 143, 153, 154, 157, 164, 195, 196, 199, 200, 220, 248, 267, 270, 273, 274, 280, 288, 292, 369, 411, 425, 428, 429, 451, 465], "summary": {"covered_lines": 47, "num_statements": 119, "percent_covered": 39.49579831932773, "percent_covered_display": "39", "missing_lines": 72, "excluded_lines": 0}, "missing_lines": [140, 158, 161, 203, 206, 207, 210, 213, 214, 217, 235, 238, 241, 242, 245, 277, 314, 315, 318, 321, 324, 327, 328, 329, 332, 335, 336, 337, 340, 341, 344, 345, 347, 348, 350, 351, 352, 353, 354, 355, 357, 360, 363, 366, 379, 380, 383, 386, 389, 392, 393, 394, 397, 398, 401, 404, 405, 406, 407, 408, 452, 455, 462, 486, 487, 490, 492, 494, 495, 498, 499, 502], "excluded_lines": [], "functions": {"Spectrum.__init__": {"executed_lines": [109, 112, 115, 118, 121, 124, 127, 128], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Spectrum.__len__": {"executed_lines": [132], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Spectrum._kernel": {"executed_lines": [139], "summary": {"covered_lines": 1, "num_statements": 2, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [140], "excluded_lines": []}, "Spectrum.set_solver": {"executed_lines": [153, 154], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Spectrum.reset": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [158, 161], "excluded_lines": []}, "Spectrum.read": {"executed_lines": [195, 196, 199, 200], "summary": {"covered_lines": 4, "num_statements": 11, "percent_covered": 36.36363636363637, "percent_covered_display": "36", "missing_lines": 7, "excluded_lines": 0}, "missing_lines": [203, 206, 207, 210, 213, 214, 217], "excluded_lines": []}, "Spectrum.solve": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [235, 238, 241, 242, 245], "excluded_lines": []}, "Spectrum.step": {"executed_lines": [267, 270, 273, 274], "summary": {"covered_lines": 4, "num_statements": 5, "percent_covered": 80.0, "percent_covered_display": "80", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [277], "excluded_lines": []}, "Spectrum.sample": {"executed_lines": [288], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Spectrum.plot": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 22, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 22, "excluded_lines": 0}, "missing_lines": [314, 315, 318, 321, 324, 327, 328, 329, 332, 335, 336, 337, 340, 341, 344, 345, 347, 348, 350, 360, 363, 366], "excluded_lines": []}, "Spectrum.plot.on_pick": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [351, 352, 353, 354, 355, 357], "excluded_lines": []}, "Spectrum.save": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 16, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 16, "excluded_lines": 0}, "missing_lines": [379, 380, 383, 386, 389, 392, 393, 394, 397, 398, 401, 404, 405, 406, 407, 408], "excluded_lines": []}, "Spectrum.update": {"executed_lines": [425], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RealtimeSpectrum.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [452, 455, 462], "excluded_lines": []}, "RealtimeSpectrum.step": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 9, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 9, "excluded_lines": 0}, "missing_lines": [486, 487, 490, 492, 494, 495, 498, 499, 502], "excluded_lines": []}, "": {"executed_lines": [11, 13, 15, 16, 18, 20, 22, 27, 28, 105, 106, 108, 131, 135, 143, 157, 164, 220, 248, 280, 292, 369, 411, 428, 429, 451, 465], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Spectrum": {"executed_lines": [109, 112, 115, 118, 121, 124, 127, 128, 132, 139, 153, 154, 195, 196, 199, 200, 267, 270, 273, 274, 288, 425], "summary": {"covered_lines": 22, "num_statements": 82, "percent_covered": 26.829268292682926, "percent_covered_display": "27", "missing_lines": 60, "excluded_lines": 0}, "missing_lines": [140, 158, 161, 203, 206, 207, 210, 213, 214, 217, 235, 238, 241, 242, 245, 277, 314, 315, 318, 321, 324, 327, 328, 329, 332, 335, 336, 337, 340, 341, 344, 345, 347, 348, 350, 351, 352, 353, 354, 355, 357, 360, 363, 366, 379, 380, 383, 386, 389, 392, 393, 394, 397, 398, 401, 404, 405, 406, 407, 408], "excluded_lines": []}, "RealtimeSpectrum": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 12, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 12, "excluded_lines": 0}, "missing_lines": [452, 455, 462, 486, 487, 490, 492, 494, 495, 498, 499, 502], "excluded_lines": []}, "": {"executed_lines": [11, 13, 15, 16, 18, 20, 22, 27, 28, 105, 106, 108, 131, 135, 143, 157, 164, 220, 248, 280, 292, 369, 411, 428, 429, 451, 465], "summary": {"covered_lines": 25, "num_statements": 25, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\switch.py": {"executed_lines": [12, 17, 18, 58, 59, 62, 64, 65, 67, 70, 72, 75, 89, 92, 102, 103], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"Switch.__init__": {"executed_lines": [65, 67], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Switch.__len__": {"executed_lines": [72], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Switch.select": {"executed_lines": [89], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Switch.update": {"executed_lines": [102, 103], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 58, 59, 62, 64, 70, 75, 92], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Switch": {"executed_lines": [65, 67, 72, 89, 102, 103], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 58, 59, 62, 64, 70, 75, 92], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\table.py": {"executed_lines": [10, 11, 12, 17, 18, 57, 58, 59, 61, 63, 66, 67, 111, 112, 113, 116, 118, 119, 120, 123, 124, 125, 127], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"LUT.__init__": {"executed_lines": [58, 59, 61, 63], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "LUT1D.__init__": {"executed_lines": [112, 113, 116, 118, 119, 123, 124, 125, 127], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "LUT1D.__init__.func": {"executed_lines": [120], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 11, 12, 17, 18, 57, 66, 67, 111], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"LUT": {"executed_lines": [58, 59, 61, 63], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "LUT1D": {"executed_lines": [112, 113, 116, 118, 119, 120, 123, 124, 125, 127], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 11, 12, 17, 18, 57, 66, 67, 111], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\tritium\\__init__.py": {"executed_lines": [1, 2, 3], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 2, 3], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [1, 2, 3], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\tritium\\bubbler.py": {"executed_lines": [10, 12, 13, 18, 19, 96, 103, 108, 116, 117, 118, 121, 124, 125, 128, 131, 132, 133, 134, 136, 139, 142, 143, 146, 148, 150, 153, 156, 159, 164, 166, 168, 170, 172, 178, 191, 192, 195, 196, 198, 202, 205, 207, 208, 210, 211, 216], "summary": {"covered_lines": 46, "num_statements": 47, "percent_covered": 97.87234042553192, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [199], "excluded_lines": [], "functions": {"Bubbler4.__init__": {"executed_lines": [116, 117, 118, 121, 139, 153, 156], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Bubbler4.__init__._fn_d": {"executed_lines": [124, 125, 128, 131, 132, 133, 134, 136], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Bubbler4.__init__._fn_a": {"executed_lines": [142, 143, 146, 148, 150], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Bubbler4._create_reset_event_vial": {"executed_lines": [164, 172], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Bubbler4._create_reset_event_vial.reset_vial_i": {"executed_lines": [166, 168, 170], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Bubbler4._create_reset_events": {"executed_lines": [191, 192, 195, 196, 198, 202, 205, 207, 208, 210, 211, 216], "summary": {"covered_lines": 12, "num_statements": 13, "percent_covered": 92.3076923076923, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [199], "excluded_lines": []}, "": {"executed_lines": [10, 12, 13, 18, 19, 96, 103, 108, 159, 178], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Bubbler4": {"executed_lines": [116, 117, 118, 121, 124, 125, 128, 131, 132, 133, 134, 136, 139, 142, 143, 146, 148, 150, 153, 156, 164, 166, 168, 170, 172, 191, 192, 195, 196, 198, 202, 205, 207, 208, 210, 211, 216], "summary": {"covered_lines": 37, "num_statements": 38, "percent_covered": 97.36842105263158, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [199], "excluded_lines": []}, "": {"executed_lines": [10, 12, 13, 18, 19, 96, 103, 108, 159, 178], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\tritium\\residencetime.py": {"executed_lines": [10, 12, 17, 18, 52, 55, 56, 59, 60, 61, 62, 65, 69, 73, 74, 77, 80, 81, 117, 120, 122, 123], "summary": {"covered_lines": 20, "num_statements": 22, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [66, 70], "excluded_lines": [], "functions": {"ResidenceTime.__init__": {"executed_lines": [55, 56, 59, 60, 61, 62, 65, 69, 73, 77], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ResidenceTime.__init__._fn_d": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [66], "excluded_lines": []}, "ResidenceTime.__init__._jc_d": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [70], "excluded_lines": []}, "ResidenceTime.__init__._fn_a": {"executed_lines": [74], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Process.__init__": {"executed_lines": [123], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 17, 18, 52, 80, 81, 117, 120, 122], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ResidenceTime": {"executed_lines": [55, 56, 59, 60, 61, 62, 65, 69, 73, 74, 77], "summary": {"covered_lines": 11, "num_statements": 13, "percent_covered": 84.61538461538461, "percent_covered_display": "85", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [66, 70], "excluded_lines": []}, "Process": {"executed_lines": [123], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 17, 18, 52, 80, 81, 117, 120, 122], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\tritium\\splitter.py": {"executed_lines": [10, 12, 17, 18, 33, 36, 38, 40, 43, 44, 47, 50], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"Splitter.__init__": {"executed_lines": [40, 43, 44, 47, 50], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 17, 18, 33, 36, 38], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Splitter": {"executed_lines": [40, 43, 44, 47, 50], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 17, 18, 33, 36, 38], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\blocks\\tritium\\tcap.py": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [10, 15, 25], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [10, 15, 25], "excluded_lines": []}}, "classes": {"TCAP1D": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [10, 15, 25], "excluded_lines": []}}}, "src\\pathsim\\blocks\\wrapper.py": {"executed_lines": [10, 11, 16, 17, 74, 75, 76, 77, 80, 81, 83, 86, 89, 92, 95, 100, 103, 120, 121, 129, 132, 133, 141, 142, 143, 144, 147, 148, 156, 159, 160, 167, 168, 169, 170, 173, 174, 198, 199, 200], "summary": {"covered_lines": 39, "num_statements": 40, "percent_covered": 97.5, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [117], "excluded_lines": [], "functions": {"Wrapper.__init__": {"executed_lines": [75, 76, 77, 80, 81, 83, 95, 100], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Wrapper.__init__._sample": {"executed_lines": [86, 89, 92], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Wrapper.update": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [117], "excluded_lines": []}, "Wrapper.tau": {"executed_lines": [141, 142, 143, 144], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Wrapper.T": {"executed_lines": [167, 168, 169, 170], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Wrapper.dec": {"executed_lines": [198, 200], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Wrapper.dec.decorator": {"executed_lines": [199], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 11, 16, 17, 74, 103, 120, 121, 132, 133, 147, 148, 159, 160, 173, 174], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Wrapper": {"executed_lines": [75, 76, 77, 80, 81, 83, 86, 89, 92, 95, 100, 129, 141, 142, 143, 144, 156, 167, 168, 169, 170, 198, 199, 200], "summary": {"covered_lines": 24, "num_statements": 25, "percent_covered": 96.0, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [117], "excluded_lines": []}, "": {"executed_lines": [10, 11, 16, 17, 74, 103, 120, 121, 132, 133, 147, 148, 159, 160, 173, 174], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\connection.py": {"executed_lines": [15, 17, 19, 24, 25, 149, 152, 155, 158, 161, 164, 167, 170, 174, 177, 182, 183, 186, 204, 208, 209, 210, 211, 214, 221, 222, 223, 226, 235, 236, 237, 238, 239, 242, 243, 246, 247, 250, 267, 268, 271, 272, 275, 278, 279, 280, 282, 285, 287, 294, 298, 299, 302, 303, 308, 311, 313, 314, 317, 320, 322, 323, 326, 335, 341, 342], "summary": {"covered_lines": 64, "num_statements": 69, "percent_covered": 92.7536231884058, "percent_covered_display": "93", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [179, 199, 200, 201, 328], "excluded_lines": [], "functions": {"Connection.__init__": {"executed_lines": [155, 158, 161, 164, 167], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Connection.__str__": {"executed_lines": [174], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Connection.__len__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [179], "excluded_lines": []}, "Connection.__bool__": {"executed_lines": [183], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Connection.__contains__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [199, 200, 201], "excluded_lines": []}, "Connection._validate_dimensions": {"executed_lines": [208, 209, 210, 211], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Connection._validate_ports": {"executed_lines": [221, 222, 223], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Connection.get_blocks": {"executed_lines": [235, 236, 237, 238, 239], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Connection.on": {"executed_lines": [243], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Connection.off": {"executed_lines": [247], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Connection.overwrites": {"executed_lines": [267, 268, 271, 272, 275, 278, 279, 280, 282], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Connection.to_dict": {"executed_lines": [287], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Connection.update": {"executed_lines": [298, 299], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Duplex.__init__": {"executed_lines": [313, 314, 317, 320, 322, 323], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Duplex.to_dict": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [328], "excluded_lines": []}, "Duplex.update": {"executed_lines": [341, 342], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [15, 17, 19, 24, 25, 149, 152, 170, 177, 182, 186, 204, 214, 226, 242, 246, 250, 285, 294, 302, 303, 308, 311, 326, 335], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Connection": {"executed_lines": [155, 158, 161, 164, 167, 174, 183, 208, 209, 210, 211, 221, 222, 223, 235, 236, 237, 238, 239, 243, 247, 267, 268, 271, 272, 275, 278, 279, 280, 282, 287, 298, 299], "summary": {"covered_lines": 33, "num_statements": 37, "percent_covered": 89.1891891891892, "percent_covered_display": "89", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [179, 199, 200, 201], "excluded_lines": []}, "Duplex": {"executed_lines": [313, 314, 317, 320, 322, 323, 341, 342], "summary": {"covered_lines": 8, "num_statements": 9, "percent_covered": 88.88888888888889, "percent_covered_display": "89", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [328], "excluded_lines": []}, "": {"executed_lines": [15, 17, 19, 24, 25, 149, 152, 170, 177, 182, 186, 204, 214, 226, 242, 246, 250, 285, 294, 302, 303, 308, 311, 326, 335], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\events\\__init__.py": {"executed_lines": [1, 2, 3], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 2, 3], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [1, 2, 3], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\events\\_event.py": {"executed_lines": [12, 13, 15, 17, 19, 24, 25, 65, 73, 76, 79, 82, 85, 88, 91, 101, 104, 108, 109, 112, 113, 118, 119, 122, 127, 128, 129, 132, 141, 142, 145, 161, 164, 188, 191, 205, 208, 209], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"Event.__init__": {"executed_lines": [73, 76, 79, 82, 85, 88], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Event.__len__": {"executed_lines": [101], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Event.__iter__": {"executed_lines": [108, 109], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Event.__bool__": {"executed_lines": [113], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Event.on": {"executed_lines": [118], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Event.off": {"executed_lines": [119], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Event.reset": {"executed_lines": [127, 128, 129], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Event.buffer": {"executed_lines": [141, 142], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Event.estimate": {"executed_lines": [161], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Event.detect": {"executed_lines": [188], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Event.resolve": {"executed_lines": [205, 208, 209], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 13, 15, 17, 19, 24, 25, 65, 91, 104, 112, 122, 132, 145, 164, 191], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Event": {"executed_lines": [73, 76, 79, 82, 85, 88, 101, 108, 109, 113, 118, 119, 127, 128, 129, 141, 142, 161, 188, 205, 208, 209], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 13, 15, 17, 19, 24, 25, 65, 91, 104, 112, 122, 132, 145, 164, 191], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\events\\condition.py": {"executed_lines": [12, 14, 19, 20, 65, 103], "summary": {"covered_lines": 5, "num_statements": 14, "percent_covered": 35.714285714285715, "percent_covered_display": "36", "missing_lines": 9, "excluded_lines": 0}, "missing_lines": [88, 91, 94, 97, 100, 116, 119, 120, 123], "excluded_lines": [], "functions": {"Condition.detect": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [88, 91, 94, 97, 100], "excluded_lines": []}, "Condition.resolve": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [116, 119, 120, 123], "excluded_lines": []}, "": {"executed_lines": [12, 14, 19, 20, 65, 103], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Condition": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 9, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 9, "excluded_lines": 0}, "missing_lines": [88, 91, 94, 97, 100, 116, 119, 120, 123], "excluded_lines": []}, "": {"executed_lines": [12, 14, 19, 20, 65, 103], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\events\\schedule.py": {"executed_lines": [10, 12, 14, 19, 20, 62, 70, 73, 74, 75, 78, 82, 85, 98, 101, 109, 112, 132, 135, 140, 141, 144, 145, 148, 151, 155, 157, 160, 161, 198, 204, 207, 208, 211, 214, 216, 217, 218, 222, 242, 243, 248, 251, 252, 255, 259, 262, 266, 268], "summary": {"covered_lines": 47, "num_statements": 55, "percent_covered": 85.45454545454545, "percent_covered_display": "85", "missing_lines": 8, "excluded_lines": 0}, "missing_lines": [136, 137, 152, 219, 244, 245, 256, 263], "excluded_lines": [], "functions": {"Schedule.__init__": {"executed_lines": [70, 73, 74, 75], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Schedule._next": {"executed_lines": [82], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Schedule.estimate": {"executed_lines": [98], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Schedule.buffer": {"executed_lines": [109], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Schedule.detect": {"executed_lines": [132, 135, 140, 141, 144, 145, 148, 151, 155, 157], "summary": {"covered_lines": 10, "num_statements": 13, "percent_covered": 76.92307692307692, "percent_covered_display": "77", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [136, 137, 152], "excluded_lines": []}, "ScheduleList.__init__": {"executed_lines": [204, 207, 208, 211], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ScheduleList._next": {"executed_lines": [216, 217, 218], "summary": {"covered_lines": 3, "num_statements": 4, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [219], "excluded_lines": []}, "ScheduleList.detect": {"executed_lines": [242, 243, 248, 251, 252, 255, 259, 262, 266, 268], "summary": {"covered_lines": 10, "num_statements": 14, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [244, 245, 256, 263], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 19, 20, 62, 78, 85, 101, 112, 160, 161, 198, 214, 222], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Schedule": {"executed_lines": [70, 73, 74, 75, 82, 98, 109, 132, 135, 140, 141, 144, 145, 148, 151, 155, 157], "summary": {"covered_lines": 17, "num_statements": 20, "percent_covered": 85.0, "percent_covered_display": "85", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [136, 137, 152], "excluded_lines": []}, "ScheduleList": {"executed_lines": [204, 207, 208, 211, 216, 217, 218, 242, 243, 248, 251, 252, 255, 259, 262, 266, 268], "summary": {"covered_lines": 17, "num_statements": 22, "percent_covered": 77.27272727272727, "percent_covered_display": "77", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [219, 244, 245, 256, 263], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 19, 20, 62, 78, 85, 101, 112, 160, 161, 198, 214, 222], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\events\\zerocrossing.py": {"executed_lines": [12, 14, 16, 21, 22, 70, 89, 92, 95, 98, 99, 102, 105, 106, 109, 111, 114, 115, 120, 139, 142, 145, 148, 152, 155, 156, 159, 161, 164, 165, 170, 189, 192, 195, 198, 202, 205, 206], "summary": {"covered_lines": 35, "num_statements": 39, "percent_covered": 89.74358974358974, "percent_covered_display": "90", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [149, 199, 209, 211], "excluded_lines": [], "functions": {"ZeroCrossing.detect": {"executed_lines": [89, 92, 95, 98, 99, 102, 105, 106, 109, 111], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ZeroCrossingUp.detect": {"executed_lines": [139, 142, 145, 148, 152, 155, 156, 159, 161], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [149], "excluded_lines": []}, "ZeroCrossingDown.detect": {"executed_lines": [189, 192, 195, 198, 202, 205, 206], "summary": {"covered_lines": 7, "num_statements": 10, "percent_covered": 70.0, "percent_covered_display": "70", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [199, 209, 211], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 70, 114, 115, 120, 164, 165, 170], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ZeroCrossing": {"executed_lines": [89, 92, 95, 98, 99, 102, 105, 106, 109, 111], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ZeroCrossingUp": {"executed_lines": [139, 142, 145, 148, 152, 155, 156, 159, 161], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [149], "excluded_lines": []}, "ZeroCrossingDown": {"executed_lines": [189, 192, 195, 198, 202, 205, 206], "summary": {"covered_lines": 7, "num_statements": 10, "percent_covered": 70.0, "percent_covered_display": "70", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [199, 209, 211], "excluded_lines": []}, "": {"executed_lines": [12, 14, 16, 21, 22, 70, 114, 115, 120, 164, 165, 170], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\optim\\__init__.py": {"executed_lines": [1, 2], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [1, 2], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [1, 2], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\optim\\anderson.py": {"executed_lines": [10, 12, 14, 16, 25, 26, 44, 47, 50, 53, 54, 57, 58, 61, 65, 69, 99, 100, 101, 102, 103, 108, 112, 113, 116, 117, 120, 139, 140, 143, 146, 150, 153, 154, 156, 159, 160, 163, 164, 167, 168, 169, 172, 173, 176, 179, 182, 183, 186, 189, 192, 196, 197, 206, 238, 239, 240, 241, 242, 247, 269, 270, 271, 274, 277, 279, 282, 285, 309, 312, 315, 318, 320], "summary": {"covered_lines": 71, "num_statements": 76, "percent_covered": 93.42105263157895, "percent_covered_display": "93", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [62, 66, 105, 147, 244], "excluded_lines": [], "functions": {"Anderson.__init__": {"executed_lines": [47, 50, 53, 54, 57, 58], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Anderson.__bool__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [62], "excluded_lines": []}, "Anderson.__len__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [66], "excluded_lines": []}, "Anderson.solve": {"executed_lines": [99, 100, 101, 102, 103], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [105], "excluded_lines": []}, "Anderson.reset": {"executed_lines": [112, 113, 116, 117], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Anderson.step": {"executed_lines": [139, 140, 143, 146, 150, 153, 154, 156, 159, 160, 163, 164, 167, 168, 169, 172, 173, 176, 179, 182, 183, 186, 189, 192], "summary": {"covered_lines": 24, "num_statements": 25, "percent_covered": 96.0, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [147], "excluded_lines": []}, "NewtonAnderson.solve": {"executed_lines": [238, 239, 240, 241, 242], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [244], "excluded_lines": []}, "NewtonAnderson._newton": {"executed_lines": [269, 270, 271, 274, 277, 279, 282], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "NewtonAnderson.step": {"executed_lines": [309, 312, 315, 318, 320], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 16, 25, 26, 44, 61, 65, 69, 108, 120, 196, 197, 206, 247, 285], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Anderson": {"executed_lines": [47, 50, 53, 54, 57, 58, 99, 100, 101, 102, 103, 112, 113, 116, 117, 139, 140, 143, 146, 150, 153, 154, 156, 159, 160, 163, 164, 167, 168, 169, 172, 173, 176, 179, 182, 183, 186, 189, 192], "summary": {"covered_lines": 39, "num_statements": 43, "percent_covered": 90.69767441860465, "percent_covered_display": "91", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [62, 66, 105, 147], "excluded_lines": []}, "NewtonAnderson": {"executed_lines": [238, 239, 240, 241, 242, 269, 270, 271, 274, 277, 279, 282, 309, 312, 315, 318, 320], "summary": {"covered_lines": 17, "num_statements": 18, "percent_covered": 94.44444444444444, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [244], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 16, 25, 26, 44, 61, 65, 69, 108, 120, 196, 197, 206, 247, 285], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\optim\\booster.py": {"executed_lines": [13, 15, 20, 21, 39, 40, 41, 44, 47, 51, 60, 63, 73, 74, 77, 81, 82, 85, 96, 97, 98, 99], "summary": {"covered_lines": 21, "num_statements": 22, "percent_covered": 95.45454545454545, "percent_covered_display": "95", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [48], "excluded_lines": [], "functions": {"ConnectionBooster.__init__": {"executed_lines": [40, 41, 44], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ConnectionBooster.__bool__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [48], "excluded_lines": []}, "ConnectionBooster.get": {"executed_lines": [60], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ConnectionBooster.set": {"executed_lines": [73, 74], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ConnectionBooster.reset": {"executed_lines": [81, 82], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ConnectionBooster.update": {"executed_lines": [96, 97, 98, 99], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [13, 15, 20, 21, 39, 47, 51, 63, 77, 85], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ConnectionBooster": {"executed_lines": [40, 41, 44, 60, 73, 74, 81, 82, 96, 97, 98, 99], "summary": {"covered_lines": 12, "num_statements": 13, "percent_covered": 92.3076923076923, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [48], "excluded_lines": []}, "": {"executed_lines": [13, 15, 20, 21, 39, 47, 51, 63, 77, 85], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\optim\\numerical.py": {"executed_lines": [12, 14, 19, 44, 47, 48, 51, 57, 75, 76, 77, 78], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"num_jac": {"executed_lines": [44, 47, 48, 51], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "num_autojac": {"executed_lines": [75, 78], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "num_autojac.wrap_func": {"executed_lines": [76, 77], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 19, 57], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [12, 14, 19, 44, 47, 48, 51, 57, 75, 76, 77, 78], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\optim\\operator.py": {"executed_lines": [12, 14, 15, 20, 21, 91, 92, 93, 94, 95, 96, 99, 100, 103, 126, 127, 128, 129, 132, 150, 151, 153, 154, 160, 163, 175, 178, 184, 187, 188, 267, 269, 271, 272, 274, 275, 276, 277, 278, 281, 282, 285, 312, 313, 316, 317, 320, 321, 323, 326, 348, 350, 351, 353, 355, 356, 362, 365, 387, 389, 390, 392, 394, 395, 396, 398, 401, 404, 420, 421, 422, 423, 424, 427, 433, 434, 435], "summary": {"covered_lines": 75, "num_statements": 79, "percent_covered": 94.9367088607595, "percent_covered_display": "95", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [155, 157, 357, 359], "excluded_lines": [], "functions": {"Operator.__init__": {"executed_lines": [92, 93, 94, 95, 96], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Operator.__bool__": {"executed_lines": [100], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Operator.__call__": {"executed_lines": [126, 127, 128, 129], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Operator.jac": {"executed_lines": [150, 151, 153, 154, 160], "summary": {"covered_lines": 5, "num_statements": 7, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [155, 157], "excluded_lines": []}, "Operator.linearize": {"executed_lines": [175], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Operator.reset": {"executed_lines": [184], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicOperator.__init__": {"executed_lines": [269, 271, 272, 274, 275, 276, 277, 278], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicOperator.__bool__": {"executed_lines": [282], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicOperator.__call__": {"executed_lines": [312, 313, 316, 317, 320, 321, 323], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicOperator.jac_x": {"executed_lines": [348, 350, 353, 355, 356, 362], "summary": {"covered_lines": 6, "num_statements": 8, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [357, 359], "excluded_lines": []}, "DynamicOperator.jac_x.func_x": {"executed_lines": [351], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicOperator.jac_u": {"executed_lines": [387, 389, 392, 394, 395, 396, 398, 401], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicOperator.jac_u.func_u": {"executed_lines": [390], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicOperator.linearize": {"executed_lines": [420, 421, 422, 423, 424], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DynamicOperator.reset": {"executed_lines": [433, 434, 435], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 20, 21, 91, 99, 103, 132, 163, 178, 187, 188, 267, 281, 285, 326, 365, 404, 427], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Operator": {"executed_lines": [92, 93, 94, 95, 96, 100, 126, 127, 128, 129, 150, 151, 153, 154, 160, 175, 184], "summary": {"covered_lines": 17, "num_statements": 19, "percent_covered": 89.47368421052632, "percent_covered_display": "89", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [155, 157], "excluded_lines": []}, "DynamicOperator": {"executed_lines": [269, 271, 272, 274, 275, 276, 277, 278, 282, 312, 313, 316, 317, 320, 321, 323, 348, 350, 351, 353, 355, 356, 362, 387, 389, 390, 392, 394, 395, 396, 398, 401, 420, 421, 422, 423, 424, 433, 434, 435], "summary": {"covered_lines": 40, "num_statements": 42, "percent_covered": 95.23809523809524, "percent_covered_display": "95", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [357, 359], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 20, 21, 91, 99, 103, 132, 163, 178, 187, 188, 267, 281, 285, 326, 365, 404, 427], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\optim\\value.py": {"executed_lines": [12, 13, 15, 20, 36, 39, 57, 60, 81, 90, 91, 92, 93, 94, 95, 100, 142, 147, 148, 149, 150, 151, 158, 161, 162, 163, 165, 170, 171, 172, 253, 256, 259, 260, 262, 265, 266, 267, 269, 270, 273, 278, 279, 280, 286, 287, 288, 296, 297, 312, 313, 317, 318, 352, 353, 358, 359, 360, 363, 364, 365, 368, 369, 375, 389, 390, 393, 397, 398, 401, 405, 406, 411, 412, 413, 415, 418, 419, 420, 425, 426, 427, 432, 433, 434, 439, 440, 441, 446, 447, 448, 455, 456, 459, 460, 463, 464, 467, 473, 477, 478, 484, 485, 493, 494, 495, 496, 497, 501, 502, 504, 510, 511, 514, 515, 516, 517, 518, 524, 525, 526, 527, 528, 529, 533, 534, 536, 542, 543, 546, 547, 548, 549, 550, 551, 557, 558, 559, 560, 561, 565, 568, 574, 575, 578, 579, 580, 581, 582, 588, 589, 590, 591, 592, 593, 594, 595, 599, 602, 608, 609, 612, 613, 614, 615, 616, 622, 623, 624, 625, 626, 627, 628, 629, 634, 640, 641, 642], "summary": {"covered_lines": 177, "num_statements": 197, "percent_covered": 89.84771573604061, "percent_covered_display": "90", "missing_lines": 20, "excluded_lines": 0}, "missing_lines": [78, 370, 394, 402, 422, 429, 436, 443, 450, 468, 474, 520, 521, 553, 554, 566, 584, 585, 600, 610], "excluded_lines": [], "functions": {"der": {"executed_lines": [36], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "jac": {"executed_lines": [57], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "var": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [78], "excluded_lines": []}, "autojac": {"executed_lines": [90, 91, 95], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "autojac.wrap": {"executed_lines": [92, 93, 94], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "add_funcs": {"executed_lines": [147, 148, 161, 162, 163, 165], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "add_funcs.create_method": {"executed_lines": [149, 158], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "add_funcs.create_method.method": {"executed_lines": [150, 151], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__init__": {"executed_lines": [259, 260, 262, 265, 266, 267, 269, 270, 273], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.real": {"executed_lines": [280], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.imag": {"executed_lines": [288], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.numeric": {"executed_lines": [312, 313], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.array": {"executed_lines": [352, 353], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.der": {"executed_lines": [360], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.jac": {"executed_lines": [365], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.var": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [370], "excluded_lines": []}, "Value.__call__": {"executed_lines": [389, 390], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__hash__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [394], "excluded_lines": []}, "Value.__iter__": {"executed_lines": [398], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__len__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [402], "excluded_lines": []}, "Value.__repr__": {"executed_lines": [406], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__eq__": {"executed_lines": [412, 413, 415], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__ne__": {"executed_lines": [419, 420], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [422], "excluded_lines": []}, "Value.__lt__": {"executed_lines": [426, 427], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [429], "excluded_lines": []}, "Value.__gt__": {"executed_lines": [433, 434], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [436], "excluded_lines": []}, "Value.__le__": {"executed_lines": [440, 441], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [443], "excluded_lines": []}, "Value.__ge__": {"executed_lines": [447, 448], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [450], "excluded_lines": []}, "Value.__bool__": {"executed_lines": [456], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__int__": {"executed_lines": [460], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__float__": {"executed_lines": [464], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__complex__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [468], "excluded_lines": []}, "Value.__pos__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [474], "excluded_lines": []}, "Value.__neg__": {"executed_lines": [478], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__abs__": {"executed_lines": [485], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__add__": {"executed_lines": [494, 495, 496, 497, 501, 502, 504], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__radd__": {"executed_lines": [511], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__iadd__": {"executed_lines": [515, 516, 517, 518], "summary": {"covered_lines": 4, "num_statements": 6, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [520, 521], "excluded_lines": []}, "Value.__mul__": {"executed_lines": [525, 526, 527, 528, 529, 533, 534, 536], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__rmul__": {"executed_lines": [543], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__imul__": {"executed_lines": [547, 548, 549, 550, 551], "summary": {"covered_lines": 5, "num_statements": 7, "percent_covered": 71.42857142857143, "percent_covered_display": "71", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [553, 554], "excluded_lines": []}, "Value.__sub__": {"executed_lines": [558, 559, 560, 561, 565, 568], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [566], "excluded_lines": []}, "Value.__rsub__": {"executed_lines": [575], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__isub__": {"executed_lines": [579, 580, 581, 582], "summary": {"covered_lines": 4, "num_statements": 6, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [584, 585], "excluded_lines": []}, "Value.__truediv__": {"executed_lines": [589, 590, 591, 592, 593, 594, 595, 599, 602], "summary": {"covered_lines": 9, "num_statements": 10, "percent_covered": 90.0, "percent_covered_display": "90", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [600], "excluded_lines": []}, "Value.__rtruediv__": {"executed_lines": [609, 612, 613, 614, 615, 616], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [610], "excluded_lines": []}, "Value.__pow__": {"executed_lines": [623, 624, 625, 626, 627, 628, 629, 634], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Value.__rpow__": {"executed_lines": [641, 642], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 13, 15, 20, 39, 60, 81, 100, 142, 170, 171, 172, 253, 256, 278, 279, 286, 287, 296, 297, 317, 318, 358, 359, 363, 364, 368, 369, 375, 393, 397, 401, 405, 411, 418, 425, 432, 439, 446, 455, 459, 463, 467, 473, 477, 484, 493, 510, 514, 524, 542, 546, 557, 574, 578, 588, 608, 622, 640], "summary": {"covered_lines": 58, "num_statements": 58, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Value": {"executed_lines": [259, 260, 262, 265, 266, 267, 269, 270, 273, 280, 288, 312, 313, 352, 353, 360, 365, 389, 390, 398, 406, 412, 413, 415, 419, 420, 426, 427, 433, 434, 440, 441, 447, 448, 456, 460, 464, 478, 485, 494, 495, 496, 497, 501, 502, 504, 511, 515, 516, 517, 518, 525, 526, 527, 528, 529, 533, 534, 536, 543, 547, 548, 549, 550, 551, 558, 559, 560, 561, 565, 568, 575, 579, 580, 581, 582, 589, 590, 591, 592, 593, 594, 595, 599, 602, 609, 612, 613, 614, 615, 616, 623, 624, 625, 626, 627, 628, 629, 634, 641, 642], "summary": {"covered_lines": 101, "num_statements": 120, "percent_covered": 84.16666666666667, "percent_covered_display": "84", "missing_lines": 19, "excluded_lines": 0}, "missing_lines": [370, 394, 402, 422, 429, 436, 443, 450, 468, 474, 520, 521, 553, 554, 566, 584, 585, 600, 610], "excluded_lines": []}, "": {"executed_lines": [12, 13, 15, 20, 36, 39, 57, 60, 81, 90, 91, 92, 93, 94, 95, 100, 142, 147, 148, 149, 150, 151, 158, 161, 162, 163, 165, 170, 171, 172, 253, 256, 278, 279, 286, 287, 296, 297, 317, 318, 358, 359, 363, 364, 368, 369, 375, 393, 397, 401, 405, 411, 418, 425, 432, 439, 446, 455, 459, 463, 467, 473, 477, 484, 493, 510, 514, 524, 542, 546, 557, 574, 578, 588, 608, 622, 640], "summary": {"covered_lines": 76, "num_statements": 77, "percent_covered": 98.7012987012987, "percent_covered_display": "99", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [78], "excluded_lines": []}}}, "src\\pathsim\\simulation.py": {"executed_lines": [13, 15, 16, 17, 19, 21, 23, 32, 34, 35, 36, 37, 39, 41, 43, 45, 50, 51, 162, 178, 179, 180, 183, 184, 185, 188, 191, 194, 197, 200, 203, 206, 209, 212, 215, 218, 221, 224, 225, 226, 229, 230, 231, 234, 235, 236, 239, 242, 245, 252, 265, 272, 280, 285, 286, 296, 297, 298, 299, 300, 301, 306, 312, 315, 318, 337, 338, 341, 342, 343, 346, 347, 352, 376, 398, 399, 415, 416, 420, 461, 462, 477, 480, 483, 484, 485, 486, 487, 490, 491, 494, 495, 496, 499, 500, 501, 502, 503, 508, 513, 514, 518, 521, 522, 525, 528, 538, 551, 552, 553, 556, 559, 560, 563, 566, 573, 574, 577, 578, 581, 594, 595, 596, 599, 600, 601, 602, 605, 608, 609, 612, 624, 629, 634, 640, 641, 644, 645, 649, 658, 666, 667, 668, 671, 672, 680, 696, 697, 700, 703, 706, 707, 708, 711, 712, 715, 727, 746, 749, 752, 755, 758, 759, 762, 763, 766, 771, 794, 804, 818, 819, 822, 825, 828, 829, 832, 835, 849, 850, 853, 869, 870, 873, 876, 879, 880, 883, 888, 920, 923, 924, 927, 937, 940, 941, 944, 945, 948, 959, 960, 963, 966, 969, 970, 973, 974, 977, 978, 979, 980, 981, 984, 985, 996, 1025, 1028, 1031, 1032, 1035, 1036, 1039, 1043, 1044, 1045, 1048, 1049, 1052, 1055, 1129, 1136, 1139, 1140, 1143, 1153, 1154, 1157, 1172, 1175, 1176, 1179, 1212, 1215, 1218, 1221, 1224, 1225, 1228, 1229, 1232, 1233, 1236, 1237, 1240, 1245, 1272, 1275, 1276, 1279, 1282, 1285, 1288, 1291, 1292, 1295, 1298, 1301, 1302, 1305, 1308, 1311, 1312, 1315, 1318, 1321, 1324, 1351, 1354, 1358, 1361, 1364, 1367, 1370, 1373, 1379, 1380, 1383, 1386, 1389, 1390, 1393, 1403, 1406, 1409, 1412, 1446, 1449, 1453, 1456, 1459, 1462, 1465, 1466, 1469, 1472, 1473, 1474, 1475, 1476, 1479, 1482, 1483, 1486, 1489, 1490, 1493, 1494, 1498, 1499, 1500, 1501, 1504, 1507, 1510, 1513, 1551, 1554, 1558, 1561, 1564, 1567, 1570, 1573, 1574, 1577, 1578, 1579, 1580, 1583, 1586, 1587, 1588, 1589, 1592, 1595, 1596, 1599, 1602, 1603, 1606, 1607, 1611, 1612, 1613, 1614, 1617, 1620, 1623, 1626, 1652, 1653, 1654, 1656, 1658, 1659, 1661, 1664, 1666, 1669, 1674, 1679, 1682, 1710, 1713, 1714, 1717, 1720, 1723, 1726, 1727, 1730, 1733, 1736, 1737, 1740, 1743, 1751, 1754, 1757, 1762, 1768, 1771, 1775, 1778, 1779, 1783, 1784, 1787, 1790, 1793, 1795], "summary": {"covered_lines": 399, "num_statements": 455, "percent_covered": 87.6923076923077, "percent_covered_display": "88", "missing_lines": 56, "excluded_lines": 0}, "missing_lines": [249, 321, 322, 324, 327, 331, 332, 334, 370, 371, 389, 392, 394, 395, 417, 436, 437, 438, 441, 515, 625, 626, 673, 784, 787, 788, 789, 791, 796, 797, 799, 988, 1040, 1090, 1091, 1094, 1097, 1100, 1103, 1104, 1107, 1108, 1115, 1118, 1124, 1355, 1374, 1396, 1399, 1400, 1450, 1555, 1758, 1759, 1772, 1780], "excluded_lines": [], "functions": {"Simulation.__init__": {"executed_lines": [178, 179, 180, 183, 184, 185, 188, 191, 194, 197, 200, 203, 206, 209, 212, 215, 218, 221, 224, 225, 226, 229, 230, 231, 234, 235, 236, 239, 242], "summary": {"covered_lines": 29, "num_statements": 29, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.__str__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [249], "excluded_lines": []}, "Simulation.__contains__": {"executed_lines": [265], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.__bool__": {"executed_lines": [280], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.size": {"executed_lines": [296, 297, 298, 299, 300, 301], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._initialize_logger": {"executed_lines": [312, 315, 318], "summary": {"covered_lines": 3, "num_statements": 10, "percent_covered": 30.0, "percent_covered_display": "30", "missing_lines": 7, "excluded_lines": 0}, "missing_lines": [321, 322, 324, 327, 331, 332, 334], "excluded_lines": []}, "Simulation._logger_info": {"executed_lines": [338], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._logger_error": {"executed_lines": [342, 343], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._logger_warning": {"executed_lines": [347], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.plot": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [370, 371], "excluded_lines": []}, "Simulation.save": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [389, 392, 394, 395], "excluded_lines": []}, "Simulation.load": {"executed_lines": [415, 416], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [417], "excluded_lines": []}, "Simulation.to_dict": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [436, 437, 438, 441], "excluded_lines": []}, "Simulation.from_dict": {"executed_lines": [477, 480, 483, 484, 485, 486, 487, 490, 491, 494, 495, 496, 499, 500, 501, 502, 503, 508, 513, 514, 518, 521, 522, 525, 528], "summary": {"covered_lines": 25, "num_statements": 26, "percent_covered": 96.15384615384616, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [515], "excluded_lines": []}, "Simulation.add_block": {"executed_lines": [551, 552, 553, 556, 559, 560, 563, 566, 573, 574, 577, 578], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.add_connection": {"executed_lines": [594, 595, 596, 599, 600, 601, 602, 605, 608, 609], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.add_event": {"executed_lines": [624, 629], "summary": {"covered_lines": 2, "num_statements": 4, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [625, 626], "excluded_lines": []}, "Simulation._assemble_graph": {"executed_lines": [640, 641, 644, 645, 649], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._check_blocks_are_managed": {"executed_lines": [666, 667, 668, 671, 672], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [673], "excluded_lines": []}, "Simulation._set_solver": {"executed_lines": [696, 697, 700, 703, 706, 707, 708, 711, 712, 715], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.reset": {"executed_lines": [746, 749, 752, 755, 758, 759, 762, 763, 766], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.linearize": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 5, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [784, 787, 788, 789, 791], "excluded_lines": []}, "Simulation.delinearize": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [796, 797, 799], "excluded_lines": []}, "Simulation._estimate_events": {"executed_lines": [818, 819, 822, 825, 828, 829, 832], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._buffer_events": {"executed_lines": [849, 850], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._detected_events": {"executed_lines": [869, 870, 873, 876, 879, 880, 883], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._update": {"executed_lines": [920, 923, 924], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._dag": {"executed_lines": [937, 940, 941, 944, 945], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._loops": {"executed_lines": [959, 960, 963, 966, 969, 970, 973, 974, 977, 978, 979, 980, 981, 984, 985], "summary": {"covered_lines": 15, "num_statements": 16, "percent_covered": 93.75, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [988], "excluded_lines": []}, "Simulation._solve": {"executed_lines": [1025, 1028, 1031, 1032, 1035, 1036, 1039, 1043, 1044, 1045, 1048, 1049, 1052], "summary": {"covered_lines": 13, "num_statements": 14, "percent_covered": 92.85714285714286, "percent_covered_display": "93", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [1040], "excluded_lines": []}, "Simulation.steadystate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 12, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 12, "excluded_lines": 0}, "missing_lines": [1090, 1091, 1094, 1097, 1100, 1103, 1104, 1107, 1108, 1115, 1118, 1124], "excluded_lines": []}, "Simulation._revert": {"executed_lines": [1136, 1139, 1140], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._sample": {"executed_lines": [1153, 1154], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._buffer_blocks": {"executed_lines": [1172, 1175, 1176], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation._step": {"executed_lines": [1212, 1215, 1218, 1221, 1224, 1225, 1228, 1229, 1232, 1233, 1236, 1237, 1240], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.timestep_fixed_explicit": {"executed_lines": [1272, 1275, 1276, 1279, 1282, 1285, 1288, 1291, 1292, 1295, 1298, 1301, 1302, 1305, 1308, 1311, 1312, 1315, 1318, 1321], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.timestep_fixed_implicit": {"executed_lines": [1351, 1354, 1358, 1361, 1364, 1367, 1370, 1373, 1379, 1380, 1383, 1386, 1389, 1390, 1393, 1403, 1406, 1409], "summary": {"covered_lines": 18, "num_statements": 23, "percent_covered": 78.26086956521739, "percent_covered_display": "78", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [1355, 1374, 1396, 1399, 1400], "excluded_lines": []}, "Simulation.timestep_adaptive_explicit": {"executed_lines": [1446, 1449, 1453, 1456, 1459, 1462, 1465, 1466, 1469, 1472, 1473, 1474, 1475, 1476, 1479, 1482, 1483, 1486, 1489, 1490, 1493, 1494, 1498, 1499, 1500, 1501, 1504, 1507, 1510], "summary": {"covered_lines": 29, "num_statements": 30, "percent_covered": 96.66666666666667, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [1450], "excluded_lines": []}, "Simulation.timestep_adaptive_implicit": {"executed_lines": [1551, 1554, 1558, 1561, 1564, 1567, 1570, 1573, 1574, 1577, 1578, 1579, 1580, 1583, 1586, 1587, 1588, 1589, 1592, 1595, 1596, 1599, 1602, 1603, 1606, 1607, 1611, 1612, 1613, 1614, 1617, 1620, 1623], "summary": {"covered_lines": 33, "num_statements": 34, "percent_covered": 97.05882352941177, "percent_covered_display": "97", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [1555], "excluded_lines": []}, "Simulation.timestep": {"executed_lines": [1652, 1653, 1654, 1656, 1658, 1659, 1661], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.step": {"executed_lines": [1666, 1669], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.stop": {"executed_lines": [1679], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Simulation.run": {"executed_lines": [1710, 1713, 1714, 1717, 1720, 1723, 1726, 1727, 1730, 1733, 1736, 1737, 1740, 1743, 1751, 1754, 1757, 1762, 1768, 1771, 1775, 1778, 1779, 1783, 1784, 1787, 1790, 1793, 1795], "summary": {"covered_lines": 29, "num_statements": 33, "percent_covered": 87.87878787878788, "percent_covered_display": "88", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [1758, 1759, 1772, 1780], "excluded_lines": []}, "": {"executed_lines": [13, 15, 16, 17, 19, 21, 23, 32, 34, 35, 36, 37, 39, 41, 43, 45, 50, 51, 162, 245, 252, 272, 285, 286, 306, 337, 341, 346, 352, 376, 398, 399, 420, 461, 462, 538, 581, 612, 634, 658, 680, 727, 771, 794, 804, 835, 853, 888, 927, 948, 996, 1055, 1129, 1143, 1157, 1179, 1245, 1324, 1412, 1513, 1626, 1664, 1674, 1682], "summary": {"covered_lines": 63, "num_statements": 63, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Simulation": {"executed_lines": [178, 179, 180, 183, 184, 185, 188, 191, 194, 197, 200, 203, 206, 209, 212, 215, 218, 221, 224, 225, 226, 229, 230, 231, 234, 235, 236, 239, 242, 265, 280, 296, 297, 298, 299, 300, 301, 312, 315, 318, 338, 342, 343, 347, 415, 416, 477, 480, 483, 484, 485, 486, 487, 490, 491, 494, 495, 496, 499, 500, 501, 502, 503, 508, 513, 514, 518, 521, 522, 525, 528, 551, 552, 553, 556, 559, 560, 563, 566, 573, 574, 577, 578, 594, 595, 596, 599, 600, 601, 602, 605, 608, 609, 624, 629, 640, 641, 644, 645, 649, 666, 667, 668, 671, 672, 696, 697, 700, 703, 706, 707, 708, 711, 712, 715, 746, 749, 752, 755, 758, 759, 762, 763, 766, 818, 819, 822, 825, 828, 829, 832, 849, 850, 869, 870, 873, 876, 879, 880, 883, 920, 923, 924, 937, 940, 941, 944, 945, 959, 960, 963, 966, 969, 970, 973, 974, 977, 978, 979, 980, 981, 984, 985, 1025, 1028, 1031, 1032, 1035, 1036, 1039, 1043, 1044, 1045, 1048, 1049, 1052, 1136, 1139, 1140, 1153, 1154, 1172, 1175, 1176, 1212, 1215, 1218, 1221, 1224, 1225, 1228, 1229, 1232, 1233, 1236, 1237, 1240, 1272, 1275, 1276, 1279, 1282, 1285, 1288, 1291, 1292, 1295, 1298, 1301, 1302, 1305, 1308, 1311, 1312, 1315, 1318, 1321, 1351, 1354, 1358, 1361, 1364, 1367, 1370, 1373, 1379, 1380, 1383, 1386, 1389, 1390, 1393, 1403, 1406, 1409, 1446, 1449, 1453, 1456, 1459, 1462, 1465, 1466, 1469, 1472, 1473, 1474, 1475, 1476, 1479, 1482, 1483, 1486, 1489, 1490, 1493, 1494, 1498, 1499, 1500, 1501, 1504, 1507, 1510, 1551, 1554, 1558, 1561, 1564, 1567, 1570, 1573, 1574, 1577, 1578, 1579, 1580, 1583, 1586, 1587, 1588, 1589, 1592, 1595, 1596, 1599, 1602, 1603, 1606, 1607, 1611, 1612, 1613, 1614, 1617, 1620, 1623, 1652, 1653, 1654, 1656, 1658, 1659, 1661, 1666, 1669, 1679, 1710, 1713, 1714, 1717, 1720, 1723, 1726, 1727, 1730, 1733, 1736, 1737, 1740, 1743, 1751, 1754, 1757, 1762, 1768, 1771, 1775, 1778, 1779, 1783, 1784, 1787, 1790, 1793, 1795], "summary": {"covered_lines": 336, "num_statements": 392, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 56, "excluded_lines": 0}, "missing_lines": [249, 321, 322, 324, 327, 331, 332, 334, 370, 371, 389, 392, 394, 395, 417, 436, 437, 438, 441, 515, 625, 626, 673, 784, 787, 788, 789, 791, 796, 797, 799, 988, 1040, 1090, 1091, 1094, 1097, 1100, 1103, 1104, 1107, 1108, 1115, 1118, 1124, 1355, 1374, 1396, 1399, 1400, 1450, 1555, 1758, 1759, 1772, 1780], "excluded_lines": []}, "": {"executed_lines": [13, 15, 16, 17, 19, 21, 23, 32, 34, 35, 36, 37, 39, 41, 43, 45, 50, 51, 162, 245, 252, 272, 285, 286, 306, 337, 341, 346, 352, 376, 398, 399, 420, 461, 462, 538, 581, 612, 634, 658, 680, 727, 771, 794, 804, 835, 853, 888, 927, 948, 996, 1055, 1129, 1143, 1157, 1179, 1245, 1324, 1412, 1513, 1626, 1664, 1674, 1682], "summary": {"covered_lines": 63, "num_statements": 63, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\__init__.py": {"executed_lines": [2, 5, 8, 11, 14, 15, 16, 19, 20, 21, 22, 25, 26, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [2, 5, 8, 11, 14, 15, 16, 19, 20, 21, 22, 25, 26, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [2, 5, 8, 11, 14, 15, 16, 19, 20, 21, 22, 25, 26, 27, 28, 31, 32, 33, 34, 35, 36, 37, 38], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\_rungekutta.py": {"executed_lines": [10, 12, 19, 24, 25, 53, 54, 57, 58, 61, 64, 67, 70, 73, 76, 98, 99, 100, 103, 106, 109, 112, 115, 118, 120, 123, 145, 148, 151, 152, 153, 154, 157, 158, 161, 164, 165, 199, 200, 203, 204, 207, 210, 213, 216, 219, 222, 225, 247, 248, 249, 252, 255, 258, 261, 264, 267, 269, 272, 291, 292, 295, 298, 301, 302, 303, 306, 309, 312, 316, 319, 322, 344, 345, 348, 351, 354, 357, 358, 359, 360, 363, 364, 367, 370], "summary": {"covered_lines": 83, "num_statements": 83, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"ExplicitRungeKutta.__init__": {"executed_lines": [54, 57, 58, 61, 64, 67, 70, 73], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ExplicitRungeKutta.error_controller": {"executed_lines": [98, 99, 100, 103, 106, 109, 112, 115, 118, 120], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ExplicitRungeKutta.step": {"executed_lines": [145, 148, 151, 152, 153, 154, 157, 158, 161], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DiagonallyImplicitRungeKutta.__init__": {"executed_lines": [200, 203, 204, 207, 210, 213, 216, 219, 222], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DiagonallyImplicitRungeKutta.error_controller": {"executed_lines": [247, 248, 249, 252, 255, 258, 261, 264, 267, 269], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DiagonallyImplicitRungeKutta.solve": {"executed_lines": [291, 292, 295, 298, 301, 302, 303, 306, 309, 312, 316, 319], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DiagonallyImplicitRungeKutta.step": {"executed_lines": [344, 345, 348, 351, 354, 357, 358, 359, 360, 363, 364, 367, 370], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 19, 24, 25, 53, 76, 123, 164, 165, 199, 225, 272, 322], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ExplicitRungeKutta": {"executed_lines": [54, 57, 58, 61, 64, 67, 70, 73, 98, 99, 100, 103, 106, 109, 112, 115, 118, 120, 145, 148, 151, 152, 153, 154, 157, 158, 161], "summary": {"covered_lines": 27, "num_statements": 27, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "DiagonallyImplicitRungeKutta": {"executed_lines": [200, 203, 204, 207, 210, 213, 216, 219, 222, 247, 248, 249, 252, 255, 258, 261, 264, 267, 269, 291, 292, 295, 298, 301, 302, 303, 306, 309, 312, 316, 319, 344, 345, 348, 351, 354, 357, 358, 359, 360, 363, 364, 367, 370], "summary": {"covered_lines": 44, "num_statements": 44, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 19, 24, 25, 53, 76, 123, 164, 165, 199, 225, 272, 322], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\_solver.py": {"executed_lines": [10, 12, 14, 25, 33, 34, 67, 76, 79, 80, 83, 86, 89, 92, 95, 98, 101, 104, 105, 108, 116, 119, 120, 123, 124, 132, 133, 134, 137, 138, 147, 150, 151, 154, 155, 158, 170, 171, 174, 182, 185, 199, 202, 206, 207, 210, 227, 230, 231, 251, 255, 261, 264, 271, 273, 278, 294, 303, 308, 331, 336, 362, 363, 382, 383, 386, 387, 390, 395, 419, 422, 423, 424, 426, 429, 492, 493, 496, 499, 502, 505, 506, 508, 509, 510, 513, 514, 516, 519, 522, 523, 545, 546, 549, 550, 553, 556, 559, 572, 575, 578, 583, 602, 607, 648, 651, 654, 655, 656, 657, 658, 661, 665, 666, 668, 671, 743, 744, 747, 750, 753, 764, 765, 767, 768, 769, 772, 773, 775, 778], "summary": {"covered_lines": 127, "num_statements": 134, "percent_covered": 94.77611940298507, "percent_covered_display": "95", "missing_lines": 7, "excluded_lines": 0}, "missing_lines": [252, 291, 356, 357, 515, 662, 774], "excluded_lines": [], "functions": {"Solver.__init__": {"executed_lines": [76, 79, 80, 83, 86, 89, 92, 95, 98, 101], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.__str__": {"executed_lines": [105], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.__len__": {"executed_lines": [116], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.__bool__": {"executed_lines": [120], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.stage": {"executed_lines": [147], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.is_first_stage": {"executed_lines": [151], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.is_last_stage": {"executed_lines": [155], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.stages": {"executed_lines": [170, 171], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.get": {"executed_lines": [182], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.set": {"executed_lines": [199], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.reset": {"executed_lines": [206, 207], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.buffer": {"executed_lines": [227], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.cast": {"executed_lines": [251, 255, 261, 264, 271, 273], "summary": {"covered_lines": 6, "num_statements": 7, "percent_covered": 85.71428571428571, "percent_covered_display": "86", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [252], "excluded_lines": []}, "Solver.error_controller": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [291], "excluded_lines": []}, "Solver.revert": {"executed_lines": [303], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.step": {"executed_lines": [331], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Solver.interpolate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [356, 357], "excluded_lines": []}, "ExplicitSolver.__init__": {"executed_lines": [383, 386, 387, 390], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ExplicitSolver.integrate_singlestep": {"executed_lines": [419, 422, 423, 424, 426], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ExplicitSolver.integrate": {"executed_lines": [492, 493, 496, 499, 502, 505, 506, 508, 509, 510, 513, 514, 516, 519], "summary": {"covered_lines": 14, "num_statements": 15, "percent_covered": 93.33333333333333, "percent_covered_display": "93", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [515], "excluded_lines": []}, "ImplicitSolver.__init__": {"executed_lines": [546, 549, 550, 553, 556], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ImplicitSolver.buffer": {"executed_lines": [572, 575, 578], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ImplicitSolver.solve": {"executed_lines": [602], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ImplicitSolver.integrate_singlestep": {"executed_lines": [648, 651, 654, 655, 656, 657, 658, 661, 665, 666, 668], "summary": {"covered_lines": 11, "num_statements": 12, "percent_covered": 91.66666666666667, "percent_covered_display": "92", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [662], "excluded_lines": []}, "ImplicitSolver.integrate": {"executed_lines": [743, 744, 747, 750, 753, 764, 765, 767, 768, 769, 772, 773, 775, 778], "summary": {"covered_lines": 14, "num_statements": 15, "percent_covered": 93.33333333333333, "percent_covered_display": "93", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [774], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 25, 33, 34, 67, 104, 108, 119, 123, 124, 137, 138, 150, 154, 158, 174, 185, 202, 210, 230, 231, 278, 294, 308, 336, 362, 363, 382, 395, 429, 522, 523, 545, 559, 583, 607, 671], "summary": {"covered_lines": 36, "num_statements": 36, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Solver": {"executed_lines": [76, 79, 80, 83, 86, 89, 92, 95, 98, 101, 105, 116, 120, 132, 133, 134, 147, 151, 155, 170, 171, 182, 199, 206, 207, 227, 251, 255, 261, 264, 271, 273, 303, 331], "summary": {"covered_lines": 34, "num_statements": 38, "percent_covered": 89.47368421052632, "percent_covered_display": "89", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [252, 291, 356, 357], "excluded_lines": []}, "ExplicitSolver": {"executed_lines": [383, 386, 387, 390, 419, 422, 423, 424, 426, 492, 493, 496, 499, 502, 505, 506, 508, 509, 510, 513, 514, 516, 519], "summary": {"covered_lines": 23, "num_statements": 24, "percent_covered": 95.83333333333333, "percent_covered_display": "96", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [515], "excluded_lines": []}, "ImplicitSolver": {"executed_lines": [546, 549, 550, 553, 556, 572, 575, 578, 602, 648, 651, 654, 655, 656, 657, 658, 661, 665, 666, 668, 743, 744, 747, 750, 753, 764, 765, 767, 768, 769, 772, 773, 775, 778], "summary": {"covered_lines": 34, "num_statements": 36, "percent_covered": 94.44444444444444, "percent_covered_display": "94", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [662, 774], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 25, 33, 34, 67, 104, 108, 119, 123, 124, 137, 138, 150, 154, 158, 174, 185, 202, 210, 230, 231, 278, 294, 308, 336, 362, 363, 382, 395, 429, 522, 523, 545, 559, 583, 607, 671], "summary": {"covered_lines": 36, "num_statements": 36, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\bdf.py": {"executed_lines": [10, 12, 13, 18, 19, 50, 51, 54, 57, 65, 68, 69, 72, 73, 90, 91, 93, 96, 109, 110, 111, 113, 114, 117, 121, 124, 127, 130, 140, 143, 146, 149, 150, 153, 172, 173, 174, 175, 178, 179, 180, 183, 186, 190, 193, 196, 222, 223, 224, 226, 231, 232, 264, 265, 268, 271, 274, 275, 308, 309, 312, 315, 318, 319, 352, 353, 356, 359, 362, 363, 396, 397, 400, 403, 406, 407, 443, 444, 447, 450], "summary": {"covered_lines": 74, "num_statements": 74, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"BDF.__init__": {"executed_lines": [51, 54, 57, 65, 68, 69], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF.cast": {"executed_lines": [90, 91, 93], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF.stages": {"executed_lines": [109, 110, 111, 113, 114], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF.reset": {"executed_lines": [121, 124, 127], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF.buffer": {"executed_lines": [140, 143, 146, 149, 150], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF.solve": {"executed_lines": [172, 173, 174, 175, 178, 179, 180, 183, 186, 190, 193], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF.step": {"executed_lines": [222, 223, 224, 226], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF2.__init__": {"executed_lines": [265, 268, 271], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF3.__init__": {"executed_lines": [309, 312, 315], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF4.__init__": {"executed_lines": [353, 356, 359], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF5.__init__": {"executed_lines": [397, 400, 403], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF6.__init__": {"executed_lines": [444, 447, 450], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 13, 18, 19, 50, 72, 73, 96, 117, 130, 153, 196, 231, 232, 264, 274, 275, 308, 318, 319, 352, 362, 363, 396, 406, 407, 443], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"BDF": {"executed_lines": [51, 54, 57, 65, 68, 69, 90, 91, 93, 109, 110, 111, 113, 114, 121, 124, 127, 140, 143, 146, 149, 150, 172, 173, 174, 175, 178, 179, 180, 183, 186, 190, 193, 222, 223, 224, 226], "summary": {"covered_lines": 37, "num_statements": 37, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF2": {"executed_lines": [265, 268, 271], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF3": {"executed_lines": [309, 312, 315], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF4": {"executed_lines": [353, 356, 359], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF5": {"executed_lines": [397, 400, 403], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "BDF6": {"executed_lines": [444, 447, 450], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 13, 18, 19, 50, 72, 73, 96, 117, 130, 153, 196, 231, 232, 264, 274, 275, 308, 318, 319, 352, 362, 363, 396, 406, 407, 443], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\dirk2.py": {"executed_lines": [12, 17, 18, 52, 53, 56, 59, 62, 65, 71], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"DIRK2.__init__": {"executed_lines": [53, 56, 59, 62, 65, 71], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 52], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"DIRK2": {"executed_lines": [53, 56, 59, 62, 65, 71], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 52], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\dirk3.py": {"executed_lines": [12, 17, 18, 57, 58, 61, 64, 67, 70], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"DIRK3.__init__": {"executed_lines": [58, 61, 64, 67, 70], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 57], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"DIRK3": {"executed_lines": [58, 61, 64, 67, 70], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 57], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\esdirk32.py": {"executed_lines": [12, 17, 18, 56, 57, 60, 63, 64, 67, 70, 73, 81], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"ESDIRK32.__init__": {"executed_lines": [57, 60, 63, 64, 67, 70, 73, 81], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 56], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ESDIRK32": {"executed_lines": [57, 60, 63, 64, 67, 70, 73, 81], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 56], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\esdirk4.py": {"executed_lines": [12, 17, 18, 52, 53, 56, 59, 62, 67], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"ESDIRK4.__init__": {"executed_lines": [53, 56, 59, 62, 67], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 52], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ESDIRK4": {"executed_lines": [53, 56, 59, 62, 67], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 52], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\esdirk43.py": {"executed_lines": [12, 14, 19, 20, 56, 57, 60, 63, 64, 67, 70, 73, 86, 90, 95], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"ESDIRK43.__init__": {"executed_lines": [57, 60, 63, 64, 67, 70, 73, 86, 90, 95], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 19, 20, 56], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ESDIRK43": {"executed_lines": [57, 60, 63, 64, 67, 70, 73, 86, 90, 95], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 14, 19, 20, 56], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\esdirk54.py": {"executed_lines": [12, 17, 18, 54, 55, 58, 61, 62, 65, 68, 74, 91, 96, 102], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"ESDIRK54.__init__": {"executed_lines": [55, 58, 61, 62, 65, 68, 74, 91, 96, 102], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 54], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ESDIRK54": {"executed_lines": [55, 58, 61, 62, 65, 68, 74, 91, 96, 102], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 54], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\esdirk85.py": {"executed_lines": [12, 17, 18, 58, 59, 62, 65, 66, 69, 72, 82, 137, 145, 153], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"ESDIRK85.__init__": {"executed_lines": [59, 62, 65, 66, 69, 72, 82, 137, 145, 153], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 58], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ESDIRK85": {"executed_lines": [59, 62, 65, 66, 69, 72, 82, 137, 145, 153], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 58], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\euler.py": {"executed_lines": [10, 15, 16, 58, 80, 83, 86, 89, 90, 136, 156, 159, 162, 165, 172], "summary": {"covered_lines": 13, "num_statements": 14, "percent_covered": 92.85714285714286, "percent_covered_display": "93", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [169], "excluded_lines": [], "functions": {"EUF.step": {"executed_lines": [80, 83, 86], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "EUB.solve": {"executed_lines": [156, 159, 162, 165, 172], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [169], "excluded_lines": []}, "": {"executed_lines": [10, 15, 16, 58, 89, 90, 136], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"EUF": {"executed_lines": [80, 83, 86], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "EUB": {"executed_lines": [156, 159, 162, 165, 172], "summary": {"covered_lines": 5, "num_statements": 6, "percent_covered": 83.33333333333333, "percent_covered_display": "83", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [169], "excluded_lines": []}, "": {"executed_lines": [10, 15, 16, 58, 89, 90, 136], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\gear.py": {"executed_lines": [10, 12, 14, 15, 17, 27, 52, 56, 57, 60, 63, 64, 65, 66, 69, 70, 71, 72, 73, 76, 79, 84, 85, 125, 126, 129, 130, 133, 136, 139, 142, 143, 146, 147, 164, 165, 167, 170, 183, 184, 185, 187, 188, 191, 195, 196, 199, 202, 205, 217, 220, 221, 224, 227, 228, 231, 232, 233, 238, 246, 249, 252, 253, 256, 278, 281, 284, 287, 290, 293, 295, 300, 320, 321, 322, 323, 326, 327, 328, 331, 334, 338, 341, 344, 367, 368, 369, 370, 373, 374, 375, 378, 383, 384, 421, 422, 425, 426, 429, 430, 433, 434, 470, 471, 474, 475, 478, 479, 482, 483, 519, 520, 523, 524, 527, 528, 531, 532, 569, 570, 573, 574, 577, 578, 581, 582, 626, 627, 630, 633, 636, 637, 640, 652, 655, 656, 659, 662, 663, 666, 667, 668, 673, 701, 704, 705, 708, 709, 712, 715, 718, 721, 722, 726, 728, 733, 753, 754, 755, 756, 759, 760, 761, 764, 767, 771, 774, 777, 802, 803, 804, 805, 808, 811, 812, 813, 816, 817, 818, 820], "summary": {"covered_lines": 174, "num_statements": 175, "percent_covered": 99.42857142857143, "percent_covered_display": "99", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [53], "excluded_lines": [], "functions": {"compute_bdf_coefficients": {"executed_lines": [52, 56, 57, 60, 63, 64, 65, 66, 69, 70, 71, 72, 73, 76, 79], "summary": {"covered_lines": 15, "num_statements": 16, "percent_covered": 93.75, "percent_covered_display": "94", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [53], "excluded_lines": []}, "GEAR.__init__": {"executed_lines": [126, 129, 130, 133, 136, 139, 142, 143], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR.cast": {"executed_lines": [164, 165, 167], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR.stages": {"executed_lines": [183, 184, 185, 187, 188], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR.reset": {"executed_lines": [195, 196, 199, 202], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR.buffer": {"executed_lines": [217, 220, 221, 224, 227, 228, 231, 232, 233], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR.revert": {"executed_lines": [246, 249, 252, 253], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR.error_controller": {"executed_lines": [278, 281, 284, 287, 290, 293, 295], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR.solve": {"executed_lines": [320, 321, 322, 323, 326, 327, 328, 331, 334, 338, 341], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR.step": {"executed_lines": [367, 368, 369, 370, 373, 374, 375, 378], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR21.__init__": {"executed_lines": [422, 425, 426, 429, 430], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR32.__init__": {"executed_lines": [471, 474, 475, 478, 479], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR43.__init__": {"executed_lines": [520, 523, 524, 527, 528], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR54.__init__": {"executed_lines": [570, 573, 574, 577, 578], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR52A.__init__": {"executed_lines": [627, 630, 633, 636, 637], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR52A.buffer": {"executed_lines": [652, 655, 656, 659, 662, 663, 666, 667, 668], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR52A.error_controller": {"executed_lines": [701, 704, 705, 708, 709, 712, 715, 718, 721, 722, 726, 728], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR52A.solve": {"executed_lines": [753, 754, 755, 756, 759, 760, 761, 764, 767, 771, 774], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR52A.step": {"executed_lines": [802, 803, 804, 805, 808, 811, 812, 813, 816, 817, 818, 820], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 15, 17, 27, 84, 85, 125, 146, 147, 170, 191, 205, 238, 256, 300, 344, 383, 384, 421, 433, 434, 470, 482, 483, 519, 531, 532, 569, 581, 582, 626, 640, 673, 733, 777], "summary": {"covered_lines": 31, "num_statements": 31, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"GEAR": {"executed_lines": [126, 129, 130, 133, 136, 139, 142, 143, 164, 165, 167, 183, 184, 185, 187, 188, 195, 196, 199, 202, 217, 220, 221, 224, 227, 228, 231, 232, 233, 246, 249, 252, 253, 278, 281, 284, 287, 290, 293, 295, 320, 321, 322, 323, 326, 327, 328, 331, 334, 338, 341, 367, 368, 369, 370, 373, 374, 375, 378], "summary": {"covered_lines": 59, "num_statements": 59, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR21": {"executed_lines": [422, 425, 426, 429, 430], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR32": {"executed_lines": [471, 474, 475, 478, 479], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR43": {"executed_lines": [520, 523, 524, 527, 528], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR54": {"executed_lines": [570, 573, 574, 577, 578], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "GEAR52A": {"executed_lines": [627, 630, 633, 636, 637, 652, 655, 656, 659, 662, 663, 666, 667, 668, 701, 704, 705, 708, 709, 712, 715, 718, 721, 722, 726, 728, 753, 754, 755, 756, 759, 760, 761, 764, 767, 771, 774, 802, 803, 804, 805, 808, 811, 812, 813, 816, 817, 818, 820], "summary": {"covered_lines": 49, "num_statements": 49, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 14, 15, 17, 27, 52, 56, 57, 60, 63, 64, 65, 66, 69, 70, 71, 72, 73, 76, 79, 84, 85, 125, 146, 147, 170, 191, 205, 238, 256, 300, 344, 383, 384, 421, 433, 434, 470, 482, 483, 519, 531, 532, 569, 581, 582, 626, 640, 673, 733, 777], "summary": {"covered_lines": 46, "num_statements": 47, "percent_covered": 97.87234042553192, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [53], "excluded_lines": []}}}, "src\\pathsim\\solvers\\rk4.py": {"executed_lines": [12, 17, 18, 58, 59, 62, 65, 68, 71, 79], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [80, 81, 82], "excluded_lines": [], "functions": {"RK4.__init__": {"executed_lines": [59, 62, 65, 68, 71], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RK4.interpolate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [80, 81, 82], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 58, 79], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"RK4": {"executed_lines": [59, 62, 65, 68, 71], "summary": {"covered_lines": 5, "num_statements": 8, "percent_covered": 62.5, "percent_covered_display": "62", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [80, 81, 82], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 58, 79], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\rkbs32.py": {"executed_lines": [12, 17, 18, 59, 60, 63, 66, 67, 70, 73, 76, 84], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"RKBS32.__init__": {"executed_lines": [60, 63, 66, 67, 70, 73, 76, 84], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 59], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"RKBS32": {"executed_lines": [60, 63, 66, 67, 70, 73, 76, 84], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 59], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\rkck54.py": {"executed_lines": [12, 17, 18, 61, 62, 65, 68, 69, 72, 75, 78, 88], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"RKCK54.__init__": {"executed_lines": [62, 65, 68, 69, 72, 75, 78, 88], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 61], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"RKCK54": {"executed_lines": [62, 65, 68, 69, 72, 75, 78, 88], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 61], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\rkdp54.py": {"executed_lines": [12, 17, 18, 58, 59, 62, 65, 66, 69, 72, 75, 86], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"RKDP54.__init__": {"executed_lines": [59, 62, 65, 66, 69, 72, 75, 86], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 58], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"RKDP54": {"executed_lines": [59, 62, 65, 66, 69, 72, 75, 86], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 58], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\rkdp87.py": {"executed_lines": [12, 17, 18, 59, 60, 63, 66, 67, 70, 73, 76, 93, 96], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"RKDP87.__init__": {"executed_lines": [60, 63, 66, 67, 70, 73, 76, 93, 96], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 59], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"RKDP87": {"executed_lines": [60, 63, 66, 67, 70, 73, 76, 93, 96], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 59], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\rkf21.py": {"executed_lines": [12, 17, 18, 57, 58, 61, 64, 65, 68, 71, 74, 81], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"RKF21.__init__": {"executed_lines": [58, 61, 64, 65, 68, 71, 74, 81], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 57], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"RKF21": {"executed_lines": [58, 61, 64, 65, 68, 71, 74, 81], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 57], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\rkf45.py": {"executed_lines": [12, 17, 18, 60, 61, 64, 67, 68, 71, 74, 77, 87], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"RKF45.__init__": {"executed_lines": [61, 64, 67, 68, 71, 74, 77, 87], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 60], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"RKF45": {"executed_lines": [61, 64, 67, 68, 71, 74, 77, 87], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 60], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\rkf78.py": {"executed_lines": [12, 17, 18, 57, 58, 61, 64, 65, 68, 71, 74, 91], "summary": {"covered_lines": 11, "num_statements": 11, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"RKF78.__init__": {"executed_lines": [58, 61, 64, 65, 68, 71, 74, 91], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 57], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"RKF78": {"executed_lines": [58, 61, 64, 65, 68, 71, 74, 91], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 57], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\rkv65.py": {"executed_lines": [12, 17, 18, 57, 58, 61, 64, 65, 68, 71, 74, 87, 88, 89], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"RKV65.__init__": {"executed_lines": [58, 61, 64, 65, 68, 71, 74, 87, 88, 89], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 57], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"RKV65": {"executed_lines": [58, 61, 64, 65, 68, 71, 74, 87, 88, 89], "summary": {"covered_lines": 10, "num_statements": 10, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 57], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\ssprk22.py": {"executed_lines": [12, 17, 18, 59, 60, 63, 66, 69, 72, 78], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [79, 80, 81], "excluded_lines": [], "functions": {"SSPRK22.__init__": {"executed_lines": [60, 63, 66, 69, 72], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SSPRK22.interpolate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [79, 80, 81], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 59, 78], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"SSPRK22": {"executed_lines": [60, 63, 66, 69, 72], "summary": {"covered_lines": 5, "num_statements": 8, "percent_covered": 62.5, "percent_covered_display": "62", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [79, 80, 81], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 59, 78], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\ssprk33.py": {"executed_lines": [12, 17, 18, 55, 56, 59, 62, 65, 68, 74], "summary": {"covered_lines": 9, "num_statements": 12, "percent_covered": 75.0, "percent_covered_display": "75", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [75, 76, 77], "excluded_lines": [], "functions": {"SSPRK33.__init__": {"executed_lines": [56, 59, 62, 65, 68], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "SSPRK33.interpolate": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [75, 76, 77], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 55, 74], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"SSPRK33": {"executed_lines": [56, 59, 62, 65, 68], "summary": {"covered_lines": 5, "num_statements": 8, "percent_covered": 62.5, "percent_covered_display": "62", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [75, 76, 77], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 55, 74], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\ssprk34.py": {"executed_lines": [12, 17, 18, 55, 56, 59, 62, 65, 68], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"SSPRK34.__init__": {"executed_lines": [56, 59, 62, 65, 68], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 55], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"SSPRK34": {"executed_lines": [56, 59, 62, 65, 68], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [12, 17, 18, 55], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\solvers\\steadystate.py": {"executed_lines": [12, 14, 19, 20, 34], "summary": {"covered_lines": 4, "num_statements": 10, "percent_covered": 40.0, "percent_covered_display": "40", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [54, 56, 59, 62, 67, 69], "excluded_lines": [], "functions": {"SteadyState.solve": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [54, 56, 59, 62, 67, 69], "excluded_lines": []}, "": {"executed_lines": [12, 14, 19, 20, 34], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"SteadyState": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [54, 56, 59, 62, 67, 69], "excluded_lines": []}, "": {"executed_lines": [12, 14, 19, 20, 34], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\subsystem.py": {"executed_lines": [13, 15, 17, 19, 21, 23, 24, 25, 27, 36, 37, 50, 51, 54, 64, 65, 68, 73, 74, 152, 160, 163, 166, 169, 172, 173, 176, 179, 182, 185, 187, 188, 189, 191, 193, 195, 198, 201, 202, 205, 208, 211, 214, 218, 219, 222, 230, 231, 232, 233, 234, 235, 236, 239, 251, 256, 264, 267, 268, 269, 270, 275, 279, 282, 283, 290, 291, 301, 302, 303, 304, 305, 306, 311, 329, 333, 336, 337, 340, 344, 345, 346, 349, 353, 354, 355, 356, 359, 379, 387, 400, 401, 441, 442, 447, 448, 449, 450, 455, 456, 457, 459, 460, 461, 466, 477, 478, 483, 494, 497, 501, 511, 512, 515, 518, 519, 522, 523, 526, 574, 590, 591, 592, 593, 594, 595, 596, 599, 626, 629, 632, 634, 637, 638, 641, 642, 645, 646, 649, 650, 653, 656, 674, 677, 678, 679, 680, 681, 684, 688, 689, 692, 701, 702], "summary": {"covered_lines": 152, "num_statements": 197, "percent_covered": 77.15736040609137, "percent_covered_display": "77", "missing_lines": 45, "excluded_lines": 0}, "missing_lines": [323, 324, 375, 376, 381, 382, 389, 392, 395, 397, 403, 406, 407, 408, 409, 410, 413, 414, 417, 418, 419, 422, 423, 424, 425, 426, 431, 436, 498, 537, 538, 541, 544, 547, 548, 551, 552, 555, 556, 557, 558, 559, 562, 563, 566], "excluded_lines": [], "functions": {"Interface.__len__": {"executed_lines": [51], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Interface.register_port_map": {"executed_lines": [64, 65, 68], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.__init__": {"executed_lines": [160, 163, 166, 169, 172, 173, 176, 179, 182, 185, 187, 188, 189, 191, 193, 195, 198, 201, 202, 205, 208, 211], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.__len__": {"executed_lines": [218, 219], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.__call__": {"executed_lines": [230, 231, 232, 233, 234, 235, 236], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.__contains__": {"executed_lines": [251], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem._check_connections": {"executed_lines": [264, 267, 268, 269, 270], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem._assemble_graph": {"executed_lines": [279, 282, 283], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.size": {"executed_lines": [301, 302, 303, 304, 305, 306], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.plot": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [323, 324], "excluded_lines": []}, "Subsystem.reset": {"executed_lines": [333, 336, 337], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.on": {"executed_lines": [344, 345, 346], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.off": {"executed_lines": [353, 354, 355, 356], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.linearize": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [375, 376], "excluded_lines": []}, "Subsystem.delinearize": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [381, 382], "excluded_lines": []}, "Subsystem.to_dict": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [389, 392, 395, 397], "excluded_lines": []}, "Subsystem.from_dict": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 18, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 18, "excluded_lines": 0}, "missing_lines": [403, 406, 407, 408, 409, 410, 413, 414, 417, 418, 419, 422, 423, 424, 425, 426, 431, 436], "excluded_lines": []}, "Subsystem.events": {"executed_lines": [447, 448, 449, 450], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.inputs": {"executed_lines": [457], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.outputs": {"executed_lines": [461], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.sample": {"executed_lines": [477, 478], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.update": {"executed_lines": [494, 497], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [498], "excluded_lines": []}, "Subsystem._dag": {"executed_lines": [511, 512, 515, 518, 519, 522, 523], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem._loops": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 16, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 16, "excluded_lines": 0}, "missing_lines": [537, 538, 541, 544, 547, 548, 551, 552, 555, 556, 557, 558, 559, 562, 563, 566], "excluded_lines": []}, "Subsystem.solve": {"executed_lines": [590, 591, 592, 593, 594, 595, 596], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.step": {"executed_lines": [626, 629, 632, 634, 637, 638, 641, 642, 645, 646, 649, 650, 653], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.set_solver": {"executed_lines": [674, 677, 678, 679, 680, 681], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.revert": {"executed_lines": [688, 689], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem.buffer": {"executed_lines": [701, 702], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [13, 15, 17, 19, 21, 23, 24, 25, 27, 36, 37, 50, 54, 73, 74, 152, 214, 222, 239, 256, 275, 290, 291, 311, 329, 340, 349, 359, 379, 387, 400, 401, 441, 442, 455, 456, 459, 460, 466, 483, 501, 526, 574, 599, 656, 684, 692], "summary": {"covered_lines": 45, "num_statements": 45, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Interface": {"executed_lines": [51, 64, 65, 68], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Subsystem": {"executed_lines": [160, 163, 166, 169, 172, 173, 176, 179, 182, 185, 187, 188, 189, 191, 193, 195, 198, 201, 202, 205, 208, 211, 218, 219, 230, 231, 232, 233, 234, 235, 236, 251, 264, 267, 268, 269, 270, 279, 282, 283, 301, 302, 303, 304, 305, 306, 333, 336, 337, 344, 345, 346, 353, 354, 355, 356, 447, 448, 449, 450, 457, 461, 477, 478, 494, 497, 511, 512, 515, 518, 519, 522, 523, 590, 591, 592, 593, 594, 595, 596, 626, 629, 632, 634, 637, 638, 641, 642, 645, 646, 649, 650, 653, 674, 677, 678, 679, 680, 681, 688, 689, 701, 702], "summary": {"covered_lines": 103, "num_statements": 148, "percent_covered": 69.5945945945946, "percent_covered_display": "70", "missing_lines": 45, "excluded_lines": 0}, "missing_lines": [323, 324, 375, 376, 381, 382, 389, 392, 395, 397, 403, 406, 407, 408, 409, 410, 413, 414, 417, 418, 419, 422, 423, 424, 425, 426, 431, 436, 498, 537, 538, 541, 544, 547, 548, 551, 552, 555, 556, 557, 558, 559, 562, 563, 566], "excluded_lines": []}, "": {"executed_lines": [13, 15, 17, 19, 21, 23, 24, 25, 27, 36, 37, 50, 54, 73, 74, 152, 214, 222, 239, 256, 275, 290, 291, 311, 329, 340, 349, 359, 379, 387, 400, 401, 441, 442, 455, 456, 459, 460, 466, 483, 501, 526, 574, 599, 656, 684, 692], "summary": {"covered_lines": 45, "num_statements": 45, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\utils\\__init__.py": {"executed_lines": [0], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 0, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\utils\\adaptivebuffer.py": {"executed_lines": [13, 14, 19, 20, 40, 43, 44, 45, 48, 51, 52, 55, 67, 68, 71, 72, 73, 74, 77, 92, 93, 96, 97, 100, 101, 102, 105, 108, 117, 120, 122, 123], "summary": {"covered_lines": 31, "num_statements": 31, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"AdaptiveBuffer.__init__": {"executed_lines": [43, 44, 45, 48], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AdaptiveBuffer.__len__": {"executed_lines": [52], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AdaptiveBuffer.add": {"executed_lines": [67, 68, 71, 72, 73, 74], "summary": {"covered_lines": 6, "num_statements": 6, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AdaptiveBuffer.interp": {"executed_lines": [92, 93, 96, 97, 100, 101, 102, 105], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AdaptiveBuffer.get": {"executed_lines": [117], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "AdaptiveBuffer.clear": {"executed_lines": [122, 123], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [13, 14, 19, 20, 40, 51, 55, 77, 108, 120], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"AdaptiveBuffer": {"executed_lines": [43, 44, 45, 48, 52, 67, 68, 71, 72, 73, 74, 92, 93, 96, 97, 100, 101, 102, 105, 117, 122, 123], "summary": {"covered_lines": 22, "num_statements": 22, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [13, 14, 19, 20, 40, 51, 55, 77, 108, 120], "summary": {"covered_lines": 9, "num_statements": 9, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\utils\\analysis.py": {"executed_lines": [12, 13, 14, 16, 17, 18, 25, 26, 47, 48, 49, 52, 56, 57, 58, 61, 62, 63, 66, 67, 68, 71, 90, 92, 111, 120, 125], "summary": {"covered_lines": 25, "num_statements": 49, "percent_covered": 51.02040816326531, "percent_covered_display": "51", "missing_lines": 24, "excluded_lines": 0}, "missing_lines": [19, 20, 53, 81, 82, 83, 84, 85, 86, 87, 112, 113, 114, 115, 116, 117, 121, 122, 126, 127, 128, 129, 130, 131], "excluded_lines": [], "functions": {"Timer.__init__": {"executed_lines": [48, 49], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Timer.__float__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 1, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [53], "excluded_lines": []}, "Timer.__repr__": {"executed_lines": [57, 58], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Timer.__enter__": {"executed_lines": [62, 63], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Timer.__exit__": {"executed_lines": [67, 68], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "timer": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [81, 82, 87], "excluded_lines": []}, "timer.wrap_func": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 4, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [83, 84, 85, 86], "excluded_lines": []}, "Profiler.__init__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [112, 113, 114, 115, 116, 117], "excluded_lines": []}, "Profiler.__enter__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [121, 122], "excluded_lines": []}, "Profiler.__exit__": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [126, 127, 128, 129, 130, 131], "excluded_lines": []}, "": {"executed_lines": [12, 13, 14, 16, 17, 18, 25, 26, 47, 52, 56, 61, 66, 71, 90, 92, 111, 120, 125], "summary": {"covered_lines": 17, "num_statements": 19, "percent_covered": 89.47368421052632, "percent_covered_display": "89", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [19, 20], "excluded_lines": []}}, "classes": {"Timer": {"executed_lines": [48, 49, 57, 58, 62, 63, 67, 68], "summary": {"covered_lines": 8, "num_statements": 9, "percent_covered": 88.88888888888889, "percent_covered_display": "89", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [53], "excluded_lines": []}, "Profiler": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 14, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 14, "excluded_lines": 0}, "missing_lines": [112, 113, 114, 115, 116, 117, 121, 122, 126, 127, 128, 129, 130, 131], "excluded_lines": []}, "": {"executed_lines": [12, 13, 14, 16, 17, 18, 25, 26, 47, 52, 56, 61, 66, 71, 90, 92, 111, 120, 125], "summary": {"covered_lines": 17, "num_statements": 26, "percent_covered": 65.38461538461539, "percent_covered_display": "65", "missing_lines": 9, "excluded_lines": 0}, "missing_lines": [19, 20, 81, 82, 83, 84, 85, 86, 87], "excluded_lines": []}}}, "src\\pathsim\\utils\\gilbert.py": {"executed_lines": [12, 17, 69, 70, 73, 74, 76, 77, 80, 81, 83, 84, 85, 88, 89, 90, 92, 93, 94, 95, 96, 99, 100, 101, 102, 103, 104, 105, 106, 111, 112, 115, 118, 119, 122, 123, 125, 126, 127, 128, 129, 130, 131, 134, 135, 138, 139, 140, 142], "summary": {"covered_lines": 49, "num_statements": 50, "percent_covered": 98.0, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [108], "excluded_lines": [], "functions": {"gilbert_realization": {"executed_lines": [69, 70, 73, 74, 76, 77, 80, 81, 83, 84, 85, 88, 89, 90, 92, 93, 94, 95, 96, 99, 100, 101, 102, 103, 104, 105, 106, 111, 112, 115, 118, 119, 122, 123, 125, 126, 127, 128, 129, 130, 131, 134, 135, 138, 139, 140, 142], "summary": {"covered_lines": 47, "num_statements": 48, "percent_covered": 97.91666666666667, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [108], "excluded_lines": []}, "": {"executed_lines": [12, 17], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"": {"executed_lines": [12, 17, 69, 70, 73, 74, 76, 77, 80, 81, 83, 84, 85, 88, 89, 90, 92, 93, 94, 95, 96, 99, 100, 101, 102, 103, 104, 105, 106, 111, 112, 115, 118, 119, 122, 123, 125, 126, 127, 128, 129, 130, 131, 134, 135, 138, 139, 140, 142], "summary": {"covered_lines": 49, "num_statements": 50, "percent_covered": 98.0, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [108], "excluded_lines": []}}}, "src\\pathsim\\utils\\graph.py": {"executed_lines": [9, 14, 15, 51, 52, 53, 56, 59, 60, 63, 64, 65, 66, 67, 70, 73, 76, 77, 80, 81, 84, 85, 93, 96, 97, 109, 112, 120, 121, 122, 124, 125, 126, 127, 128, 130, 132, 133, 134, 136, 137, 138, 140, 141, 142, 143, 146, 153, 154, 155, 156, 157, 158, 161, 162, 165, 166, 167, 168, 171, 172, 173, 174, 177, 179, 182, 183, 185, 186, 187, 189, 190, 191, 193, 195, 196, 198, 201, 213, 214, 215, 217, 218, 219, 222, 224, 225, 228, 230, 234, 240, 244, 250, 253, 259, 260, 261, 262, 265, 268, 269, 270, 275, 276, 279, 282, 283, 284, 286, 287, 288, 289, 291, 292, 294, 295, 297, 299, 301, 304, 316, 320, 322, 324, 325, 328, 329, 330, 332, 335, 336, 337, 339, 340, 342, 343, 345, 346, 349, 350, 351, 354, 355, 356, 358, 359, 362, 365, 366, 369, 370, 373, 374, 375, 378, 379, 380, 381, 384, 385, 388, 389, 390, 391, 393, 394, 395, 396, 398, 399, 401, 403, 406, 423, 426, 427, 428, 429, 430, 431, 432, 435, 436, 437, 438, 440, 441, 442, 446, 448, 449, 452, 453, 454, 455, 456, 458, 459, 462, 463, 466, 469, 471, 474, 476, 477, 478, 479, 480, 481, 482, 485, 486, 487, 488, 491, 492, 493, 494, 496, 499, 502, 504, 506, 507, 509, 510, 512, 515, 535, 537, 540, 541, 544, 548, 550, 552, 553, 555, 558, 561, 563, 565, 566, 569, 570, 573, 574, 576, 579, 596, 597, 633, 646, 649, 660, 661, 664, 675, 676, 679, 691], "summary": {"covered_lines": 261, "num_statements": 291, "percent_covered": 89.69072164948453, "percent_covered_display": "90", "missing_lines": 30, "excluded_lines": 0}, "missing_lines": [235, 236, 237, 241, 245, 246, 247, 317, 363, 424, 545, 556, 600, 602, 603, 606, 607, 609, 610, 612, 613, 616, 617, 619, 622, 623, 626, 627, 628, 630], "excluded_lines": [], "functions": {"Graph.__init__": {"executed_lines": [52, 53, 56, 59, 60, 63, 64, 65, 66, 67, 70, 73], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Graph.__bool__": {"executed_lines": [77], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Graph.__len__": {"executed_lines": [81], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Graph.size": {"executed_lines": [93], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Graph.depth": {"executed_lines": [109], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Graph._build_all_maps": {"executed_lines": [120, 121, 122, 124, 125, 126, 127, 128, 130, 132, 133, 134, 136, 137, 138, 140, 141, 142, 143], "summary": {"covered_lines": 19, "num_statements": 19, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Graph._assemble": {"executed_lines": [153, 154, 155, 156, 157, 158, 161, 162, 165, 166, 167, 168, 171, 172, 173, 174, 177, 179, 182, 183, 185, 186, 187, 189, 190, 191, 193, 195, 196, 198], "summary": {"covered_lines": 30, "num_statements": 30, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Graph._compute_depths_iterative": {"executed_lines": [213, 214, 215, 217, 218, 219, 222, 224, 225, 228, 230, 234, 240, 244, 250, 253, 259, 260, 261, 262, 265, 268, 269, 270, 275, 276, 279, 282, 283, 284, 286, 287, 288, 289, 291, 292, 294, 295, 297, 299, 301], "summary": {"covered_lines": 41, "num_statements": 48, "percent_covered": 85.41666666666667, "percent_covered_display": "85", "missing_lines": 7, "excluded_lines": 0}, "missing_lines": [235, 236, 237, 241, 245, 246, 247], "excluded_lines": []}, "Graph._process_loops": {"executed_lines": [316, 320, 322, 324, 325, 328, 329, 330, 332, 335, 336, 337, 339, 340, 342, 343, 345, 346, 349, 350, 351, 354, 355, 356, 358, 359, 362, 365, 366, 369, 370, 373, 374, 375, 378, 379, 380, 381, 384, 385, 388, 389, 390, 391, 393, 394, 395, 396, 398, 399, 401, 403], "summary": {"covered_lines": 52, "num_statements": 54, "percent_covered": 96.29629629629629, "percent_covered_display": "96", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [317, 363], "excluded_lines": []}, "Graph._find_strongly_connected_components": {"executed_lines": [423, 426, 427, 428, 429, 430, 431, 432, 435, 436, 437, 438, 440, 441, 442, 446, 448, 449, 452, 453, 454, 455, 456, 458, 459, 462, 463, 466, 469, 471, 474, 476, 477, 478, 479, 480, 481, 482, 485, 486, 487, 488, 491, 492, 493, 494, 496, 499, 502, 504, 506, 507, 509, 510, 512], "summary": {"covered_lines": 55, "num_statements": 56, "percent_covered": 98.21428571428571, "percent_covered_display": "98", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [424], "excluded_lines": []}, "Graph.is_algebraic_path": {"executed_lines": [535, 537, 540, 541, 544, 548, 550, 552, 553, 555, 558, 561, 563, 565, 566, 569, 570, 573, 574, 576], "summary": {"covered_lines": 20, "num_statements": 22, "percent_covered": 90.9090909090909, "percent_covered_display": "91", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [545, 556], "excluded_lines": []}, "Graph._has_algebraic_self_loop": {"executed_lines": [596, 597], "summary": {"covered_lines": 2, "num_statements": 20, "percent_covered": 10.0, "percent_covered_display": "10", "missing_lines": 18, "excluded_lines": 0}, "missing_lines": [600, 602, 603, 606, 607, 609, 610, 612, 613, 616, 617, 619, 622, 623, 626, 627, 628, 630], "excluded_lines": []}, "Graph.outgoing_connections": {"executed_lines": [646], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Graph.dag": {"executed_lines": [660, 661], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Graph.loop": {"executed_lines": [675, 676], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Graph.loop_closing_connections": {"executed_lines": [691], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [9, 14, 15, 51, 76, 80, 84, 85, 96, 97, 112, 146, 201, 304, 406, 515, 579, 633, 649, 664, 679], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Graph": {"executed_lines": [52, 53, 56, 59, 60, 63, 64, 65, 66, 67, 70, 73, 77, 81, 93, 109, 120, 121, 122, 124, 125, 126, 127, 128, 130, 132, 133, 134, 136, 137, 138, 140, 141, 142, 143, 153, 154, 155, 156, 157, 158, 161, 162, 165, 166, 167, 168, 171, 172, 173, 174, 177, 179, 182, 183, 185, 186, 187, 189, 190, 191, 193, 195, 196, 198, 213, 214, 215, 217, 218, 219, 222, 224, 225, 228, 230, 234, 240, 244, 250, 253, 259, 260, 261, 262, 265, 268, 269, 270, 275, 276, 279, 282, 283, 284, 286, 287, 288, 289, 291, 292, 294, 295, 297, 299, 301, 316, 320, 322, 324, 325, 328, 329, 330, 332, 335, 336, 337, 339, 340, 342, 343, 345, 346, 349, 350, 351, 354, 355, 356, 358, 359, 362, 365, 366, 369, 370, 373, 374, 375, 378, 379, 380, 381, 384, 385, 388, 389, 390, 391, 393, 394, 395, 396, 398, 399, 401, 403, 423, 426, 427, 428, 429, 430, 431, 432, 435, 436, 437, 438, 440, 441, 442, 446, 448, 449, 452, 453, 454, 455, 456, 458, 459, 462, 463, 466, 469, 471, 474, 476, 477, 478, 479, 480, 481, 482, 485, 486, 487, 488, 491, 492, 493, 494, 496, 499, 502, 504, 506, 507, 509, 510, 512, 535, 537, 540, 541, 544, 548, 550, 552, 553, 555, 558, 561, 563, 565, 566, 569, 570, 573, 574, 576, 596, 597, 646, 660, 661, 675, 676, 691], "summary": {"covered_lines": 241, "num_statements": 271, "percent_covered": 88.92988929889299, "percent_covered_display": "89", "missing_lines": 30, "excluded_lines": 0}, "missing_lines": [235, 236, 237, 241, 245, 246, 247, 317, 363, 424, 545, 556, 600, 602, 603, 606, 607, 609, 610, 612, 613, 616, 617, 619, 622, 623, 626, 627, 628, 630], "excluded_lines": []}, "": {"executed_lines": [9, 14, 15, 51, 76, 80, 84, 85, 96, 97, 112, 146, 201, 304, 406, 515, 579, 633, 649, 664, 679], "summary": {"covered_lines": 20, "num_statements": 20, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\utils\\portreference.py": {"executed_lines": [10, 15, 16, 31, 34, 37, 40, 41, 43, 46, 47, 50, 51, 54, 55, 58, 59, 61, 62, 65, 67, 70, 74, 75, 79, 83, 84, 88, 97, 98, 101, 109, 112, 120, 121, 124, 132, 135, 143, 144, 147, 149], "summary": {"covered_lines": 41, "num_statements": 43, "percent_covered": 95.34883720930233, "percent_covered_display": "95", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [76, 85], "excluded_lines": [], "functions": {"PortReference.__init__": {"executed_lines": [37, 40, 41, 43, 46, 47, 50, 51, 54, 55, 58, 59, 61, 62], "summary": {"covered_lines": 14, "num_statements": 14, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PortReference.__len__": {"executed_lines": [67], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PortReference._validate_input_ports": {"executed_lines": [74, 75], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [76], "excluded_lines": []}, "PortReference._validate_output_ports": {"executed_lines": [83, 84], "summary": {"covered_lines": 2, "num_statements": 3, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 1, "excluded_lines": 0}, "missing_lines": [85], "excluded_lines": []}, "PortReference.to": {"executed_lines": [97, 98], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PortReference.get_inputs": {"executed_lines": [109], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PortReference.set_inputs": {"executed_lines": [120, 121], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PortReference.get_outputs": {"executed_lines": [132], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PortReference.set_outputs": {"executed_lines": [143, 144], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "PortReference.to_dict": {"executed_lines": [149], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 15, 16, 31, 34, 65, 70, 79, 88, 101, 112, 124, 135, 147], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"PortReference": {"executed_lines": [37, 40, 41, 43, 46, 47, 50, 51, 54, 55, 58, 59, 61, 62, 67, 74, 75, 83, 84, 97, 98, 109, 120, 121, 132, 143, 144, 149], "summary": {"covered_lines": 28, "num_statements": 30, "percent_covered": 93.33333333333333, "percent_covered_display": "93", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [76, 85], "excluded_lines": []}, "": {"executed_lines": [10, 15, 16, 31, 34, 65, 70, 79, 88, 101, 112, 124, 135, 147], "summary": {"covered_lines": 13, "num_statements": 13, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\utils\\progresstracker.py": {"executed_lines": [12, 13, 14, 15, 16, 18, 23, 24, 79, 91, 93, 95, 98, 99, 100, 101, 102, 103, 106, 109, 112, 115, 116, 117, 118, 119, 120, 123, 126, 129, 133, 136, 138, 145, 148, 151, 152, 166, 167, 170, 175, 180, 182, 187, 188, 190, 193, 194, 196, 201, 209, 210, 213, 230, 231, 236, 245, 250, 251, 256, 259, 262, 265, 268, 269, 270, 271, 273, 274, 280, 283, 300, 304, 310, 311, 312, 315, 318, 321, 324, 327, 329, 332, 333, 336, 339, 340, 346, 347, 349, 351, 353, 358, 373, 374, 377, 378, 379, 382, 389, 392, 395, 398, 400, 403, 404, 407, 410, 412, 414, 415, 418, 419, 421, 422, 424, 427, 429, 432, 433, 434, 438, 439, 440, 451, 452, 455, 456, 459, 460, 464, 471, 474, 477, 480, 483, 485], "summary": {"covered_lines": 136, "num_statements": 154, "percent_covered": 88.31168831168831, "percent_covered_display": "88", "missing_lines": 18, "excluded_lines": 0}, "missing_lines": [92, 94, 96, 171, 172, 176, 246, 247, 301, 302, 306, 307, 442, 444, 448, 453, 461, 487], "excluded_lines": [], "functions": {"ProgressTracker.__init__": {"executed_lines": [91, 93, 95, 98, 99, 100, 101, 102, 103, 106, 109, 112, 115, 116, 117, 118, 119, 120, 123, 126, 129, 133, 136, 138, 145, 148], "summary": {"covered_lines": 26, "num_statements": 29, "percent_covered": 89.65517241379311, "percent_covered_display": "90", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [92, 94, 96], "excluded_lines": []}, "ProgressTracker._has_configured_handler": {"executed_lines": [166, 167, 170, 175, 180, 182], "summary": {"covered_lines": 6, "num_statements": 9, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [171, 172, 176], "excluded_lines": []}, "ProgressTracker.current_progress": {"executed_lines": [196], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ProgressTracker.__enter__": {"executed_lines": [209, 210], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ProgressTracker.__exit__": {"executed_lines": [230, 231], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ProgressTracker.__iter__": {"executed_lines": [245, 250, 251], "summary": {"covered_lines": 3, "num_statements": 5, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [246, 247], "excluded_lines": []}, "ProgressTracker.interrupt": {"executed_lines": [259], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ProgressTracker.start": {"executed_lines": [265, 268, 269, 270, 271, 273, 274, 280], "summary": {"covered_lines": 8, "num_statements": 8, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ProgressTracker.update": {"executed_lines": [300, 304, 310, 311, 312, 315, 318, 321], "summary": {"covered_lines": 8, "num_statements": 12, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 4, "excluded_lines": 0}, "missing_lines": [301, 302, 306, 307], "excluded_lines": []}, "ProgressTracker.close": {"executed_lines": [327, 329, 332, 333, 336, 339, 340, 346, 347, 349, 351, 353], "summary": {"covered_lines": 12, "num_statements": 12, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ProgressTracker._format_time": {"executed_lines": [373, 374, 377, 378, 379], "summary": {"covered_lines": 5, "num_statements": 5, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "ProgressTracker._log_progress": {"executed_lines": [389, 392, 395, 398, 400, 403, 404, 407, 410, 412, 414, 415, 418, 419, 421, 422, 424, 427, 429, 432, 433, 434, 438, 439, 440, 451, 452, 455, 456, 459, 460, 464, 471, 474, 477, 480, 483, 485], "summary": {"covered_lines": 38, "num_statements": 44, "percent_covered": 86.36363636363636, "percent_covered_display": "86", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [442, 444, 448, 453, 461, 487], "excluded_lines": []}, "": {"executed_lines": [12, 13, 14, 15, 16, 18, 23, 24, 79, 151, 152, 187, 188, 193, 194, 201, 213, 236, 256, 262, 283, 324, 358, 382], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"ProgressTracker": {"executed_lines": [91, 93, 95, 98, 99, 100, 101, 102, 103, 106, 109, 112, 115, 116, 117, 118, 119, 120, 123, 126, 129, 133, 136, 138, 145, 148, 166, 167, 170, 175, 180, 182, 190, 196, 209, 210, 230, 231, 245, 250, 251, 259, 265, 268, 269, 270, 271, 273, 274, 280, 300, 304, 310, 311, 312, 315, 318, 321, 327, 329, 332, 333, 336, 339, 340, 346, 347, 349, 351, 353, 373, 374, 377, 378, 379, 389, 392, 395, 398, 400, 403, 404, 407, 410, 412, 414, 415, 418, 419, 421, 422, 424, 427, 429, 432, 433, 434, 438, 439, 440, 451, 452, 455, 456, 459, 460, 464, 471, 474, 477, 480, 483, 485], "summary": {"covered_lines": 113, "num_statements": 131, "percent_covered": 86.25954198473282, "percent_covered_display": "86", "missing_lines": 18, "excluded_lines": 0}, "missing_lines": [92, 94, 96, 171, 172, 176, 246, 247, 301, 302, 306, 307, 442, 444, 448, 453, 461, 487], "excluded_lines": []}, "": {"executed_lines": [12, 13, 14, 15, 16, 18, 23, 24, 79, 151, 152, 187, 188, 193, 194, 201, 213, 236, 256, 262, 283, 324, 358, 382], "summary": {"covered_lines": 23, "num_statements": 23, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\utils\\realtimeplotter.py": {"executed_lines": [12, 13, 14, 16, 18, 19, 21, 27, 28, 52, 55, 56, 57, 58, 59, 62, 69, 72, 73, 74, 77, 78, 81, 84, 87, 90, 91, 94, 97, 109, 113, 116, 117, 120, 121, 122, 125, 126, 129, 130, 140, 143, 155, 159, 162, 165, 168, 171, 175, 176, 177, 199, 200, 203, 204, 205, 212, 213, 217, 220, 235, 236, 239, 240, 243, 246, 250, 258], "summary": {"covered_lines": 67, "num_statements": 102, "percent_covered": 65.68627450980392, "percent_covered_display": "66", "missing_lines": 35, "excluded_lines": 0}, "missing_lines": [110, 133, 134, 135, 137, 138, 156, 169, 182, 184, 187, 188, 190, 194, 195, 196, 207, 208, 209, 214, 215, 223, 224, 227, 228, 231, 232, 247, 248, 251, 252, 253, 254, 255, 256], "excluded_lines": [], "functions": {"RealtimePlotter.__init__": {"executed_lines": [55, 56, 57, 58, 59, 62, 69, 72, 73, 74, 77, 78, 81, 84, 87, 90, 91, 94], "summary": {"covered_lines": 18, "num_statements": 18, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RealtimePlotter.update_all": {"executed_lines": [109, 113, 116, 117, 120, 121, 122, 125, 126, 129, 130, 140], "summary": {"covered_lines": 12, "num_statements": 18, "percent_covered": 66.66666666666667, "percent_covered_display": "67", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [110, 133, 134, 135, 137, 138], "excluded_lines": []}, "RealtimePlotter.update": {"executed_lines": [155, 159, 162, 165, 168, 171, 175, 176, 177, 199, 200, 203, 204, 205, 212, 213, 217], "summary": {"covered_lines": 17, "num_statements": 32, "percent_covered": 53.125, "percent_covered_display": "53", "missing_lines": 15, "excluded_lines": 0}, "missing_lines": [156, 169, 182, 184, 187, 188, 190, 194, 195, 196, 207, 208, 209, 214, 215], "excluded_lines": []}, "RealtimePlotter._update_plot": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [223, 224, 227, 228, 231, 232], "excluded_lines": []}, "RealtimePlotter.show": {"executed_lines": [236], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RealtimePlotter.on_close": {"executed_lines": [240], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "RealtimePlotter._setup_legend_picking": {"executed_lines": [246, 250, 258], "summary": {"covered_lines": 3, "num_statements": 5, "percent_covered": 60.0, "percent_covered_display": "60", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [247, 248], "excluded_lines": []}, "RealtimePlotter._setup_legend_picking.on_pick": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 6, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 6, "excluded_lines": 0}, "missing_lines": [251, 252, 253, 254, 255, 256], "excluded_lines": []}, "": {"executed_lines": [12, 13, 14, 16, 18, 19, 21, 27, 28, 52, 97, 143, 220, 235, 239, 243], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"RealtimePlotter": {"executed_lines": [55, 56, 57, 58, 59, 62, 69, 72, 73, 74, 77, 78, 81, 84, 87, 90, 91, 94, 109, 113, 116, 117, 120, 121, 122, 125, 126, 129, 130, 140, 155, 159, 162, 165, 168, 171, 175, 176, 177, 199, 200, 203, 204, 205, 212, 213, 217, 236, 240, 246, 250, 258], "summary": {"covered_lines": 52, "num_statements": 87, "percent_covered": 59.770114942528735, "percent_covered_display": "60", "missing_lines": 35, "excluded_lines": 0}, "missing_lines": [110, 133, 134, 135, 137, 138, 156, 169, 182, 184, 187, 188, 190, 194, 195, 196, 207, 208, 209, 214, 215, 223, 224, 227, 228, 231, 232, 247, 248, 251, 252, 253, 254, 255, 256], "excluded_lines": []}, "": {"executed_lines": [12, 13, 14, 16, 18, 19, 21, 27, 28, 52, 97, 143, 220, 235, 239, 243], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\utils\\register.py": {"executed_lines": [10, 12, 17, 18, 40, 43, 44, 45, 46, 48, 50, 53, 54, 55, 58, 71, 74, 76, 77, 80, 82, 83, 86, 102, 105, 120, 121, 122, 124, 125, 126, 127, 130, 143, 146, 157, 158, 159, 160, 163, 177, 178], "summary": {"covered_lines": 41, "num_statements": 41, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": [], "functions": {"Register.__init__": {"executed_lines": [44, 45, 46], "summary": {"covered_lines": 3, "num_statements": 3, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Register.__len__": {"executed_lines": [50], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Register.__iter__": {"executed_lines": [54, 55], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Register._map": {"executed_lines": [71], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Register.reset": {"executed_lines": [76, 77], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Register.clear": {"executed_lines": [82, 83], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Register.to_array": {"executed_lines": [102], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Register.update_from_array": {"executed_lines": [120, 121, 122, 124, 125, 126, 127], "summary": {"covered_lines": 7, "num_statements": 7, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Register.__contains__": {"executed_lines": [143], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Register.__setitem__": {"executed_lines": [157, 158, 159, 160], "summary": {"covered_lines": 4, "num_statements": 4, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Register.__getitem__": {"executed_lines": [177, 178], "summary": {"covered_lines": 2, "num_statements": 2, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 17, 18, 40, 43, 48, 53, 58, 74, 80, 86, 105, 130, 146, 163], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Register": {"executed_lines": [44, 45, 46, 50, 54, 55, 71, 76, 77, 82, 83, 102, 120, 121, 122, 124, 125, 126, 127, 143, 157, 158, 159, 160, 177, 178], "summary": {"covered_lines": 26, "num_statements": 26, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "": {"executed_lines": [10, 12, 17, 18, 40, 43, 48, 53, 58, 74, 80, 86, 105, 130, 146, 163], "summary": {"covered_lines": 15, "num_statements": 15, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}}, "src\\pathsim\\utils\\serialization.py": {"executed_lines": [12, 14, 15, 16, 17, 18, 19, 20, 25, 83, 146, 161, 162, 165, 174, 183, 184, 187, 189, 191, 193, 194, 199, 202, 205, 209, 210, 226, 227, 232, 233, 236, 250, 251, 271, 272, 288, 289, 291, 294, 295, 296, 306, 310, 313, 314, 315, 318, 322, 326, 329, 333, 334, 337, 338, 350, 359, 360], "summary": {"covered_lines": 57, "num_statements": 131, "percent_covered": 43.51145038167939, "percent_covered_display": "44", "missing_lines": 74, "excluded_lines": 0}, "missing_lines": [40, 41, 48, 52, 54, 55, 56, 57, 60, 61, 66, 67, 70, 76, 98, 99, 100, 103, 106, 109, 110, 111, 114, 115, 122, 123, 131, 137, 166, 167, 168, 169, 170, 171, 175, 176, 177, 178, 179, 180, 196, 197, 206, 212, 214, 216, 218, 221, 246, 247, 266, 267, 268, 297, 298, 303, 307, 319, 340, 342, 345, 346, 348, 364, 366, 367, 370, 371, 373, 374, 377, 378, 379, 381], "excluded_lines": [], "functions": {"serialize_callable": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 14, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 14, "excluded_lines": 0}, "missing_lines": [40, 41, 48, 52, 54, 55, 56, 57, 60, 61, 66, 67, 70, 76], "excluded_lines": []}, "serialize_object": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 14, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 14, "excluded_lines": 0}, "missing_lines": [98, 99, 100, 103, 106, 109, 110, 111, 114, 115, 122, 123, 131, 137], "excluded_lines": []}, "deserialize": {"executed_lines": [161, 162, 165, 174, 183, 184, 187, 189, 191, 193, 194, 199, 202, 205, 209, 210], "summary": {"covered_lines": 16, "num_statements": 36, "percent_covered": 44.44444444444444, "percent_covered_display": "44", "missing_lines": 20, "excluded_lines": 0}, "missing_lines": [166, 167, 168, 169, 170, 171, 175, 176, 177, 178, 179, 180, 196, 197, 206, 212, 214, 216, 218, 221], "excluded_lines": []}, "Serializable.__str__": {"executed_lines": [233], "summary": {"covered_lines": 1, "num_statements": 1, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}, "Serializable.save": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 2, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 2, "excluded_lines": 0}, "missing_lines": [246, 247], "excluded_lines": []}, "Serializable.load": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 3, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 3, "excluded_lines": 0}, "missing_lines": [266, 267, 268], "excluded_lines": []}, "Serializable.from_dict": {"executed_lines": [288, 289, 291, 294, 295, 296, 306, 310, 313, 314, 315, 318, 322, 326], "summary": {"covered_lines": 14, "num_statements": 19, "percent_covered": 73.6842105263158, "percent_covered_display": "74", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [297, 298, 303, 307, 319], "excluded_lines": []}, "Serializable.to_dict": {"executed_lines": [333, 334, 337, 338, 350], "summary": {"covered_lines": 5, "num_statements": 10, "percent_covered": 50.0, "percent_covered_display": "50", "missing_lines": 5, "excluded_lines": 0}, "missing_lines": [340, 342, 345, 346, 348], "excluded_lines": []}, "Serializable._find_class": {"executed_lines": [], "summary": {"covered_lines": 0, "num_statements": 11, "percent_covered": 0.0, "percent_covered_display": "0", "missing_lines": 11, "excluded_lines": 0}, "missing_lines": [364, 366, 367, 370, 371, 373, 374, 377, 378, 379, 381], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 16, 17, 18, 19, 20, 25, 83, 146, 226, 227, 232, 236, 250, 251, 271, 272, 329, 359, 360], "summary": {"covered_lines": 21, "num_statements": 21, "percent_covered": 100.0, "percent_covered_display": "100", "missing_lines": 0, "excluded_lines": 0}, "missing_lines": [], "excluded_lines": []}}, "classes": {"Serializable": {"executed_lines": [233, 288, 289, 291, 294, 295, 296, 306, 310, 313, 314, 315, 318, 322, 326, 333, 334, 337, 338, 350], "summary": {"covered_lines": 20, "num_statements": 46, "percent_covered": 43.47826086956522, "percent_covered_display": "43", "missing_lines": 26, "excluded_lines": 0}, "missing_lines": [246, 247, 266, 267, 268, 297, 298, 303, 307, 319, 340, 342, 345, 346, 348, 364, 366, 367, 370, 371, 373, 374, 377, 378, 379, 381], "excluded_lines": []}, "": {"executed_lines": [12, 14, 15, 16, 17, 18, 19, 20, 25, 83, 146, 161, 162, 165, 174, 183, 184, 187, 189, 191, 193, 194, 199, 202, 205, 209, 210, 226, 227, 232, 236, 250, 251, 271, 272, 329, 359, 360], "summary": {"covered_lines": 37, "num_statements": 85, "percent_covered": 43.529411764705884, "percent_covered_display": "44", "missing_lines": 48, "excluded_lines": 0}, "missing_lines": [40, 41, 48, 52, 54, 55, 56, 57, 60, 61, 66, 67, 70, 76, 98, 99, 100, 103, 106, 109, 110, 111, 114, 115, 122, 123, 131, 137, 166, 167, 168, 169, 170, 171, 175, 176, 177, 178, 179, 180, 196, 197, 206, 212, 214, 216, 218, 221], "excluded_lines": []}}}}, "totals": {"covered_lines": 3924, "num_statements": 4682, "percent_covered": 83.81033746262281, "percent_covered_display": "84", "missing_lines": 758, "excluded_lines": 0}} \ No newline at end of file diff --git a/docs/source/_static/redirect.js b/docs/source/_static/redirect.js new file mode 100644 index 00000000..93887caa --- /dev/null +++ b/docs/source/_static/redirect.js @@ -0,0 +1,8 @@ +// Redirect visitors from RTD to docs.pathsim.org after a brief delay. +// The banner is shown immediately; redirect fires after 3 seconds +// so users understand what's happening. Click the link to go immediately. +(function () { + if (window.location.hostname.indexOf('readthedocs') === -1) return; + var target = 'https://docs.pathsim.org'; + setTimeout(function () { window.location.replace(target); }, 3000); +})(); diff --git a/docs/source/conf.py b/docs/source/conf.py index 49d68f97..1fc03c44 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -40,6 +40,8 @@ html_favicon = "logos/pathsim_icon.png" html_title = "PathSim Documentation" html_css_files = ['custom.css'] # Add custom CSS for link previews and styling +html_js_files = ['redirect.js'] # Auto-redirect RTD visitors to docs.pathsim.org +html_baseurl = 'https://docs.pathsim.org/' # Canonical URL for SEO html_theme_options = { "light_css_variables": { diff --git a/docs/source/examples/algebraic_loop.ipynb b/docs/source/examples/algebraic_loop.ipynb index 00623f4a..c25a6c78 100644 --- a/docs/source/examples/algebraic_loop.ipynb +++ b/docs/source/examples/algebraic_loop.ipynb @@ -3,11 +3,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "# Algebraic Loop\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_algebraicloop.py).This example demonstrates PathSim's ability to handle **algebraic loops** - situations where a signal feeds back on itself instantaneously without any dynamic element (integrator, delay, etc.) in the loop.\n" - ] + "source": "# Algebraic Loop\n\nDemonstration of PathSim's automatic handling of algebraic loops.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_algebraicloop.py)." }, { "cell_type": "raw", @@ -365,4 +361,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/billards.ipynb b/docs/source/examples/billards.ipynb index 9e197191..10d4b648 100644 --- a/docs/source/examples/billards.ipynb +++ b/docs/source/examples/billards.ipynb @@ -10,13 +10,7 @@ }, "tags": [] }, - "source": [ - "# Billards & Collisions\n", - "\n", - "You can also find this example as a standalone Python file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_event/example_billards_sphere.py).\n", - "\n", - "This example demonstrates how to simulate a ball bouncing inside a circular boundary using PathSim's event detection system. The ball is subject to gravity and bounces elastically off the circular wall. This showcases the use of zero-crossing events to detect and handle collisions." - ] + "source": "# Billards & Collisions\n\nSimulation of a ball bouncing inside a circular boundary using event detection.\n\nYou can also find this example as a standalone Python file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_event/example_billards_sphere.py)." }, { "cell_type": "raw", @@ -439,4 +433,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/source/examples/bouncing_ball.ipynb b/docs/source/examples/bouncing_ball.ipynb index c872bc21..1a04e397 100644 --- a/docs/source/examples/bouncing_ball.ipynb +++ b/docs/source/examples/bouncing_ball.ipynb @@ -12,13 +12,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "# Bouncing Ball\n", - "\n", - "This example demonstrates PathSim's event handling system. The classical example here is the bouncing ball. The ball accelerates downward due to gravity and once it hits the floor, it bounces back. The dynamics of this system are discontinuous due to the jump in velocity (sign change) exactly at the bounce. This is called a discrete event, specifically a zero crossing event.\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_event/example_bouncingball.py)." - ] + "source": "# Bouncing Ball\n\nSimulation of a bouncing ball using PathSim's event handling system.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_event/example_bouncingball.py)." }, { "cell_type": "raw", @@ -411,4 +405,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/cascade_controller.ipynb b/docs/source/examples/cascade_controller.ipynb index 67179799..0e48c715 100644 --- a/docs/source/examples/cascade_controller.ipynb +++ b/docs/source/examples/cascade_controller.ipynb @@ -3,13 +3,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "# Cascade Controller\n", - "\n", - "In this example we demonstrate a cascade control architecture with a two-loop PID control system. Cascade control is widely used in process control when a fast inner loop can help improve the performance of a slower outer loop.\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_cascade.py)." - ] + "source": "# Cascade Controller\n\nDemonstration of a two-loop cascade PID control system with inner and outer loops.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_cascade.py)." }, { "cell_type": "markdown", @@ -313,4 +307,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/checkpoints.ipynb b/docs/source/examples/checkpoints.ipynb new file mode 100644 index 00000000..7d7f4eb7 --- /dev/null +++ b/docs/source/examples/checkpoints.ipynb @@ -0,0 +1,308 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Checkpoints\n", + "\n", + "PathSim supports saving and loading simulation state via checkpoints. This allows you to pause a simulation, save its complete state to disk, and resume it later from exactly where you left off.\n", + "\n", + "Checkpoints also enable **rollback** — returning to a saved state and exploring different what-if scenarios by changing parameters.\n", + "\n", + "Checkpoints use a split format: a JSON file for metadata and structure, and an NPZ file for numerical data (block states, solver histories, etc.)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Building the System\n", + "\n", + "We'll use the coupled oscillators system to demonstrate checkpoints. The energy exchange between the two oscillators produces a sustained, non-trivial response that makes it easy to visually verify checkpoint continuity." + ] + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "First let's import the :class:`.Simulation` and :class:`.Connection` classes and the required blocks:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from pathsim import Simulation, Connection\n", + "from pathsim.blocks import ODE, Function, Scope" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define the system parameters:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Mass parameters\n", + "m1 = 1.0\n", + "m2 = 1.5\n", + "\n", + "# Spring constants\n", + "k1 = 2.0\n", + "k2 = 3.0\n", + "k12 = 0.5 # coupling spring\n", + "\n", + "# Damping coefficients\n", + "c1 = 0.02\n", + "c2 = 0.03\n", + "\n", + "# Initial conditions [position, velocity]\n", + "x1_0 = np.array([2.0, 0.0]) # oscillator 1 displaced\n", + "x2_0 = np.array([0.0, 0.0]) # oscillator 2 at rest" + ] + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "Define the differential equations for each oscillator using :class:`.ODE` blocks and the coupling force using a :class:`.Function` block:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Oscillator 1: m1*x1'' = -k1*x1 - c1*x1' - k12*(x1 - x2)\n", + "def osc1_func(x1, u, t):\n", + " f_e = u[0]\n", + " return np.array([x1[1], (-k1*x1[0] - c1*x1[1] - f_e) / m1])\n", + "\n", + "# Oscillator 2: m2*x2'' = -k2*x2 - c2*x2' + k12*(x1 - x2)\n", + "def osc2_func(x2, u, t):\n", + " f_e = u[0]\n", + " return np.array([x2[1], (-k2*x2[0] - c2*x2[1] - f_e) / m2])\n", + "\n", + "# Coupling force\n", + "def coupling_func(x1, x2):\n", + " f = k12 * (x1 - x2)\n", + " return f, -f\n", + "\n", + "# Blocks\n", + "osc1 = ODE(osc1_func, x1_0)\n", + "osc2 = ODE(osc2_func, x2_0)\n", + "fn = Function(coupling_func)\n", + "sc = Scope(labels=[r\"$x_1(t)$ - Oscillator 1\", r\"$x_2(t)$ - Oscillator 2\"])\n", + "\n", + "blocks = [osc1, osc2, fn, sc]\n", + "\n", + "# Connections\n", + "connections = [\n", + " Connection(osc1[0], fn[0], sc[0]),\n", + " Connection(osc2[0], fn[1], sc[1]),\n", + " Connection(fn[0], osc1[0]),\n", + " Connection(fn[1], osc2[0]),\n", + "]" + ] + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "Create the :class:`.Simulation` and run for 60 seconds:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sim = Simulation(blocks, connections, dt=0.01)\n", + "\n", + "sim.run(60)\n", + "\n", + "fig, ax = sc.plot()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The two oscillators exchange energy through the coupling spring, producing a characteristic beat pattern." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Saving a Checkpoint\n", + "\n", + "Now let's save the simulation state at t=60s. This creates two files: `coupled.json` (metadata) and `coupled.npz` (numerical data)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sim.save_checkpoint(\"coupled\")\n", + "print(f\"Checkpoint saved at t = {sim.time:.1f}s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can inspect the JSON file to see what was saved:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "with open(\"coupled.json\") as f:\n", + " cp = json.load(f)\n", + "\n", + "print(f\"PathSim version: {cp['pathsim_version']}\")\n", + "print(f\"Simulation time: {cp['simulation']['time']:.1f}s\")\n", + "print(f\"Solver: {cp['simulation']['solver']}\")\n", + "print(f\"Blocks saved:\")\n", + "for b in cp[\"blocks\"]:\n", + " print(f\" {b['_key']} ({b['type']})\")" + ] + }, + { + "cell_type": "raw", + "metadata": {}, + "source": [ + "Blocks are identified by type and insertion order (``ODE_0``, ``ODE_1``, etc.), so the checkpoint can be loaded into any simulation with the same block structure, regardless of the specific Python objects." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Rollback: What-If Scenarios\n", + "\n", + "This is where checkpoints really shine. We'll load the same checkpoint three times with different coupling strengths to explore how the system evolves from the exact same state.\n", + "\n", + "Since the checkpoint restores all block states by type and insertion order, we just need to rebuild the simulation with the same block structure but different parameters." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "def run_scenario(k12_new, duration=60):\n", + " \"\"\"Load checkpoint and continue with a different coupling constant.\"\"\"\n", + " def coupling_new(x1, x2):\n", + " f = k12_new * (x1 - x2)\n", + " return f, -f\n", + "\n", + " o1 = ODE(osc1_func, x1_0)\n", + " o2 = ODE(osc2_func, x2_0)\n", + " f = Function(coupling_new)\n", + " s = Scope()\n", + "\n", + " sim = Simulation(\n", + " [o1, o2, f, s],\n", + " [Connection(o1[0], f[0], s[0]),\n", + " Connection(o2[0], f[1], s[1]),\n", + " Connection(f[0], o1[0]),\n", + " Connection(f[1], o2[0])],\n", + " dt=0.01\n", + " )\n", + " sim.load_checkpoint(\"coupled\")\n", + " sim.run(duration)\n", + " return s.read()\n", + "\n", + "# Original coupling (continuation)\n", + "t_a, d_a = run_scenario(k12_new=0.5)\n", + "\n", + "# Stronger coupling\n", + "t_b, d_b = run_scenario(k12_new=2.0)\n", + "\n", + "# Decoupled\n", + "t_c, d_c = run_scenario(k12_new=0.0)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Comparing the Scenarios\n", + "\n", + "The plot shows the original run (0-60s) followed by three different futures branching from the checkpoint at t=60s. We show oscillator 1 for clarity." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "time_orig, data_orig = sc.read()\n", + "\n", + "fig, ax = plt.subplots(figsize=(10, 4))\n", + "\n", + "# Original run\n", + "ax.plot(time_orig, data_orig[0], \"k-\", lw=1.5, label=r\"original ($k_{12}=0.5$)\")\n", + "\n", + "# Three futures from checkpoint\n", + "ax.plot(t_a, d_a[0], \"C0-\", alpha=0.8, label=r\"continued ($k_{12}=0.5$)\")\n", + "ax.plot(t_b, d_b[0], \"C1-\", alpha=0.8, label=r\"stronger coupling ($k_{12}=2.0$)\")\n", + "ax.plot(t_c, d_c[0], \"C2-\", alpha=0.8, label=r\"decoupled ($k_{12}=0$)\")\n", + "\n", + "ax.axvline(60, color=\"gray\", ls=\":\", lw=2, alpha=0.5, label=\"checkpoint\")\n", + "ax.set_xlabel(\"time [s]\")\n", + "ax.set_ylabel(r\"$x_1(t)$\")\n", + "ax.set_title(\"Checkpoint Rollback: Three Futures from the Same State\")\n", + "ax.legend(loc=\"upper right\", fontsize=8)\n", + "fig.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "All three scenarios start from the exact same state at t=60s. The blue continuation matches the original trajectory perfectly, confirming checkpoint fidelity. The stronger coupling (orange) produces faster energy exchange, while the decoupled system (green) oscillates independently at its natural frequency." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/docs/source/examples/chemical_reactor.ipynb b/docs/source/examples/chemical_reactor.ipynb index b0c4dfe3..9f3d5c7a 100644 --- a/docs/source/examples/chemical_reactor.ipynb +++ b/docs/source/examples/chemical_reactor.ipynb @@ -3,44 +3,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "# Chemical Reactor \n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_reactor.py).This example demonstrates a **Continuous Stirred Tank Reactor (CSTR)** with consecutive exothermic reactions. This is a classic **stiff ODE system** that requires specialized solvers for efficient simulation.\n", - "\n", - "## Chemical Reaction System\n", - "\n", - "The reactor contains two consecutive reactions:\n", - "\n", - "$$A \\xrightarrow{k_1} B \\xrightarrow{k_2} C$$\n", - "\n", - "Both reactions are:\n", - "- **Exothermic** (release heat)\n", - "- **Temperature-dependent** (Arrhenius kinetics)\n", - "- **Highly nonlinear**\n", - "\n", - "## Mathematical Model\n", - "\n", - "The system is described by three ODEs:\n", - "\n", - "### Concentration of A:\n", - "$$\\frac{dC_A}{dt} = \\frac{C_{A,in} - C_A}{\\tau} - k_1 e^{-E_1/(RT)} C_A$$\n", - "\n", - "### Concentration of B:\n", - "$$\\frac{dC_B}{dt} = \\frac{-C_B}{\\tau} + k_1 e^{-E_1/(RT)} C_A - k_2 e^{-E_2/(RT)} C_B$$\n", - "\n", - "### Reactor Temperature:\n", - "$$\\frac{dT}{dt} = \\frac{T_{in} - T}{\\tau} + \\frac{-\\Delta H_1}{\\rho C_p} k_1 e^{-E_1/(RT)} C_A + \\frac{-\\Delta H_2}{\\rho C_p} k_2 e^{-E_2/(RT)} C_B - \\frac{UA(T-T_c)}{V\\rho C_p}$$\n", - "\n", - "## Why is this System Stiff?\n", - "\n", - "The system is **stiff** because:\n", - "1. **Exponential temperature dependence** creates vastly different timescales\n", - "2. **Fast reactions** (high $k$ values) vs. **slow heat transfer**\n", - "3. **Tight coupling** between concentration and temperature\n", - "\n", - "Standard explicit solvers would require extremely small timesteps. We use **GEAR52A**, an implicit solver designed for stiff systems." - ] + "source": "# Chemical Reactor \n\nSimulation of a continuous stirred tank reactor (CSTR) with consecutive exothermic reactions.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_reactor.py).\n\n## Chemical Reaction System\n\nThe reactor contains two consecutive reactions:\n\n$$A \\xrightarrow{k_1} B \\xrightarrow{k_2} C$$\n\nBoth reactions are:\n- **Exothermic** (release heat)\n- **Temperature-dependent** (Arrhenius kinetics)\n- **Highly nonlinear**\n\n## Mathematical Model\n\nThe system is described by three ODEs:\n\n### Concentration of A:\n$$\\frac{dC_A}{dt} = \\frac{C_{A,in} - C_A}{\\tau} - k_1 e^{-E_1/(RT)} C_A$$\n\n### Concentration of B:\n$$\\frac{dC_B}{dt} = \\frac{-C_B}{\\tau} + k_1 e^{-E_1/(RT)} C_A - k_2 e^{-E_2/(RT)} C_B$$\n\n### Reactor Temperature:\n$$\\frac{dT}{dt} = \\frac{T_{in} - T}{\\tau} + \\frac{-\\Delta H_1}{\\rho C_p} k_1 e^{-E_1/(RT)} C_A + \\frac{-\\Delta H_2}{\\rho C_p} k_2 e^{-E_2/(RT)} C_B - \\frac{UA(T-T_c)}{V\\rho C_p}$$\n\n## Why is this System Stiff?\n\nThe system is **stiff** because:\n1. **Exponential temperature dependence** creates vastly different timescales\n2. **Fast reactions** (high $k$ values) vs. **slow heat transfer**\n3. **Tight coupling** between concentration and temperature\n\nStandard explicit solvers would require extremely small timesteps. We use **GEAR52A**, an implicit solver designed for stiff systems." }, { "cell_type": "raw", @@ -422,4 +385,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/coupled_oscillators.ipynb b/docs/source/examples/coupled_oscillators.ipynb index 48121247..2a4e24c7 100644 --- a/docs/source/examples/coupled_oscillators.ipynb +++ b/docs/source/examples/coupled_oscillators.ipynb @@ -4,13 +4,7 @@ "cell_type": "markdown", "id": "0", "metadata": {}, - "source": [ - "# Coupled Oscillators\n", - "\n", - "This example demonstrates a system of two coupled damped harmonic oscillators using two ODE blocks.\n", - "\n", - "The system consists of two masses connected by springs with damping. Each oscillator is coupled to the other through a coupling spring constant." - ] + "source": "# Coupled Oscillators\n\nSimulation of two coupled damped harmonic oscillators using ODE blocks." }, { "cell_type": "raw", @@ -401,4 +395,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/source/examples/delta_sigma_adc.ipynb b/docs/source/examples/delta_sigma_adc.ipynb index 45933571..30d0dc12 100644 --- a/docs/source/examples/delta_sigma_adc.ipynb +++ b/docs/source/examples/delta_sigma_adc.ipynb @@ -3,12 +3,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "# Delta-Sigma ADC\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_deltasigma.py).This example demonstrates a first-order delta-sigma (ΔΣ) ADC, a popular architecture for high-resolution analog-to-digital conversion. The system uses oversampling and noise shaping to achieve high precision with a simple 1-bit quantizer.\n", - "\n" - ] + "source": "# Delta-Sigma ADC\n\nSimulation of a first-order delta-sigma analog-to-digital converter.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_deltasigma.py)." }, { "cell_type": "raw", @@ -305,4 +300,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/diode_circuit.ipynb b/docs/source/examples/diode_circuit.ipynb index 131574bd..d67fd85a 100644 --- a/docs/source/examples/diode_circuit.ipynb +++ b/docs/source/examples/diode_circuit.ipynb @@ -9,30 +9,7 @@ }, "tags": [] }, - "source": [ - "# Diode Circuit\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_diode.py).This example demonstrates a more complex application of algebraic loop solving: a **diode circuit** with nonlinear characteristics. This showcases PathSim's ability to handle nonlinear implicit equations that arise in real electronic circuits.\n", - "\n", - "## Circuit Description\n", - "\n", - "The circuit consists of:\n", - "- A sinusoidal **voltage source**: $V_s(t) = 5\\sin(2\\pi t)$ V\n", - "- A **resistor**: $R = 1000$ Ω\n", - "- A **diode** with exponential I-V characteristic\n", - "\n", - "## Diode Model\n", - "\n", - "The diode current follows the Shockley equation:\n", - "\n", - "$$i_D = I_s \\left(e^{V_D/V_T} - 1\\right)$$\n", - "\n", - "Where:\n", - "- $I_s = 10^{-12}$ A (saturation current)\n", - "- $V_T = 26$ mV (thermal voltage at room temperature)\n", - "- $V_D$ is the diode voltage\n", - "\n" - ] + "source": "# Diode Circuit\n\nSimulation of a diode circuit demonstrating nonlinear algebraic loop solving.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_diode.py).\n\n## Circuit Description\n\nThe circuit consists of:\n- A sinusoidal **voltage source**: $V_s(t) = 5\\sin(2\\pi t)$ V\n- A **resistor**: $R = 1000$ Ω\n- A **diode** with exponential I-V characteristic\n\n## Diode Model\n\nThe diode current follows the Shockley equation:\n\n$$i_D = I_s \\left(e^{V_D/V_T} - 1\\right)$$\n\nWhere:\n- $I_s = 10^{-12}$ A (saturation current)\n- $V_T = 26$ mV (thermal voltage at room temperature)\n- $V_D$ is the diode voltage" }, { "cell_type": "raw", @@ -409,4 +386,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/elastic_pendulum.ipynb b/docs/source/examples/elastic_pendulum.ipynb index cb56802c..d753ea43 100644 --- a/docs/source/examples/elastic_pendulum.ipynb +++ b/docs/source/examples/elastic_pendulum.ipynb @@ -10,13 +10,7 @@ }, "tags": [] }, - "source": [ - "# Elastic Pendulum\n", - "\n", - "You can also find this example as a standalone Python file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_elastic_pendulum.py).\n", - "\n", - "This example is a hybrid of the mathematical pendulum and the harmonic oscillator. A spring is attached to the ceiling and a mass at the other end. Then the mass is displaced from its resting position. for the angular component the system behaves like a harmonic oscillator and for the angular position like a pendulum. The two dynamics are coupled through the coriolis and the centrifugal force. " - ] + "source": "# Elastic Pendulum\n\nSimulation of an elastic pendulum combining spring and pendulum dynamics.\n\nYou can also find this example as a standalone Python file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_elastic_pendulum.py)." }, { "cell_type": "raw", diff --git a/docs/source/examples/fmu_cosimulation.ipynb b/docs/source/examples/fmu_cosimulation.ipynb index db0e1114..ae9be3c3 100644 --- a/docs/source/examples/fmu_cosimulation.ipynb +++ b/docs/source/examples/fmu_cosimulation.ipynb @@ -10,31 +10,7 @@ }, "tags": [] }, - "source": [ - "# FMU Co-Simulation\n", - "\n", - "This example demonstrates how to integrate a **Functional Mock-up Unit (FMU)** as a PathSim block using the co-simulation interface. FMUs are standardized model exchange and co-simulation packages that allow integration of models from different tools.\n", - "\n", - "## What is an FMU?\n", - "\n", - "The [Functional Mock-up Interface](https://fmi-standard.org/) (FMI) is an open standard for model exchange and co-simulation. An FMU is a ZIP archive containing:\n", - "- Model equations (compiled binaries)\n", - "- XML description of variables and metadata\n", - "- Optional resources and documentation\n", - "\n", - "PathSim supports **FMI 2.0** and **FMI 3.0** co-simulation FMUs through the [FMPy](https://github.com/CATIA-Systems/FMPy) library.\n", - "\n", - "## The Coupled Clutches Model\n", - "\n", - "This example uses a **coupled clutches** system, which is a classic benchmark from the FMI standard examples. The system consists of:\n", - "- Multiple rotating inertias\n", - "- Clutches connecting the inertias\n", - "- An input torque driving the system\n", - "\n", - "The FMU has:\n", - "- **1 input**: Torque applied to the first clutch\n", - "- **4 outputs**: Angular velocities of the four rotating masses" - ] + "source": "# FMU Co-Simulation\n\nDemonstration of integrating Functional Mock-up Units (FMU) as PathSim blocks.\n\n## What is an FMU?\n\nThe [Functional Mock-up Interface](https://fmi-standard.org/) (FMI) is an open standard for model exchange and co-simulation. An FMU is a ZIP archive containing:\n- Model equations (compiled binaries)\n- XML description of variables and metadata\n- Optional resources and documentation\n\nPathSim supports **FMI 2.0** and **FMI 3.0** co-simulation FMUs through the [FMPy](https://github.com/CATIA-Systems/FMPy) library.\n\n## The Coupled Clutches Model\n\nThis example uses a **coupled clutches** system, which is a classic benchmark from the FMI standard examples. The system consists of:\n- Multiple rotating inertias\n- Clutches connecting the inertias\n- An input torque driving the system\n\nThe FMU has:\n- **1 input**: Torque applied to the first clutch\n- **4 outputs**: Angular velocities of the four rotating masses" }, { "cell_type": "raw", diff --git a/docs/source/examples/harmonic_oscillator.ipynb b/docs/source/examples/harmonic_oscillator.ipynb index 15b615f1..d74f134e 100644 --- a/docs/source/examples/harmonic_oscillator.ipynb +++ b/docs/source/examples/harmonic_oscillator.ipynb @@ -3,13 +3,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "# Harmonic Oscillator\n", - "\n", - "In this example we have a look at the damped harmonic oscillator. A linear dynamical system that models a spring-mass-damper system.\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_harmonic_oscillator.py)." - ] + "source": "# Harmonic Oscillator\n\nSimulation of a damped harmonic oscillator, modeling a spring-mass-damper system.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_harmonic_oscillator.py)." }, { "cell_type": "raw", diff --git a/docs/source/examples/linear_feedback.ipynb b/docs/source/examples/linear_feedback.ipynb index d5461334..f7684c07 100644 --- a/docs/source/examples/linear_feedback.ipynb +++ b/docs/source/examples/linear_feedback.ipynb @@ -4,13 +4,7 @@ "cell_type": "markdown", "id": "0", "metadata": {}, - "source": [ - "# Linear Feedback System\n", - "\n", - "Here's a simple example of a linear feedback system, simulated with PathSim.\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_feedback.py)." - ] + "source": "# Linear Feedback System\n\nSimulation of a simple linear feedback system with step response.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_feedback.py)." }, { "cell_type": "raw", @@ -215,4 +209,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/source/examples/lorenz_attractor.ipynb b/docs/source/examples/lorenz_attractor.ipynb index 96369a52..df0fab83 100644 --- a/docs/source/examples/lorenz_attractor.ipynb +++ b/docs/source/examples/lorenz_attractor.ipynb @@ -3,32 +3,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "# Lorenz Attractor \n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_odes/example_lorenz.py).This example demonstrates the famous **Lorenz attractor**, a system of ordinary differential equations that exhibits chaotic behavior. It's one of the most iconic examples in chaos theory and was discovered by Edward Lorenz in 1963 while studying atmospheric convection.\n", - "\n", - "## The Lorenz System\n", - "\n", - "The system consists of three coupled nonlinear ODEs:\n", - "\n", - "$$\\frac{dx}{dt} = \\sigma(y - x)$$\n", - "$$\\frac{dy}{dt} = x(\\rho - z) - y$$\n", - "$$\\frac{dz}{dt} = xy - \\beta z$$\n", - "\n", - "Where the parameters are:\n", - "- $\\sigma = 10$ (Prandtl number)\n", - "- $\\rho = 28$ (Rayleigh number)\n", - "- $\\beta = 8/3$ (geometric factor)\n", - "\n", - "## Chaotic Behavior\n", - "\n", - "For these parameters, the system exhibits **sensitive dependence on initial conditions** - tiny changes in starting values lead to completely different trajectories. Despite being deterministic, the system appears random and unpredictable over long timescales.\n", - "\n", - "## Building the System in PathSim\n", - "\n", - "We'll construct the Lorenz system using basic blocks (integrators, amplifiers, multipliers, adders) to show PathSim's block-diagram approach to ODEs." - ] + "source": "# Lorenz Attractor \n\nSimulation of the famous Lorenz attractor, a chaotic dynamical system.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_odes/example_lorenz.py).\n\n## The Lorenz System\n\nThe system consists of three coupled nonlinear ODEs:\n\n$$\\frac{dx}{dt} = \\sigma(y - x)$$\n$$\\frac{dy}{dt} = x(\\rho - z) - y$$\n$$\\frac{dz}{dt} = xy - \\beta z$$\n\nWhere the parameters are:\n- $\\sigma = 10$ (Prandtl number)\n- $\\rho = 28$ (Rayleigh number)\n- $\\beta = 8/3$ (geometric factor)\n\n## Chaotic Behavior\n\nFor these parameters, the system exhibits **sensitive dependence on initial conditions** - tiny changes in starting values lead to completely different trajectories. Despite being deterministic, the system appears random and unpredictable over long timescales.\n\n## Building the System in PathSim\n\nWe'll construct the Lorenz system using basic blocks (integrators, amplifiers, multipliers, adders) to show PathSim's block-diagram approach to ODEs." }, { "cell_type": "raw", @@ -431,4 +406,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/nested_subsystems.ipynb b/docs/source/examples/nested_subsystems.ipynb index 5421cea0..88deaac3 100644 --- a/docs/source/examples/nested_subsystems.ipynb +++ b/docs/source/examples/nested_subsystems.ipynb @@ -23,31 +23,7 @@ }, "tags": [] }, - "source": [ - "# Nested Subsystems\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_nested_subsystems.py).This example demonstrates PathSim's **hierarchical modeling** capabilities using nested subsystems. We'll build a complex Van der Pol oscillator by composing simpler subsystems, showing how to organize large models in a modular way.\n", - "\n", - "## Why Use Subsystems?\n", - "\n", - "Subsystems allow you to:\n", - "- **Organize** complex systems into logical modules\n", - "- **Reuse** components across different models\n", - "- **Abstract** implementation details\n", - "- **Scale** to large systems with many components\n", - "- **Debug** and test individual modules separately\n", - "\n", - "## The Van der Pol Oscillator Revisited\n", - "\n", - "The stiff Van der Pol oscillator is described by:\n", - "\n", - "$$\\frac{dx_1}{dt} = x_2$$\n", - "$$\\frac{dx_2}{dt} = \\mu(1 - x_1^2)x_2 - x_1$$\n", - "\n", - "With $\\mu = 1000$ (very stiff!)\n", - "\n", - "## Hierarchical Structure\n" - ] + "source": "# Nested Subsystems\n\nDemonstration of hierarchical modeling using nested subsystems for a Van der Pol oscillator.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_nested_subsystems.py).\n\n## Why Use Subsystems?\n\nSubsystems allow you to:\n- **Organize** complex systems into logical modules\n- **Reuse** components across different models\n- **Abstract** implementation details\n- **Scale** to large systems with many components\n- **Debug** and test individual modules separately\n\n## The Van der Pol Oscillator Revisited\n\nThe stiff Van der Pol oscillator is described by:\n\n$$\\frac{dx_1}{dt} = x_2$$\n$$\\frac{dx_2}{dt} = \\mu(1 - x_1^2)x_2 - x_1$$\n\nWith $\\mu = 1000$ (very stiff!)\n\n## Hierarchical Structure" }, { "cell_type": "raw", @@ -478,4 +454,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/noisy_amplifier.ipynb b/docs/source/examples/noisy_amplifier.ipynb index 9940a58a..0d648907 100644 --- a/docs/source/examples/noisy_amplifier.ipynb +++ b/docs/source/examples/noisy_amplifier.ipynb @@ -10,9 +10,7 @@ }, "tags": [] }, - "source": [ - "# Noisy Amplifier" - ] + "source": "# Noisy Amplifier\n\nSimulation of a nonlinear, noisy, and band-limited amplifier model." }, { "cell_type": "markdown", @@ -429,4 +427,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/source/examples/pendulum.ipynb b/docs/source/examples/pendulum.ipynb index d8094211..74c4435b 100644 --- a/docs/source/examples/pendulum.ipynb +++ b/docs/source/examples/pendulum.ipynb @@ -4,13 +4,7 @@ "cell_type": "markdown", "id": "0", "metadata": {}, - "source": [ - "# Pendulum\n", - "\n", - "In this example we have a look at the mathematical pendulum.\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_pendulum.py)." - ] + "source": "# Pendulum\n\nSimulation of a nonlinear mathematical pendulum.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_pendulum.py)." }, { "cell_type": "raw", @@ -229,4 +223,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/source/examples/pid_controller.ipynb b/docs/source/examples/pid_controller.ipynb index fe48d601..e80fb431 100644 --- a/docs/source/examples/pid_controller.ipynb +++ b/docs/source/examples/pid_controller.ipynb @@ -3,13 +3,7 @@ { "cell_type": "markdown", "metadata": {}, - "source": [ - "# PID Controller\n", - "\n", - "This example demonstrates a PID (Proportional-Integral-Derivative) controller in PathSim, including automatic differentiation for sensitivity analysis. The system tracks a step-changing setpoint and computes how the error signal responds to changes in PID parameters.\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_pid.py)." - ] + "source": "# PID Controller\n\nSimulation of a PID controller tracking a step-changing setpoint.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_pid.py)." }, { "cell_type": "raw", @@ -294,4 +288,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/poincare_maps.ipynb b/docs/source/examples/poincare_maps.ipynb index 9c0ff509..a3f07642 100644 --- a/docs/source/examples/poincare_maps.ipynb +++ b/docs/source/examples/poincare_maps.ipynb @@ -4,34 +4,7 @@ "cell_type": "markdown", "id": "e5fd6834-1ba7-467d-83be-939b3b5d7e41", "metadata": {}, - "source": [ - "# Poincaré Maps\n", - "\n", - "This example demonstrates how to use PathSim's event handling system to compute **Poincaré maps** (or Poincaré sections) of chaotic dynamical systems. A Poincaré map is a powerful tool for analyzing the behavior of continuous dynamical systems by sampling the system state whenever the trajectory intersects a lower-dimensional surface.\n", - "\n", - "## What are Poincaré Maps?\n", - "\n", - "For a 3D system like the Lorenz attractor, the full trajectory traces out a complex path in three-dimensional space. A **Poincaré section** is created by:\n", - "\n", - "1. Defining a 2D surface (or plane) in the 3D phase space\n", - "2. Recording the system state each time the trajectory crosses this surface\n", - "3. Plotting these intersection points to reveal the underlying structure\n", - "\n", - "This reduces the dimensionality from 3D to 2D, making it easier to:\n", - "- Identify periodic orbits (which appear as fixed points or closed loops)\n", - "- Detect chaotic behavior (which appears as scattered but structured point clouds)\n", - "- Analyze the stability and structure of attractors\n", - "\n", - "## The Lorenz System\n", - "\n", - "We'll use the famous Lorenz attractor as our test case. The system consists of three coupled nonlinear ODEs:\n", - "\n", - "$$\\frac{dx}{dt} = \\sigma(y - x)$$\n", - "$$\\frac{dy}{dt} = x(\\rho - z) - y$$\n", - "$$\\frac{dz}{dt} = xy - \\beta z$$\n", - "\n", - "With the classic parameters $\\sigma = 10$, $\\rho = 28$, and $\\beta = 8/3$ that produce chaotic behavior." - ] + "source": "# Poincaré Maps\n\nDemonstration of computing Poincaré sections for chaotic dynamical systems using event handling.\n\n## What are Poincaré Maps?\n\nFor a 3D system like the Lorenz attractor, the full trajectory traces out a complex path in three-dimensional space. A **Poincaré section** is created by:\n\n1. Defining a 2D surface (or plane) in the 3D phase space\n2. Recording the system state each time the trajectory crosses this surface\n3. Plotting these intersection points to reveal the underlying structure\n\nThis reduces the dimensionality from 3D to 2D, making it easier to:\n- Identify periodic orbits (which appear as fixed points or closed loops)\n- Detect chaotic behavior (which appears as scattered but structured point clouds)\n- Analyze the stability and structure of attractors\n\n## The Lorenz System\n\nWe'll use the famous Lorenz attractor as our test case. The system consists of three coupled nonlinear ODEs:\n\n$$\\frac{dx}{dt} = \\sigma(y - x)$$\n$$\\frac{dy}{dt} = x(\\rho - z) - y$$\n$$\\frac{dz}{dt} = xy - \\beta z$$\n\nWith the classic parameters $\\sigma = 10$, $\\rho = 28$, and $\\beta = 8/3$ that produce chaotic behavior." }, { "cell_type": "code", @@ -431,4 +404,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/source/examples/rf_network_oneport.ipynb b/docs/source/examples/rf_network_oneport.ipynb index 41a0cb2d..abf4e238 100644 --- a/docs/source/examples/rf_network_oneport.ipynb +++ b/docs/source/examples/rf_network_oneport.ipynb @@ -10,9 +10,7 @@ }, "tags": [] }, - "source": [ - "# RF Network - One Port" - ] + "source": "# RF Network - One Port\n\nSimulation of a Radio Frequency (RF) network using scikit-rf for state-space conversion.\n\nYou can also find a similar example in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_spectrum_rf_oneport.py)." }, { "cell_type": "markdown", @@ -258,4 +256,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/source/examples/sar_adc.ipynb b/docs/source/examples/sar_adc.ipynb index da58fd14..6dcd69ce 100644 --- a/docs/source/examples/sar_adc.ipynb +++ b/docs/source/examples/sar_adc.ipynb @@ -9,12 +9,7 @@ }, "tags": [] }, - "source": [ - "# SAR ADC\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_sar.py).This advanced example demonstrates a **SAR ADC** (Successive Approximation Register Analog-to-Digital Converter), one of the most popular ADC architectures. This example also shows how to **create custom blocks** in PathSim by extending the base `Block` class.\n", - "\n" - ] + "source": "# SAR ADC\n\nSimulation of a Successive Approximation Register ADC with custom block creation.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/example_sar.py)." }, { "cell_type": "raw", @@ -462,4 +457,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} +} \ No newline at end of file diff --git a/docs/source/examples/stick_slip.ipynb b/docs/source/examples/stick_slip.ipynb index f2f18eab..056f75e7 100644 --- a/docs/source/examples/stick_slip.ipynb +++ b/docs/source/examples/stick_slip.ipynb @@ -4,13 +4,7 @@ "cell_type": "markdown", "id": "0", "metadata": {}, - "source": [ - "# Stick Slip\n", - "\n", - "In this example we simulate a mechanical system that exhibits stick-slip behaviour, typical for coulomb friction. Lets consider the setup below, where we have a box sitting on a driven conveyor belt. The box is also coupled to a fixation by a spring-damper element.\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_event/example_stickslip_event.py)." - ] + "source": "# Stick Slip\n\nSimulation of a mechanical system exhibiting stick-slip behavior due to Coulomb friction.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_event/example_stickslip_event.py)." }, { "cell_type": "raw", diff --git a/docs/source/examples/thermostat.ipynb b/docs/source/examples/thermostat.ipynb index 5c5cef42..f9b80ba3 100644 --- a/docs/source/examples/thermostat.ipynb +++ b/docs/source/examples/thermostat.ipynb @@ -4,20 +4,7 @@ "cell_type": "markdown", "id": "0", "metadata": {}, - "source": [ - "# Thermostat\n", - "\n", - "In this example we have a look at a thermostat, which is a kind off discrete regulator. It switches a heating element on or of depending on defined upper and lower thresholds.\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_event/example_thermostat.py).\n", - "\n", - "The continuous dynamics part of the system has the following two ODEs for the two heater states:\n", - "\n", - "$$\\begin{cases} \n", - "\\dot{T} = - a ( T - T_a ) + H & \\text{heater on} \\\\\n", - "\\dot{T} = - a ( T - T_a ) & \\text{heater off}\n", - "\\end{cases}$$" - ] + "source": "# Thermostat\n\nSimulation of a thermostat with threshold-based switching between heater states.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_event/example_thermostat.py).\n\nThe continuous dynamics part of the system has the following two ODEs for the two heater states:\n\n$$\\begin{cases} \n\\dot{T} = - a ( T - T_a ) + H & \\text{heater on} \\\\\n\\dot{T} = - a ( T - T_a ) & \\text{heater off}\n\\end{cases}$$" }, { "cell_type": "markdown", @@ -379,4 +366,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/source/examples/vanderpol.ipynb b/docs/source/examples/vanderpol.ipynb index 349473d9..8d19156f 100644 --- a/docs/source/examples/vanderpol.ipynb +++ b/docs/source/examples/vanderpol.ipynb @@ -4,17 +4,7 @@ "cell_type": "markdown", "id": "0", "metadata": {}, - "source": [ - "# Van der Pol\n", - "\n", - "Here we cover a true classic from the world of dynamical systems. The *Van der Pol* system!\n", - "\n", - "You can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_odes/example_vanderpol.py).\n", - "\n", - "It is described by the following 2nd order ODE\n", - "\n", - "$$\\ddot{x} + \\mu (x^2 - 1) \\dot{x} + x = 0$$" - ] + "source": "# Van der Pol\n\nSimulation of the Van der Pol oscillator, a classic example of a stiff dynamical system.\n\nYou can also find this example as a single file in the [GitHub repository](https://github.com/milanofthe/pathsim/blob/master/examples/examples_odes/example_vanderpol.py).\n\nThe Van der Pol oscillator is described by the following 2nd order ODE\n\n$$\\ddot{x} + \\mu (x^2 - 1) \\dot{x} + x = 0$$" }, { "cell_type": "raw", @@ -34,14 +24,7 @@ "cell_type": "markdown", "id": "2", "metadata": {}, - "source": [ - "$$\\begin{eqnarray}\n", - " \\dot{x}_1 =& x_2 \\\\\n", - " \\dot{x}_2 =& \\mu (1 - x_1^2) x_2 - x_1\n", - "\\end{eqnarray}$$\n", - "\n", - "As a block diagram it would look like this:" - ] + "source": "$$\\begin{align}\n \\dot{x}_1 &= x_2 \\\\\n \\dot{x}_2 &= \\mu (1 - x_1^2) x_2 - x_1\n\\end{align}$$\n\nAs a block diagram it would look like this:" }, { "cell_type": "raw", @@ -220,4 +203,4 @@ }, "nbformat": 4, "nbformat_minor": 5 -} +} \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index 993ec87b..bb32c065 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,6 +6,16 @@ PathSim .. raw:: html ++ 📢 Redirecting to the new documentation site... +
++ This legacy documentation is no longer updated. You will be redirected to + docs.pathsim.org in a few seconds. +
+A scalable block-based time-domain system simulation framework in Python with hierarchical modeling and event handling! diff --git a/docs/source/logos/pathsim_icon.png b/docs/source/logos/pathsim_icon.png index 2c39d279..c786aa08 100644 Binary files a/docs/source/logos/pathsim_icon.png and b/docs/source/logos/pathsim_icon.png differ diff --git a/docs/source/logos/pathsim_logo.png b/docs/source/logos/pathsim_logo.png index 8ee02f13..64528d72 100644 Binary files a/docs/source/logos/pathsim_logo.png and b/docs/source/logos/pathsim_logo.png differ diff --git a/pyproject.toml b/pyproject.toml index 4542bfa7..54885df3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,12 +20,18 @@ dependencies = ["numpy>=1.15", "matplotlib>=3.1", "scipy>=1.2"] [project.optional-dependencies] rf = ["scikit-rf"] -test = ["pytest", "pytest-cov", "codecov", "FMPy", "scikit-rf"] +test = ["pytest", "pytest-cov", "pytest-xdist", "FMPy", "scikit-rf"] fmi = ["FMPy"] [project.urls] Homepage = "https://github.com/pathsim/pathsim" documentation = "https://pathsim.readthedocs.io/en/latest/" +[tool.pytest.ini_options] +addopts = "-m 'not slow'" +markers = [ + "slow: long-running integration / eval tests (deselected by default, use -m slow or --run-all)", +] + [tool.setuptools_scm] write_to = "src/pathsim/_version.py" diff --git a/src/pathsim/_constants.py b/src/pathsim/_constants.py index 4267130a..91d0c348 100644 --- a/src/pathsim/_constants.py +++ b/src/pathsim/_constants.py @@ -27,7 +27,7 @@ SOL_ITERATIONS_MAX = 200 # max number of optimizer iterations (for standalone implicit solvers) SOL_SCALE_MIN = 0.1 # min allowed timestep rescale factor (adaptive solvers) SOL_SCALE_MAX = 10 # max allowed timestep rescale factor (adaptive solvers) -SOL_BETA = 0.9 # savety for timestep control (adaptive solvers) +SOL_BETA = 0.9 # safety for timestep control (adaptive solvers) # optimizer default constants ---------------------------------------------------- diff --git a/src/pathsim/blocks/__init__.py b/src/pathsim/blocks/__init__.py index c8580e25..dd67152b 100644 --- a/src/pathsim/blocks/__init__.py +++ b/src/pathsim/blocks/__init__.py @@ -1,6 +1,7 @@ from .differentiator import * from .integrator import * from .multiplier import * +from .divider import * from .converters import * from .comparator import * from .samplehold import * @@ -20,6 +21,7 @@ from .noise import * from .table import * from .relay import * +from .logic import * from .math import * from .ctrl import * from .lti import * diff --git a/src/pathsim/blocks/_block.py b/src/pathsim/blocks/_block.py index 3716d895..6a3c3e3c 100644 --- a/src/pathsim/blocks/_block.py +++ b/src/pathsim/blocks/_block.py @@ -21,9 +21,9 @@ # BASE BLOCK CLASS ====================================================================== class Block: - """Base 'Block' object that defines the inputs, outputs and the connect method. + """Base 'Block' object that defines the inputs, outputs and the block interface. - Block interconnections are handeled via the io interface of the blocks. + Block interconnections are handled via the io interface of the blocks. It is realized by dicts for the 'inputs' and for the 'outputs', where the key of the dict is the input/output channel and the corresponding value is the input/output value. @@ -35,10 +35,10 @@ class Block: .. math:: - \\begin{eqnarray} + \\begin{align} \\dot{x} &= f_\\mathrm{dyn}(x, u, t)\\\\ y &= f_\\mathrm{alg}(x, u, t) - \\end{eqnarray} + \\end{align} they are algebraic operators for the algebraic path of the block and for the @@ -65,8 +65,10 @@ class definition for other blocks to be inherited. input value register of block outputs : Register output value register of block + state : None | float | np.ndarray + state of `engine` exposed as a property, only exists for dynamic blocks engine : None | Solver - numerical integrator instance + numerical integrator instance, only exists for dynamic blocks events : list[Event] list of internal events, for mixed signal blocks _active : bool @@ -108,7 +110,7 @@ def __len__(self): """The '__len__' method of the block is used to compute the length of the algebraic path of the block. - For instant time blocks or blocks with purely algenbraic components + For instant time blocks or blocks with purely algebraic components (adders, amplifiers, etc.) it returns 1, otherwise (integrator, delay, etc.) it returns 0. @@ -309,13 +311,18 @@ def reset(self): self.inputs.reset() self.outputs.reset() - #reset engine if block has solver - if self.engine: self.engine.reset() + #reset engine if block has solver (updating the initial condition) + if self.engine: + self.engine.reset(self.initial_value) #reset operators if defined if self.op_alg: self.op_alg.reset() if self.op_dyn: self.op_dyn.reset() + #reset internal events (if there are any) + for event in self.events: + event.reset() + def linearize(self, t): """Linearize the algebraic and dynamic components of the block. @@ -323,7 +330,7 @@ def linearize(self, t): This is done by linearizing the internal 'Operator' and 'DynamicOperator' instances in the current system operating point. The operators create 1st order taylor approximations internally and use them on subsequent - calls after linarization. + calls after linearization. Parameters ---------- @@ -418,7 +425,7 @@ def buffer(self, dt): def sample(self, t, dt): """Samples the data of the blocks inputs or internal state when called. - This can record block parameters after a succesful timestep such as + This can record block parameters after a successful timestep such as for the 'Scope' and 'Delay' blocks but also for sampling from a random distribution in the 'RNG' and the noise blocks. @@ -482,6 +489,128 @@ def get_all(self): return _inputs, _outputs, _states + @property + def state(self): + """Expose the state of the internal integration engine / + `Solver` instance as an attribute of `Block`. + + Note + ---- + Only applies to blocks that are dynamic. + + Returns + ------- + state : None, float, np.ndarray + returns the current state of the block if the block is dynamic + (has an internal `Solver` instance), otherwise returns `None` + """ + return self.engine.state if self.engine else None + + + @state.setter + def state(self, val): + """Setter method for the exposed internal `Solver` instance. + + Note + ---- + Only applies to blocks that are dynamic. + + Parameters + ---------- + val : float, np.ndarray + value to set internal solver state to + """ + if self.engine: + self.engine.state = val + + + # checkpoint methods ---------------------------------------------------------------- + + def to_checkpoint(self, prefix, recordings=False): + """Serialize block state for checkpointing. + + Parameters + ---------- + prefix : str + key prefix for NPZ arrays (assigned by simulation) + recordings : bool + include recording data (for Scope blocks) + + Returns + ------- + json_data : dict + JSON-serializable metadata + npz_data : dict + numpy arrays keyed by path + """ + json_data = { + "type": self.__class__.__name__, + "active": self._active, + } + + npz_data = { + f"{prefix}/inputs": self.inputs.to_array(), + f"{prefix}/outputs": self.outputs.to_array(), + } + + #solver state + if self.engine: + e_json, e_npz = self.engine.to_checkpoint(f"{prefix}/engine") + json_data["engine"] = e_json + npz_data.update(e_npz) + + #internal events + if self.events: + evt_jsons = [] + for i, event in enumerate(self.events): + evt_prefix = f"{prefix}/evt_{i}" + e_json, e_npz = event.to_checkpoint(evt_prefix) + evt_jsons.append(e_json) + npz_data.update(e_npz) + json_data["events"] = evt_jsons + + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + """Restore block state from checkpoint. + + Parameters + ---------- + prefix : str + key prefix for NPZ arrays (assigned by simulation) + json_data : dict + block metadata from checkpoint JSON + npz : dict-like + numpy arrays from checkpoint NPZ + """ + #verify type + if json_data["type"] != self.__class__.__name__: + raise ValueError( + f"Checkpoint type mismatch: expected '{self.__class__.__name__}', " + f"got '{json_data['type']}'" + ) + + self._active = json_data["active"] + + #restore registers + inp_key = f"{prefix}/inputs" + out_key = f"{prefix}/outputs" + if inp_key in npz: + self.inputs.update_from_array(npz[inp_key]) + if out_key in npz: + self.outputs.update_from_array(npz[out_key]) + + #restore solver state + if self.engine and "engine" in json_data: + self.engine.load_checkpoint(json_data["engine"], npz, f"{prefix}/engine") + + #restore internal events + if self.events and "events" in json_data: + for i, (event, evt_data) in enumerate(zip(self.events, json_data["events"])): + event.load_checkpoint(f"{prefix}/evt_{i}", evt_data, npz) + + # methods for block output and state updates ---------------------------------------- def update(self, t): @@ -578,4 +707,4 @@ def step(self, t, dt): """ #by default no error estimate (error norm -> 0.0) - return True, 0.0, None \ No newline at end of file + return True, 0.0, None diff --git a/src/pathsim/blocks/converters.py b/src/pathsim/blocks/converters.py index c2726930..b069031c 100644 --- a/src/pathsim/blocks/converters.py +++ b/src/pathsim/blocks/converters.py @@ -12,10 +12,12 @@ from ._block import Block from ..utils.register import Register from ..events.schedule import Schedule +from ..utils.mutable import mutable # MIXED SIGNAL BLOCKS =================================================================== +@mutable class ADC(Block): """Models an ideal Analog-to-Digital Converter (ADC). @@ -104,6 +106,7 @@ def __len__(self): return 0 +@mutable class DAC(Block): """Models an ideal Digital-to-Analog Converter (DAC). diff --git a/src/pathsim/blocks/counter.py b/src/pathsim/blocks/counter.py index 25114bf6..2752252e 100644 --- a/src/pathsim/blocks/counter.py +++ b/src/pathsim/blocks/counter.py @@ -19,7 +19,7 @@ class Counter(Block): """Counts the number of detected bidirectional threshold crossings. - Uses zero-crossing events for the detection and and sets the output + Uses zero-crossing events for the detection and sets the output accordingly. Parameters diff --git a/src/pathsim/blocks/ctrl.py b/src/pathsim/blocks/ctrl.py index b27edba7..70fa72f6 100644 --- a/src/pathsim/blocks/ctrl.py +++ b/src/pathsim/blocks/ctrl.py @@ -10,47 +10,212 @@ import numpy as np from ._block import Block +from .lti import StateSpace +from .dynsys import DynamicalSystem -from ..utils.register import Register -from ..optim.operator import DynamicOperator +from ..optim.operator import Operator, DynamicOperator +from ..utils.mutable import mutable -# SISO BLOCKS =========================================================================== +# LTI CONTROL BLOCKS (StateSpace subclasses) ============================================ -class PID(Block): - """Proportional-Integral-Differntiation (PID) controller. +@mutable +class PT1(StateSpace): + """First-order lag element (PT1). The transfer function is defined as .. math:: - - H_\\mathrm{diff}(s) = K_p + K_i \\frac{1}{s} + K_d \\frac{s}{1 + s / f_\\mathrm{max}} - where the differentiation is approximated by a high pass filter that holds + H(s) = \\frac{K}{1 + T s} + + where `K` is the static gain and `T` is the time constant. + + + Example + ------- + The block is initialized like this: + + .. code-block:: python + + pt1 = PT1(K=2.0, T=0.5) + + + Parameters + ---------- + K : float + static gain + T : float + time constant in seconds (must be > 0) + """ + + input_port_labels = {"in": 0} + output_port_labels = {"out": 0} + + def __init__(self, K=1.0, T=1.0): + + #element parameters + self.K = K + self.T = T + + #statespace realization + super().__init__( + A=np.array([[-1.0 / T]]), + B=np.array([[K / T]]), + C=np.array([[1.0]]), + D=np.array([[0.0]]) + ) + + +@mutable +class PT2(StateSpace): + """Second-order lag element (PT2). + + The transfer function is defined as + + .. math:: + + H(s) = \\frac{K}{1 + 2 d T s + T^2 s^2} + + where `K` is the static gain, `T` is the time constant + (related to the natural frequency by :math:`\\omega_n = 1/T`) + and `d` is the damping ratio. + + The damping ratio `d` controls the transient behavior: + + - :math:`d < 1`: underdamped (oscillatory) + - :math:`d = 1`: critically damped + - :math:`d > 1`: overdamped + + + Example + ------- + The block is initialized like this: + + .. code-block:: python + + #underdamped second-order system + pt2 = PT2(K=1.0, T=0.1, d=0.3) + + + Parameters + ---------- + K : float + static gain + T : float + time constant in seconds (must be > 0) + d : float + damping ratio (must be >= 0) + """ + + input_port_labels = {"in": 0} + output_port_labels = {"out": 0} + + def __init__(self, K=1.0, T=1.0, d=1.0): + + #element parameters + self.K = K + self.T = T + self.d = d + + #statespace realization (controllable canonical form) + super().__init__( + A=np.array([[0.0, 1.0], [-1.0 / T**2, -2.0 * d / T]]), + B=np.array([[0.0], [1.0]]), + C=np.array([[K / T**2, 0.0]]), + D=np.array([[0.0]]) + ) + + +@mutable +class LeadLag(StateSpace): + """Lead-Lag compensator. + + The transfer function is defined as + + .. math:: + + H(s) = K \\frac{T_1 s + 1}{T_2 s + 1} + + where `K` is the static gain, `T1` is the lead time constant + and `T2` is the lag time constant. + + - :math:`T_1 > T_2`: lead compensator (phase advance) + - :math:`T_1 < T_2`: lag compensator (phase lag) + - :math:`T_1 = T_2`: pure gain + + + Example + ------- + The block is initialized like this: + + .. code-block:: python + + #lead compensator + ll = LeadLag(K=1.0, T1=0.5, T2=0.1) + + + Parameters + ---------- + K : float + static gain + T1 : float + lead (numerator) time constant in seconds + T2 : float + lag (denominator) time constant in seconds (must be > 0) + """ + + input_port_labels = {"in": 0} + output_port_labels = {"out": 0} + + def __init__(self, K=1.0, T1=1.0, T2=1.0): + + #compensator parameters + self.K = K + self.T1 = T1 + self.T2 = T2 + + #statespace realization + super().__init__( + A=np.array([[-1.0 / T2]]), + B=np.array([[1.0 / T2]]), + C=np.array([[K * (T2 - T1) / T2]]), + D=np.array([[K * T1 / T2]]) + ) + + +@mutable +class PID(StateSpace): + """Proportional-Integral-Differentiation (PID) controller. + + The transfer function is defined as + + .. math:: + + H(s) = K_p + K_i \\frac{1}{s} + K_d \\frac{s}{1 + s / f_\\mathrm{max}} + + where the differentiation is approximated by a high pass filter that holds for signals up to a frequency of approximately `f_max`. + Internally realized as a linear state space model with two states + (differentiator filter state and integrator state). + Note ---- Depending on `f_max`, the resulting system might become stiff or ill conditioned! As a practical choice set `f_max` to 3x the highest expected signal frequency. - Since this block uses an approximation of real differentiation, the approximation will - not hold if there are high frequency components present in the signal. For example if + Since this block uses an approximation of real differentiation, the approximation will + not hold if there are high frequency components present in the signal. For example if you have discontinuities such as steps or square waves. - Note - ---- - This block supports vector input, meaning we can have multiple parallel - PID paths through this block. - - Example ------- The block is initialized like this: .. code-block:: python - + #cutoff at 1kHz pid = PID(Kp=2, Ki=0.5, Kd=0.1, f_max=1e3) @@ -58,26 +223,19 @@ class PID(Block): Parameters ---------- Kp : float - poroportional controller coefficient + proportional controller coefficient Ki : float integral controller coefficient Kd : float differentiator controller coefficient f_max : float highest expected signal frequency + """ + input_port_labels = {"in": 0} + output_port_labels = {"out": 0} - Attributes - ---------- - op_dyn : DynamicOperator - internal dynamic operator for ODE component - op_alg : DynamicOperator - internal algebraic operator - - """ - def __init__(self, Kp=0, Ki=0, Kd=0, f_max=100): - super().__init__() #pid controller coefficients self.Kp = Kp @@ -87,147 +245,68 @@ def __init__(self, Kp=0, Ki=0, Kd=0, f_max=100): #maximum frequency for differentiator approximation self.f_max = f_max - #initial state for integration engine (differentiator + integrator states) - self.initial_value = np.zeros(2) - - def _g_pid(x, u, t): - x1, x2 = x - yp = self.Kp * u - yi = self.Ki * x2 - yd = self.Kd * self.f_max * (u - x1) - return yp + yi + yd - - def _jac_x_g_pid(x, u, t): - return np.array([-self.Kd * self.f_max, self.Ki]) - - def _jac_u_g_pid(x, u, t): - return self.Kd * self.f_max + self.Kp - - def _f_pid(x, u, t): - x1, x2 = x - dx1, dx2 = self.f_max * (u - x1), u - return np.array([dx1, dx2]) - - #internal operators - self.op_dyn = DynamicOperator( - func=_f_pid, - ) - self.op_alg = DynamicOperator( - func=_g_pid, - jac_x=_jac_x_g_pid, - jac_u=_jac_u_g_pid, + #statespace realization + # states: x1 = differentiator filter, x2 = integrator + # dx1/dt = f_max * (u - x1) + # dx2/dt = u + # y = Kp*u + Ki*x2 + Kd*f_max*(u - x1) + super().__init__( + A=np.array([[-f_max, 0.0], [0.0, 0.0]]), + B=np.array([[f_max], [1.0]]), + C=np.array([[-Kd * f_max, Ki]]), + D=np.array([[Kd * f_max + Kp]]) ) - def __len__(self): - return 1 if self._active and (self.Kp or self.Kd) else 0 - - - def update(self, t): - """update system equation fixed point loop, with convergence control - - Parameters - ---------- - t : float - evaluation time - """ - x, u = self.engine.state, self.inputs[0] - y = self.op_alg(x, u, t) - self.outputs.update_from_array(y) - - - def solve(self, t, dt): - """advance solution of implicit update equation - - Parameters - ---------- - t : float - evaluation time - dt : float - integration timestep - - Returns - ------- - error : float - solver residual norm - """ - x, u = self.engine.state, self.inputs[0] - f, J = self.op_dyn(x, u, t), self.op_dyn.jac_x(x, u, t) - return self.engine.solve(f, J, dt) - - - def step(self, t, dt): - """compute update step with integration engine - - Parameters - ---------- - t : float - evaluation time - dt : float - integration timestep - - Returns - ------- - success : bool - step was successful - error : float - local truncation error from adaptive integrators - scale : float - timestep rescale from adaptive integrators - """ - x, u = self.engine.state, self.inputs[0] - f = self.op_dyn(x, u, t) - return self.engine.step(f, dt) - - +@mutable class AntiWindupPID(PID): - """Proportional-Integral-Differntiation (PID) controller with anti-windup mechanism (back-calculation). - - Anti-windup mechanisms are needed when the magnitude of the control signal - from the PID controller is limited by some real world saturation. In these cases, - the integrator will continue to acumulate the control error and "wind itself up". - Once the setpoint is reached, this can result in significant overshoots. This - implementation adds a conditional feedback term to the internal integrator that - "unwinds" it when the PID output crosses some limits. This is pretty much a + """Proportional-Integral-Differentiation (PID) controller with anti-windup mechanism (back-calculation). + + Anti-windup mechanisms are needed when the magnitude of the control signal + from the PID controller is limited by some real world saturation. In these cases, + the integrator will continue to accumulate the control error and "wind itself up". + Once the setpoint is reached, this can result in significant overshoots. This + implementation adds a conditional feedback term to the internal integrator that + "unwinds" it when the PID output crosses some limits. This is pretty much a deadzone feedback element for the integrator. - - Mathematically, this block implements the following set of ODEs + + Mathematically, this block implements the following set of ODEs .. math:: - - \\begin{eqnarray} - \\dot{x}_1 =& f_\\mathrm{max} (u - x_1) \\\\ - \\dot{x}_2 =& u - w \\\\ - \\end{eqnarray} - + + \\begin{align} + \\dot{x}_1 &= f_\\mathrm{max} (u - x_1) \\\\ + \\dot{x}_2 &= u - w + \\end{align} + with the anti-windup feedback (depending on the pid output) .. math:: - + w = K_s (y - \\min(\\max(y, y_\\mathrm{min}), y_\\mathrm{max})) and the output itself .. math:: - y = K_p u - K_d f_\\mathrm{max} x_1 + K_i x_2 - + y = K_p u + K_d f_\\mathrm{max} (u - x_1) + K_i x_2 + Note ---- Depending on `f_max`, the resulting system might become stiff or ill conditioned! As a practical choice set `f_max` to 3x the highest expected signal frequency. - Since this block uses an approximation of real differentiation, the approximation will - not hold if there are high frequency components present in the signal. For example if - you have discontinuities such as steps or squere waves. + Since this block uses an approximation of real differentiation, the approximation will + not hold if there are high frequency components present in the signal. For example if + you have discontinuities such as steps or square waves. + - Example ------- The block is initialized like this: .. code-block:: python - + #cutoff at 1kHz, windup limits at [-5, 5] pid = AntiWindupPID(Kp=2, Ki=0.5, Kd=0.1, f_max=1e3, limits=[-5, 5]) @@ -235,7 +314,7 @@ class AntiWindupPID(PID): Parameters ---------- Kp : float - poroportional controller coefficient + proportional controller coefficient Ki : float integral controller coefficient Kd : float @@ -246,15 +325,6 @@ class AntiWindupPID(PID): feedback term for back calculation for anti-windup control of integrator limits : array_like[float] lower and upper limit for PID output that triggers anti-windup of integrator - - - Attributes - ---------- - op_dyn : DynamicOperator - internal dynamic operator for ODE component - op_alg : DynamicOperator - internal algebraic operator - """ def __init__(self, Kp=0, Ki=0, Kd=0, f_max=100, Ks=10, limits=[-10, 10]): @@ -264,38 +334,193 @@ def __init__(self, Kp=0, Ki=0, Kd=0, f_max=100, Ks=10, limits=[-10, 10]): self.Ks = Ks self.limits = limits - def _g_pid(x, u, t): - x1, x2 = x - yp = self.Kp * u - yi = self.Ki * x2 - yd = self.Kd * self.f_max * (u - x1) - return yp + yi + yd - - def _jac_x_g_pid(x, u, t): - return np.array([-self.Kd * self.f_max, self.Ki]) - - def _jac_u_g_pid(x, u, t): - return self.Kd * self.f_max + self.Kp - + #override dynamic operator with nonlinear anti-windup feedback def _f_pid(x, u, t): x1, x2 = x + u0 = u[0] #differentiator state - dx1 = self.f_max * (u - x1) - + dx1 = self.f_max * (u0 - x1) + #integrator state with windup control - y = _g_pid(x, u, t) #pid output - w = self.Ks * (y - np.clip(y, *self.limits)) #anti-windup feedback - dx2 = u - w + y = self.Kp * u0 + self.Ki * x2 + self.Kd * self.f_max * (u0 - x1) + w = self.Ks * (y - np.clip(y, *self.limits)) + dx2 = u0 - w return np.array([dx1, dx2]) - #internal operators - self.op_dyn = DynamicOperator( - func=_f_pid, + self.op_dyn = DynamicOperator(func=_f_pid) + + +# NONLINEAR CONTROL BLOCKS ============================================================== + +class RateLimiter(DynamicalSystem): + """Rate limiter block that limits the rate of change of a signal. + + Implements a continuous-time rate limiter as a first-order tracking system + with clipped rate of change: + + .. math:: + + \\dot{x} = \\mathrm{clip}\\left(f_\\mathrm{max} (u - x),\\; -r,\\; r\\right) + + where `r` is the maximum allowed rate and `f_max` controls the tracking + bandwidth when the signal is not rate-limited. The output is the state + :math:`y = x`. + + + Note + ---- + The parameter `f_max` should be set high enough that the output tracks + the input without lag when the rate is within limits. + + + Example + ------- + The block is initialized like this: + + .. code-block:: python + + #max rate of 10 units/s + rl = RateLimiter(rate=10.0, f_max=1e3) + + + Parameters + ---------- + rate : float + maximum rate of change (positive value) + f_max : float + tracking bandwidth parameter + """ + + input_port_labels = {"in": 0} + output_port_labels = {"out": 0} + + def __init__(self, rate=1.0, f_max=100): + + #rate limiter parameters + self.rate = rate + self.f_max = f_max + + super().__init__( + func_dyn=lambda x, u, t: np.clip(self.f_max * (u - x), -self.rate, self.rate), + func_alg=lambda x, u, t: x, + initial_value=0.0 ) - self.op_alg = DynamicOperator( - func=_g_pid, - jac_x=_jac_x_g_pid, - jac_u=_jac_u_g_pid, + + + def __len__(self): + return 0 + + +class Backlash(DynamicalSystem): + """Backlash (mechanical play) element. + + Models the hysteresis-like behavior of mechanical backlash in gears, + couplings and other systems with play. The output only tracks the input + after the input has moved through the full backlash width. + + .. math:: + + \\dot{x} = f_\\mathrm{max} \\left((u - x) - \\mathrm{clip}(u - x,\\; -w/2,\\; w/2)\\right) + + where `w` is the total backlash width. Inside the dead zone :math:`|u - x| \\leq w/2` + the output does not move. Once the input pushes past the edge, the output + tracks with bandwidth `f_max`. + + + Example + ------- + The block is initialized like this: + + .. code-block:: python + + #backlash with 0.5 units of total play + bl = Backlash(width=0.5, f_max=1e3) + + + Parameters + ---------- + width : float + total backlash width (play) + f_max : float + tracking bandwidth parameter when engaged + """ + + input_port_labels = {"in": 0} + output_port_labels = {"out": 0} + + def __init__(self, width=1.0, f_max=100): + + #backlash parameters + self.width = width + self.f_max = f_max + + def _f_backlash(x, u, t): + gap = u - x + hw = self.width / 2.0 + return self.f_max * (gap - np.clip(gap, -hw, hw)) + + super().__init__( + func_dyn=_f_backlash, + func_alg=lambda x, u, t: x, + initial_value=0.0 + ) + + + def __len__(self): + return 0 + + +# ALGEBRAIC CONTROL BLOCKS ============================================================== + +class Deadband(Block): + """Deadband (dead zone) element. + + Outputs zero when the input is within the dead zone, and passes + the signal shifted by the zone boundary otherwise: + + .. math:: + + y = \\begin{cases} + u - u_\\mathrm{upper} & \\text{if } u > u_\\mathrm{upper} \\\\ + 0 & \\text{if } u_\\mathrm{lower} \\leq u \\leq u_\\mathrm{upper} \\\\ + u - u_\\mathrm{lower} & \\text{if } u < u_\\mathrm{lower} + \\end{cases} + + or equivalently :math:`y = u - \\mathrm{clip}(u,\\; u_\\mathrm{lower},\\; u_\\mathrm{upper})`. + + + Example + ------- + The block is initialized like this: + + .. code-block:: python + + #symmetric dead zone of width 0.2 + db = Deadband(lower=-0.1, upper=0.1) + + + Parameters + ---------- + lower : float + lower bound of the dead zone + upper : float + upper bound of the dead zone + """ + + input_port_labels = {"in": 0} + output_port_labels = {"out": 0} + + def __init__(self, lower=-1.0, upper=1.0): + super().__init__() + + #deadband parameters + self.lower = lower + self.upper = upper + + #algebraic operator + self.op_alg = Operator( + func=lambda u: u - np.clip(u, self.lower, self.upper), + jac=lambda u: np.diag(((u < self.lower) | (u > self.upper)).astype(float)) ) diff --git a/src/pathsim/blocks/delay.py b/src/pathsim/blocks/delay.py index 88baad10..4e6d0a4f 100644 --- a/src/pathsim/blocks/delay.py +++ b/src/pathsim/blocks/delay.py @@ -1,6 +1,6 @@ ######################################################################################### ## -## TIME DOMAIN DELAY BLOCK +## TIME DOMAIN DELAY BLOCK ## (blocks/delay.py) ## ######################################################################################### @@ -9,68 +9,120 @@ import numpy as np +from collections import deque + from ._block import Block from ..utils.adaptivebuffer import AdaptiveBuffer +from ..events.schedule import Schedule +from ..utils.mutable import mutable # BLOCKS ================================================================================ +@mutable class Delay(Block): - """Delays the input signal by a time constant 'tau' in seconds. + """Delays the input signal by a time constant 'tau' in seconds. + + Supports two modes of operation: - Mathematically this block creates a time delay of the input signal like this: + **Continuous mode** (default, ``sampling_period=None``): + Uses an adaptive interpolating buffer for continuous-time delay. .. math:: - - y(t) = + + y(t) = \\begin{cases} x(t - \\tau) & , t \\geq \\tau \\\\ 0 & , t < \\tau \\end{cases} + **Discrete mode** (``sampling_period`` provided): + Uses a ring buffer with scheduled sampling events for N-sample delay, + where ``N = round(tau / sampling_period)``. + + .. math:: + + y[k] = x[k - N] + Note ---- - The internal adaptive buffer uses interpolation for the evaluation. This is - required to be compatible with variable step solvers. It has a drawback however. - The order of the ode solver used will degrade when this block is used, due to - the interpolation. + In continuous mode, the internal adaptive buffer uses interpolation for + the evaluation. This is required to be compatible with variable step solvers. + It has a drawback however. The order of the ode solver used will degrade + when this block is used, due to the interpolation. + - Note ---- - This block supports vector input, meaning we can have multiple parallel + This block supports vector input, meaning we can have multiple parallel delay paths through this block. Example ------- - The block is initialized like this: + Continuous-time delay: .. code-block:: python - + #5 time units delay D = Delay(tau=5) - + + Discrete-time N-sample delay (10 samples): + + .. code-block:: python + + D = Delay(tau=0.01, sampling_period=0.001) + Parameters ---------- tau : float - delay time constant + delay time constant in seconds + sampling_period : float, None + sampling period for discrete mode, default is continuous mode Attributes ---------- _buffer : AdaptiveBuffer - internal interpolatable adaptive rolling buffer + internal interpolatable adaptive rolling buffer (continuous mode) + _ring : deque + internal ring buffer for N-sample delay (discrete mode) """ - def __init__(self, tau=1e-3): + def __init__(self, tau=1e-3, sampling_period=None): super().__init__() - #time delay in seconds + #time delay in seconds self.tau = tau - #create adaptive buffer - self._buffer = AdaptiveBuffer(self.tau) + #params for sampling + self.sampling_period = sampling_period + + if sampling_period is None: + + #continuous mode: adaptive buffer with interpolation + self._buffer = AdaptiveBuffer(self.tau) + + else: + + #discrete mode: ring buffer with N-sample delay + self._n = max(1, round(self.tau / self.sampling_period)) + self._ring = deque([0.0] * self._n, maxlen=self._n + 1) + + #flag to indicate this is a timestep to sample + self._sample_next_timestep = False + + #internal scheduled event for periodic sampling + def _sample(t): + self._sample_next_timestep = True + + self.events = [ + Schedule( + t_start=0, + t_period=sampling_period, + func_act=_sample + ) + ] def __len__(self): @@ -81,13 +133,51 @@ def __len__(self): def reset(self): super().reset() - #clear the buffer - self._buffer.clear() + if self.sampling_period is None: + #clear the adaptive buffer + self._buffer.clear() + else: + #clear the ring buffer + self._ring.clear() + self._ring.extend([0.0] * self._n) + + + def to_checkpoint(self, prefix, recordings=False): + """Serialize Delay state including buffer data.""" + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) + + json_data["sampling_period"] = self.sampling_period + + if self.sampling_period is None: + #continuous mode: adaptive buffer + npz_data.update(self._buffer.to_checkpoint(f"{prefix}/buffer")) + else: + #discrete mode: ring buffer + npz_data[f"{prefix}/ring"] = np.array(list(self._ring)) + json_data["_sample_next_timestep"] = self._sample_next_timestep + + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + """Restore Delay state including buffer data.""" + super().load_checkpoint(prefix, json_data, npz) + + if self.sampling_period is None: + #continuous mode + self._buffer.load_checkpoint(npz, f"{prefix}/buffer") + else: + #discrete mode + ring_key = f"{prefix}/ring" + if ring_key in npz: + self._ring.clear() + self._ring.extend(npz[ring_key].tolist()) + self._sample_next_timestep = json_data.get("_sample_next_timestep", False) def update(self, t): - """Evaluation of the buffer at different times - via interpolation. + """Evaluation of the buffer at different times + via interpolation (continuous) or ring buffer lookup (discrete). Parameters ---------- @@ -95,13 +185,17 @@ def update(self, t): evaluation time """ - #retrieve value from buffer - y = self._buffer.get(t) - self.outputs.update_from_array(y) + if self.sampling_period is None: + #continuous mode: retrieve value from buffer + y = self._buffer.get(t) + self.outputs.update_from_array(y) + else: + #discrete mode: output the oldest value in the ring buffer + self.outputs[0] = self._ring[0] def sample(self, t, dt): - """Sample input values and time of sampling + """Sample input values and time of sampling and add them to the buffer. Parameters @@ -112,5 +206,11 @@ def sample(self, t, dt): integration timestep """ - #add new value to buffer - self._buffer.add(t, self.inputs.to_array()) \ No newline at end of file + if self.sampling_period is None: + #continuous mode: add new value to buffer + self._buffer.add(t, self.inputs.to_array()) + else: + #discrete mode: only sample on scheduled events + if self._sample_next_timestep: + self._ring.append(self.inputs[0]) + self._sample_next_timestep = False \ No newline at end of file diff --git a/src/pathsim/blocks/divider.py b/src/pathsim/blocks/divider.py new file mode 100644 index 00000000..4e623170 --- /dev/null +++ b/src/pathsim/blocks/divider.py @@ -0,0 +1,218 @@ +######################################################################################### +## +## REDUCTION BLOCKS (blocks/divider.py) +## +## This module defines static 'Divider' block +## +######################################################################################### + +# IMPORTS =============================================================================== + +import numpy as np + +from math import prod + +from ._block import Block +from ..utils.register import Register +from ..optim.operator import Operator +from ..utils.mutable import mutable + + +# MISO BLOCKS =========================================================================== + +_ZERO_DIV_OPTIONS = ("warn", "raise", "clamp") + + +@mutable +class Divider(Block): + """Multiplies and divides input signals (MISO). + + This is the default behavior (multiply all): + + .. math:: + + y(t) = \\prod_i u_i(t) + + and this is the behavior with an operations string: + + .. math:: + + y(t) = \\frac{\\prod_{i \\in M} u_i(t)}{\\prod_{j \\in D} u_j(t)} + + where :math:`M` is the set of inputs with ``*`` and :math:`D` the set with ``/``. + + + Example + ------- + Default initialization multiplies the first input and divides by the second: + + .. code-block:: python + + D = Divider() + + Multiply the first two inputs and divide by the third: + + .. code-block:: python + + D = Divider('**/') + + Raise an error instead of producing ``inf`` when a denominator input is zero: + + .. code-block:: python + + D = Divider('**/', zero_div='raise') + + Clamp the denominator to machine epsilon so the output stays finite: + + .. code-block:: python + + D = Divider('**/', zero_div='clamp') + + + Note + ---- + This block is purely algebraic and its operation (``op_alg``) will be called + multiple times per timestep, each time when ``Simulation._update(t)`` is + called in the global simulation loop. + + + Parameters + ---------- + operations : str, optional + String of ``*`` and ``/`` characters indicating which inputs are + multiplied (``*``) or divided (``/``). Inputs beyond the length of + the string default to ``*``. Defaults to ``'*/'`` (divide second + input by first). + zero_div : str, optional + Behaviour when a denominator input is zero. One of: + + ``'warn'`` *(default)* + Propagates ``inf`` and emits a ``RuntimeWarning`` — numpy's + standard behaviour. + ``'raise'`` + Raises ``ZeroDivisionError``. + ``'clamp'`` + Clamps the denominator magnitude to machine epsilon + (``numpy.finfo(float).eps``), preserving sign, so the output + stays large-but-finite rather than ``inf``. + + + Attributes + ---------- + _ops : dict + Maps operation characters to exponent values (``+1`` or ``-1``). + _ops_array : numpy.ndarray + Exponents (+1 for ``*``, -1 for ``/``) converted to an array. + op_alg : Operator + Internal algebraic operator. + """ + + input_port_labels = None + output_port_labels = {"out": 0} + + def __init__(self, operations="*/", zero_div="warn"): + super().__init__() + + # validate zero_div + if zero_div not in _ZERO_DIV_OPTIONS: + raise ValueError( + f"'zero_div' must be one of {_ZERO_DIV_OPTIONS}, got '{zero_div}'" + ) + self.zero_div = zero_div + + # allowed arithmetic operations mapped to exponents + self._ops = {"*": 1, "/": -1} + self.operations = operations + + if self.operations is None: + + # Default: multiply all inputs — identical to Multiplier + self.op_alg = Operator( + func=prod, + jac=lambda x: np.array([[ + prod(np.delete(x, i)) for i in range(len(x)) + ]]) + ) + + else: + + # input validation + if not isinstance(self.operations, str): + raise ValueError("'operations' must be a string or None") + for op in self.operations: + if op not in self._ops: + raise ValueError( + f"operation '{op}' not in {set(self._ops)}" + ) + + self._ops_array = np.array( + [self._ops[op] for op in self.operations], dtype=float + ) + + # capture for closures + _ops_array = self._ops_array + _zero_div = zero_div + _eps = np.finfo(float).eps + + def _safe_den(d): + """Apply zero_div policy to a denominator value.""" + if d == 0: + if _zero_div == "raise": + raise ZeroDivisionError( + "Divider: denominator is zero. " + "Use zero_div='warn' or 'clamp' to suppress." + ) + elif _zero_div == "clamp": + return _eps + return d + + def prod_ops(X): + n = len(X) + no = len(_ops_array) + ops = np.ones(n) + ops[:min(n, no)] = _ops_array[:min(n, no)] + num = prod(X[i] for i in range(n) if ops[i] > 0) + den = _safe_den(prod(X[i] for i in range(n) if ops[i] < 0)) + return num / den + + def jac_ops(X): + n = len(X) + no = len(_ops_array) + ops = np.ones(n) + ops[:min(n, no)] = _ops_array[:min(n, no)] + X = np.asarray(X, dtype=float) + # Apply zero_div policy to all denominator inputs up front so + # both the direct division and the rest-product stay consistent. + X_safe = X.copy() + for i in range(n): + if ops[i] < 0: + X_safe[i] = _safe_den(float(X[i])) + row = [] + for k in range(n): + rest = np.prod( + np.power(np.delete(X_safe, k), np.delete(ops, k)) + ) + if ops[k] > 0: # multiply: dy/du_k = prod of rest + row.append(rest) + else: # divide: dy/du_k = -rest / u_k^2 + row.append(-rest / X_safe[k] ** 2) + return np.array([row]) + + self.op_alg = Operator(func=prod_ops, jac=jac_ops) + + + def __len__(self): + """Purely algebraic block.""" + return 1 + + + def update(self, t): + """Update system equation. + + Parameters + ---------- + t : float + Evaluation time. + """ + u = self.inputs.to_array() + self.outputs.update_from_array(self.op_alg(u)) diff --git a/src/pathsim/blocks/dynsys.py b/src/pathsim/blocks/dynsys.py index 5d292b09..6cee11ac 100644 --- a/src/pathsim/blocks/dynsys.py +++ b/src/pathsim/blocks/dynsys.py @@ -24,10 +24,10 @@ class DynamicalSystem(Block): .. math:: - \\begin{eqnarray} - \\dot{x}(t) =& \\mathrm{func}_\\mathrm{dyn}(x(t), u(t), t) \\\\ - y(t) =& \\mathrm{func}_\\mathrm{alg}(x(t), u(t), t) - \\end{eqnarray} + \\begin{align} + \\dot{x}(t) &= \\mathrm{func}_\\mathrm{dyn}(x(t), u(t), t) \\\\ + y(t) &= \\mathrm{func}_\\mathrm{alg}(x(t), u(t), t) + \\end{align} Parameters diff --git a/src/pathsim/blocks/filters.py b/src/pathsim/blocks/filters.py index ceff9cdc..6015df89 100644 --- a/src/pathsim/blocks/filters.py +++ b/src/pathsim/blocks/filters.py @@ -14,10 +14,12 @@ from .lti import StateSpace from ..utils.register import Register +from ..utils.mutable import mutable # FILTER BLOCKS ========================================================================= +@mutable class ButterworthLowpassFilter(StateSpace): """Direct implementation of a low pass butterworth filter block. @@ -52,6 +54,7 @@ def __init__(self, Fc=100, n=2): super().__init__(omega_c*A, omega_c*B, C, D) +@mutable class ButterworthHighpassFilter(StateSpace): """Direct implementation of a high pass butterworth filter block. @@ -85,6 +88,7 @@ def __init__(self, Fc=100, n=2): super().__init__(omega_c*A, omega_c*B, C, D) +@mutable class ButterworthBandpassFilter(StateSpace): """Direct implementation of a bandpass butterworth filter block. @@ -119,6 +123,7 @@ def __init__(self, Fc=[50, 100], n=2): super().__init__(*tf2ss(num, den)) +@mutable class ButterworthBandstopFilter(StateSpace): """Direct implementation of a bandstop butterworth filter block. @@ -153,6 +158,7 @@ def __init__(self, Fc=[50, 100], n=2): super().__init__(*tf2ss(num, den)) +@mutable class AllpassFilter(StateSpace): """Direct implementation of a first order allpass filter, or a cascade of n 1st order allpass filters diff --git a/src/pathsim/blocks/fir.py b/src/pathsim/blocks/fir.py index 5cdebd66..c9dc8ff7 100644 --- a/src/pathsim/blocks/fir.py +++ b/src/pathsim/blocks/fir.py @@ -10,13 +10,15 @@ import numpy as np from collections import deque -from ._block import Block +from ._block import Block from ..utils.register import Register -from ..events.schedule import Schedule +from ..events.schedule import Schedule +from ..utils.mutable import mutable # FIR FILTER BLOCK ====================================================================== +@mutable class FIR(Block): """Models a discrete-time Finite-Impulse-Response (FIR) filter. @@ -112,6 +114,22 @@ def reset(self): self._buffer = deque([0.0]*n, maxlen=n) + def to_checkpoint(self, prefix, recordings=False): + """Serialize FIR state including input buffer.""" + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) + npz_data[f"{prefix}/fir_buffer"] = np.array(list(self._buffer)) + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + """Restore FIR state including input buffer.""" + super().load_checkpoint(prefix, json_data, npz) + key = f"{prefix}/fir_buffer" + if key in npz: + self._buffer.clear() + self._buffer.extend(npz[key].tolist()) + + def __len__(self): """This block has no direct passthrough""" - return 0 \ No newline at end of file + return 0 diff --git a/src/pathsim/blocks/function.py b/src/pathsim/blocks/function.py index 246beea5..92ea90e9 100644 --- a/src/pathsim/blocks/function.py +++ b/src/pathsim/blocks/function.py @@ -64,7 +64,7 @@ def f(a, b, c): fn = Function(f) - then, when the block is uldated, the input channels of the block are + then, when the block is updated, the input channels of the block are assigned to the function arguments following this scheme: .. code-block:: diff --git a/src/pathsim/blocks/integrator.py b/src/pathsim/blocks/integrator.py index b706f5b0..d31abd17 100644 --- a/src/pathsim/blocks/integrator.py +++ b/src/pathsim/blocks/integrator.py @@ -30,10 +30,10 @@ class Integrator(Block): or in differential form like this: .. math:: - \\begin{eqnarray} + \\begin{align} \\dot{x}(t) &= u(t) \\\\ - y(t) &= x(t) - \\end{eqnarray} + y(t) &= x(t) + \\end{align} The Integrator block is inherently MIMO capable, so `u` and `y` can be vectors. diff --git a/src/pathsim/blocks/kalman.py b/src/pathsim/blocks/kalman.py index 783ae537..c835a7cc 100644 --- a/src/pathsim/blocks/kalman.py +++ b/src/pathsim/blocks/kalman.py @@ -143,6 +143,23 @@ def __len__(self): return 0 + def to_checkpoint(self, prefix, recordings=False): + """Serialize Kalman filter state estimate and covariance.""" + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) + npz_data[f"{prefix}/kf_x"] = self.x + npz_data[f"{prefix}/kf_P"] = self.P + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + """Restore Kalman filter state estimate and covariance.""" + super().load_checkpoint(prefix, json_data, npz) + if f"{prefix}/kf_x" in npz: + self.x = npz[f"{prefix}/kf_x"] + if f"{prefix}/kf_P" in npz: + self.P = npz[f"{prefix}/kf_P"] + + def _kf_update(self): """Perform one Kalman filter update step.""" diff --git a/src/pathsim/blocks/logic.py b/src/pathsim/blocks/logic.py new file mode 100644 index 00000000..31315b6f --- /dev/null +++ b/src/pathsim/blocks/logic.py @@ -0,0 +1,229 @@ +######################################################################################### +## +## COMPARISON AND LOGIC BLOCKS +## (blocks/logic.py) +## +## definitions of comparison and boolean logic blocks +## +######################################################################################### + +# IMPORTS =============================================================================== + +import numpy as np + +from ._block import Block + +from ..optim.operator import Operator + + +# BASE LOGIC BLOCK ====================================================================== + +class Logic(Block): + """Base logic block. + + Note + ---- + This block doesnt implement any functionality itself. + Its intended to be used as a base for the comparison and logic blocks. + Its **not** intended to be used directly! + + """ + + def __len__(self): + """Purely algebraic block""" + return 1 + + + def update(self, t): + """update algebraic component of system equation + + Parameters + ---------- + t : float + evaluation time + """ + u = self.inputs.to_array() + y = self.op_alg(u) + self.outputs.update_from_array(y) + + +# COMPARISON BLOCKS ===================================================================== + +class GreaterThan(Logic): + """Greater-than comparison block. + + Compares two inputs and outputs 1.0 if a > b, else 0.0. + + .. math:: + + y = + \\begin{cases} + 1 & , a > b \\\\ + 0 & , a \\leq b + \\end{cases} + + Attributes + ---------- + op_alg : Operator + internal algebraic operator + """ + + input_port_labels = {"a":0, "b":1} + output_port_labels = {"y":0} + + def __init__(self): + super().__init__() + + self.op_alg = Operator( + func=lambda x: float(x[0] > x[1]), + jac=lambda x: np.zeros((1, 2)) + ) + + +class LessThan(Logic): + """Less-than comparison block. + + Compares two inputs and outputs 1.0 if a < b, else 0.0. + + .. math:: + + y = + \\begin{cases} + 1 & , a < b \\\\ + 0 & , a \\geq b + \\end{cases} + + Attributes + ---------- + op_alg : Operator + internal algebraic operator + """ + + input_port_labels = {"a":0, "b":1} + output_port_labels = {"y":0} + + def __init__(self): + super().__init__() + + self.op_alg = Operator( + func=lambda x: float(x[0] < x[1]), + jac=lambda x: np.zeros((1, 2)) + ) + + +class Equal(Logic): + """Equality comparison block. + + Compares two inputs and outputs 1.0 if |a - b| <= tolerance, else 0.0. + + .. math:: + + y = + \\begin{cases} + 1 & , |a - b| \\leq \\epsilon \\\\ + 0 & , |a - b| > \\epsilon + \\end{cases} + + Parameters + ---------- + tolerance : float + comparison tolerance for floating point equality + + Attributes + ---------- + op_alg : Operator + internal algebraic operator + """ + + input_port_labels = {"a":0, "b":1} + output_port_labels = {"y":0} + + def __init__(self, tolerance=1e-12): + super().__init__() + + self.tolerance = tolerance + + self.op_alg = Operator( + func=lambda x: float(abs(x[0] - x[1]) <= self.tolerance), + jac=lambda x: np.zeros((1, 2)) + ) + + +# BOOLEAN LOGIC BLOCKS ================================================================== + +class LogicAnd(Logic): + """Logical AND block. + + Outputs 1.0 if both inputs are nonzero, else 0.0. + + .. math:: + + y = a \\land b + + Attributes + ---------- + op_alg : Operator + internal algebraic operator + """ + + input_port_labels = {"a":0, "b":1} + output_port_labels = {"y":0} + + def __init__(self): + super().__init__() + + self.op_alg = Operator( + func=lambda x: float(bool(x[0]) and bool(x[1])), + jac=lambda x: np.zeros((1, 2)) + ) + + +class LogicOr(Logic): + """Logical OR block. + + Outputs 1.0 if either input is nonzero, else 0.0. + + .. math:: + + y = a \\lor b + + Attributes + ---------- + op_alg : Operator + internal algebraic operator + """ + + input_port_labels = {"a":0, "b":1} + output_port_labels = {"y":0} + + def __init__(self): + super().__init__() + + self.op_alg = Operator( + func=lambda x: float(bool(x[0]) or bool(x[1])), + jac=lambda x: np.zeros((1, 2)) + ) + + +class LogicNot(Logic): + """Logical NOT block. + + Outputs 1.0 if input is zero, else 0.0. + + .. math:: + + y = \\lnot x + + Attributes + ---------- + op_alg : Operator + internal algebraic operator + """ + + def __init__(self): + super().__init__() + + self.op_alg = Operator( + func=lambda x: float(not bool(x[0])), + jac=lambda x: np.zeros((1, 1)) + ) diff --git a/src/pathsim/blocks/lti.py b/src/pathsim/blocks/lti.py index 25d64c1d..6a9cd23b 100644 --- a/src/pathsim/blocks/lti.py +++ b/src/pathsim/blocks/lti.py @@ -22,6 +22,7 @@ from ..utils.deprecation import deprecated from ..optim.operator import DynamicOperator +from ..utils.mutable import mutable # LTI BLOCKS ============================================================================ @@ -31,10 +32,10 @@ class StateSpace(Block): .. math:: - \\begin{eqnarray} + \\begin{align} \\dot{x} &= \\mathbf{A} x + \\mathbf{B} u \\\\ - y &= \\mathbf{C} x + \\mathbf{D} u - \\end{eqnarray} + y &= \\mathbf{C} x + \\mathbf{D} u + \\end{align} where `A`, `B`, `C` and `D` are the state space matrices, `x` is the state, `u` the input and `y` the output vector. @@ -169,6 +170,7 @@ def step(self, t, dt): return self.engine.step(f, dt) +@mutable class TransferFunctionPRC(StateSpace): """This block defines a LTI (MIMO for pole residue) transfer function. @@ -190,10 +192,10 @@ class TransferFunctionPRC(StateSpace): .. math:: - \\begin{eqnarray} + \\begin{align} \\dot{x} &= \\mathbf{A} x + \\mathbf{B} u \\\\ - y &= \\mathbf{C} x + \\mathbf{D} u - \\end{eqnarray} + y &= \\mathbf{C} x + \\mathbf{D} u + \\end{align} is handled the same as the 'StateSpace' block, where `A`, `B`, `C` and `D` are the state space matrices, `x` is the internal state, `u` the input and @@ -227,6 +229,7 @@ class TransferFunction(TransferFunctionPRC): pass +@mutable class TransferFunctionZPG(StateSpace): """This block defines a LTI (SISO) transfer function. @@ -247,10 +250,10 @@ class TransferFunctionZPG(StateSpace): .. math:: - \\begin{eqnarray} + \\begin{align} \\dot{x} &= \\mathbf{A} x + \\mathbf{B} u \\\\ - y &= \\mathbf{C} x + \\mathbf{D} u - \\end{eqnarray} + y &= \\mathbf{C} x + \\mathbf{D} u + \\end{align} is handled the same as the 'StateSpace' block, where `A`, `B`, `C` and `D` are the state space matrices, `x` is the internal state, `u` the input and @@ -281,6 +284,7 @@ def __init__(self, Zeros=[], Poles=[-1], Gain=1.0): super().__init__(sp_SS.A, sp_SS.B, sp_SS.C, sp_SS.D) +@mutable class TransferFunctionNumDen(StateSpace): """This block defines a LTI (SISO) transfer function. @@ -300,10 +304,10 @@ class TransferFunctionNumDen(StateSpace): .. math:: - \\begin{eqnarray} + \\begin{align} \\dot{x} &= \\mathbf{A} x + \\mathbf{B} u \\\\ - y &= \\mathbf{C} x + \\mathbf{D} u - \\end{eqnarray} + y &= \\mathbf{C} x + \\mathbf{D} u + \\end{align} is handled the same as the 'StateSpace' block, where `A`, `B`, `C` and `D` are the state space matrices, `x` is the internal state, `u` the input and diff --git a/src/pathsim/blocks/math.py b/src/pathsim/blocks/math.py index 6a08f927..aa134a28 100644 --- a/src/pathsim/blocks/math.py +++ b/src/pathsim/blocks/math.py @@ -15,6 +15,7 @@ from ..utils.register import Register from ..optim.operator import Operator +from ..utils.mutable import mutable # BASE MATH BLOCK ======================================================================= @@ -574,4 +575,154 @@ def __init__(self, A=np.eye(1)): self.op_alg = Operator( func=lambda u: np.dot(self.A, u), jac=lambda u: self.A + ) + + +class Atan2(Block): + """Two-argument arctangent block. + + Computes the four-quadrant arctangent of two inputs: + + .. math:: + + y = \\mathrm{atan2}(a, b) + + Note + ---- + This block takes exactly two inputs (a, b) and produces one output. + The first input is the y-coordinate, the second is the x-coordinate, + matching the convention of ``numpy.arctan2(y, x)``. + + Attributes + ---------- + op_alg : Operator + internal algebraic operator + """ + + input_port_labels = {"a":0, "b":1} + output_port_labels = {"y":0} + + def __init__(self): + super().__init__() + + def _atan2_jac(x): + a, b = x[0], x[1] + denom = a**2 + b**2 + if denom == 0: + return np.zeros((1, 2)) + return np.array([[b / denom, -a / denom]]) + + self.op_alg = Operator( + func=lambda x: np.arctan2(x[0], x[1]), + jac=_atan2_jac + ) + + + def __len__(self): + """Purely algebraic block""" + return 1 + + + def update(self, t): + """update algebraic component of system equation + + Parameters + ---------- + t : float + evaluation time + """ + u = self.inputs.to_array() + y = self.op_alg(u) + self.outputs.update_from_array(y) + + +@mutable +class Rescale(Math): + """Linear rescaling / mapping block. + + Maps the input linearly from range ``[i0, i1]`` to range ``[o0, o1]``. + Optionally saturates the output to ``[o0, o1]``. + + .. math:: + + y = o_0 + \\frac{(x - i_0) \\cdot (o_1 - o_0)}{i_1 - i_0} + + This block supports vector inputs. + + Parameters + ---------- + i0 : float + input range lower bound + i1 : float + input range upper bound + o0 : float + output range lower bound + o1 : float + output range upper bound + saturate : bool + if True, clamp output to [min(o0,o1), max(o0,o1)] + + Attributes + ---------- + op_alg : Operator + internal algebraic operator + """ + + def __init__(self, i0=0.0, i1=1.0, o0=0.0, o1=1.0, saturate=False): + super().__init__() + + self.i0 = i0 + self.i1 = i1 + self.o0 = o0 + self.o1 = o1 + self.saturate = saturate + + #precompute gain + self._gain = (o1 - o0) / (i1 - i0) + + def _maplin(x): + y = self.o0 + (x - self.i0) * self._gain + if self.saturate: + lo, hi = min(self.o0, self.o1), max(self.o0, self.o1) + y = np.clip(y, lo, hi) + return y + + def _maplin_jac(x): + if self.saturate: + lo, hi = min(self.o0, self.o1), max(self.o0, self.o1) + y = self.o0 + (x - self.i0) * self._gain + mask = (y >= lo) & (y <= hi) + return np.diag(mask.astype(float) * self._gain) + return np.diag(np.full_like(x, self._gain)) + + self.op_alg = Operator( + func=_maplin, + jac=_maplin_jac + ) + + +class Alias(Math): + """Signal alias / pass-through block. + + Passes the input directly to the output without modification. + This is useful for signal renaming in model composition. + + .. math:: + + y = x + + This block supports vector inputs. + + Attributes + ---------- + op_alg : Operator + internal algebraic operator + """ + + def __init__(self): + super().__init__() + + self.op_alg = Operator( + func=lambda x: x, + jac=lambda x: np.eye(len(x)) ) \ No newline at end of file diff --git a/src/pathsim/blocks/noise.py b/src/pathsim/blocks/noise.py index 101828ea..0206a1b6 100644 --- a/src/pathsim/blocks/noise.py +++ b/src/pathsim/blocks/noise.py @@ -124,6 +124,19 @@ def update(self, t): pass + def to_checkpoint(self, prefix, recordings=False): + """Serialize WhiteNoise state including current sample.""" + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) + json_data["_current_sample"] = float(self._current_sample) + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + """Restore WhiteNoise state including current sample.""" + super().load_checkpoint(prefix, json_data, npz) + self._current_sample = json_data.get("_current_sample", 0.0) + + class PinkNoise(Block): """Pink noise (1/f noise) source using the Voss-McCartney algorithm. @@ -268,4 +281,22 @@ def sample(self, t, dt): def update(self, t): - pass \ No newline at end of file + pass + + + def to_checkpoint(self, prefix, recordings=False): + """Serialize PinkNoise state including algorithm state.""" + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) + json_data["n_samples"] = self.n_samples + json_data["_current_sample"] = float(self._current_sample) + npz_data[f"{prefix}/octave_values"] = self.octave_values + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + """Restore PinkNoise state including algorithm state.""" + super().load_checkpoint(prefix, json_data, npz) + self.n_samples = json_data.get("n_samples", 0) + self._current_sample = json_data.get("_current_sample", 0.0) + if f"{prefix}/octave_values" in npz: + self.octave_values = npz[f"{prefix}/octave_values"] \ No newline at end of file diff --git a/src/pathsim/blocks/ode.py b/src/pathsim/blocks/ode.py index a89e5c04..5eb282dc 100644 --- a/src/pathsim/blocks/ode.py +++ b/src/pathsim/blocks/ode.py @@ -23,10 +23,10 @@ class ODE(Block): .. math:: - \\begin{eqnarray} - \\dot{x}(t) =& \\mathrm{func}(x(t), u(t), t) \\\\ - y(t) =& x(t) - \\end{eqnarray} + \\begin{align} + \\dot{x}(t) &= \\mathrm{func}(x(t), u(t), t) \\\\ + y(t) &= x(t) + \\end{align} with inhomogenity (input) `u` and state vector `x`. The function can be nonlinear and the ODE can be of arbitrary order. The block utilizes the integration engine diff --git a/src/pathsim/blocks/rf.py b/src/pathsim/blocks/rf.py index 51ac9e00..4a82c5db 100644 --- a/src/pathsim/blocks/rf.py +++ b/src/pathsim/blocks/rf.py @@ -8,13 +8,8 @@ ## ######################################################################################### -# TODO LIST -# class RFAmplifier Model amplifier in RF systems -# class Resistor/Capacitor/Inductor -# class RFMixer for mixer in RF systems? - - # IMPORTS =============================================================================== + from __future__ import annotations import numpy as np @@ -38,10 +33,17 @@ from .lti import StateSpace +from ..utils.deprecation import deprecated + # BLOCK DEFINITIONS ===================================================================== +@deprecated( + version="1.0.0", + replacement="pathsim_rf.RFNetwork", + reason="This block has moved to the pathsim-rf package: pip install pathsim-rf", +) class RFNetwork(StateSpace): """N-port RF network linear time invariant (LTI) multi input multi output (MIMO) state-space model. @@ -78,7 +80,7 @@ def __init__(self, ntwk: NetworkType | str | Path, auto_fit: bool = True, **kwar _msg = "The scikit-rf package is required to use this block -> 'pip install scikit-rf'" raise ImportError(_msg) - if isinstance(ntwk, Path) or isinstance(ntwk, str): + if isinstance(ntwk, (Path, str)): ntwk = rf.Network(ntwk) # Select the vector fitting function from scikit-rf diff --git a/src/pathsim/blocks/rng.py b/src/pathsim/blocks/rng.py index 5841b5a5..72824107 100644 --- a/src/pathsim/blocks/rng.py +++ b/src/pathsim/blocks/rng.py @@ -96,6 +96,21 @@ def sample(self, t, dt): self._sample = np.random.rand() + def to_checkpoint(self, prefix, recordings=False): + """Serialize RNG state including current sample.""" + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) + if self.sampling_period is None: + json_data["_sample"] = float(self._sample) + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + """Restore RNG state including current sample.""" + super().load_checkpoint(prefix, json_data, npz) + if self.sampling_period is None: + self._sample = json_data.get("_sample", 0.0) + + def __len__(self): """Essentially a source-like block without passthrough""" return 0 diff --git a/src/pathsim/blocks/samplehold.py b/src/pathsim/blocks/samplehold.py index ae3e5b96..a292877c 100644 --- a/src/pathsim/blocks/samplehold.py +++ b/src/pathsim/blocks/samplehold.py @@ -9,10 +9,12 @@ from ._block import Block from ..events.schedule import Schedule +from ..utils.mutable import mutable # MIXED SIGNAL BLOCKS =================================================================== +@mutable class SampleHold(Block): """Samples the inputs periodically and produces them at the output. diff --git a/src/pathsim/blocks/scope.py b/src/pathsim/blocks/scope.py index 4997f772..ec980785 100644 --- a/src/pathsim/blocks/scope.py +++ b/src/pathsim/blocks/scope.py @@ -448,13 +448,44 @@ def save(self, path="scope.csv"): wrt.writerow(sample) + def to_checkpoint(self, prefix, recordings=False): + """Serialize Scope state including optional recording data.""" + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) + + json_data["_incremental_idx"] = self._incremental_idx + if hasattr(self, '_sample_next_timestep'): + json_data["_sample_next_timestep"] = self._sample_next_timestep + + if recordings and self.recording_time: + npz_data[f"{prefix}/recording_time"] = np.array(self.recording_time) + npz_data[f"{prefix}/recording_data"] = np.array(self.recording_data) + + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + """Restore Scope state including optional recording data.""" + super().load_checkpoint(prefix, json_data, npz) + + self._incremental_idx = json_data.get("_incremental_idx", 0) + if hasattr(self, '_sample_next_timestep'): + self._sample_next_timestep = json_data.get("_sample_next_timestep", False) + + #restore recordings if present + rt_key = f"{prefix}/recording_time" + rd_key = f"{prefix}/recording_data" + if rt_key in npz and rd_key in npz: + self.recording_time = npz[rt_key].tolist() + self.recording_data = [row for row in npz[rd_key]] + + def update(self, t): - """update system equation for fixed point loop, + """update system equation for fixed point loop, here just setting the outputs - + Note ---- - Scope has no passthrough, so the 'update' method + Scope has no passthrough, so the 'update' method is optimized for this case (does nothing) Parameters diff --git a/src/pathsim/blocks/sources.py b/src/pathsim/blocks/sources.py index 170514b9..a2abfcea 100644 --- a/src/pathsim/blocks/sources.py +++ b/src/pathsim/blocks/sources.py @@ -14,6 +14,7 @@ from ._block import Block from ..utils.register import Register from ..utils.deprecation import deprecated +from ..utils.mutable import mutable from ..events.schedule import Schedule, ScheduleList from .._constants import TOLERANCE @@ -169,6 +170,7 @@ def update(self, t): # SPECIAL CONTINUOUS SOURCE BLOCKS ====================================================== +@mutable class TriangleWaveSource(Source): """Source block that generates an analog triangle wave @@ -214,6 +216,7 @@ def _triangle_wave(self, t, f): return 2 * abs(t*f - np.floor(t*f + 0.5)) - 1 +@mutable class SinusoidalSource(Source): """Source block that generates a sinusoid wave @@ -289,6 +292,7 @@ def _gaussian(self, t, f_max): return np.exp(-(t/tau)**2) +@mutable class SinusoidalPhaseNoiseSource(Block): """Sinusoidal source with cumulative and white phase noise. @@ -703,6 +707,7 @@ class ChirpSource(ChirpPhaseNoiseSource): # SPECIAL DISCRETE SOURCE BLOCKS ======================================================== +@mutable class PulseSource(Block): """Generates a periodic pulse waveform with defined rise and fall times. @@ -909,6 +914,7 @@ class Pulse(PulseSource): pass +@mutable class ClockSource(Block): """Discrete time clock source block. @@ -970,6 +976,7 @@ class Clock(ClockSource): +@mutable class SquareWaveSource(Block): """Discrete time square wave source. diff --git a/src/pathsim/blocks/spectrum.py b/src/pathsim/blocks/spectrum.py index 24268fe9..0dec61fe 100644 --- a/src/pathsim/blocks/spectrum.py +++ b/src/pathsim/blocks/spectrum.py @@ -15,12 +15,14 @@ from ..utils.realtimeplotter import RealtimePlotter from ..utils.deprecation import deprecated +from ..utils.mutable import mutable from .._constants import COLORS_ALL # BLOCKS FOR DATA RECORDING ============================================================= +@mutable class Spectrum(Block): """Block for fourier spectrum analysis (spectrum analyzer). @@ -281,6 +283,24 @@ def step(self, t, dt): return True, 0.0, None + def to_checkpoint(self, prefix, recordings=False): + """Serialize Spectrum state including integration time.""" + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) + + json_data["time"] = self.time + json_data["t_sample"] = self.t_sample + + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + """Restore Spectrum state including integration time.""" + super().load_checkpoint(prefix, json_data, npz) + + self.time = json_data.get("time", 0.0) + self.t_sample = json_data.get("t_sample", 0.0) + + def sample(self, t, dt): """sample time of successfull timestep for waiting period diff --git a/src/pathsim/blocks/switch.py b/src/pathsim/blocks/switch.py index e21a8ca5..f89f28ca 100644 --- a/src/pathsim/blocks/switch.py +++ b/src/pathsim/blocks/switch.py @@ -30,23 +30,23 @@ class Switch(Block): #change the state of the switch to port 3 s2.select(3) - Sets block output depending on `self.state` like this: + Sets block output depending on `self.switch_state` like this: .. code-block:: - state == None -> outputs[0] = 0 + switch_state == None -> outputs[0] = 0 - state == 0 -> outputs[0] = inputs[0] + switch_state == 0 -> outputs[0] = inputs[0] - state == 1 -> outputs[0] = inputs[1] + switch_state == 1 -> outputs[0] = inputs[1] - state == 2 -> outputs[0] = inputs[2] + switch_state == 2 -> outputs[0] = inputs[2] ... Parameters ---------- - state : int, None + switch_state : int, None state of the switch """ @@ -54,18 +54,18 @@ class Switch(Block): input_port_labels = None output_port_labels = {"out":0} - def __init__(self, state=None): + def __init__(self, switch_state=None): super().__init__() - self.state = state + self.switch_state = switch_state def __len__(self): - """Algebraic passthrough only possible if state is defined""" - return 0 if (self.state is None or not self._active) else 1 + """Algebraic passthrough only possible if switch_state is defined""" + return 0 if (self.switch_state is None or not self._active) else 1 - def select(self, state=0): + def select(self, switch_state=0): """ This method is unique to the `Switch` block and intended to be used from outside the simulation level for selecting @@ -76,12 +76,22 @@ def select(self, state=0): Parameters --------- - state : int, None + switch_state : int, None switch state / input port selection """ - self.state = state + self.switch_state = switch_state + def to_checkpoint(self, prefix, recordings=False): + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) + json_data["switch_state"] = self.switch_state + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + super().load_checkpoint(prefix, json_data, npz) + self.switch_state = json_data.get("switch_state", None) + def update(self, t): """Update switch output depending on inputs and switch state. @@ -92,5 +102,5 @@ def update(self, t): """ #early exit without error control - if self.state is None: self.outputs[0] = 0.0 - else: self.outputs[0] = self.inputs[self.state] + if self.switch_state is None: self.outputs[0] = 0.0 + else: self.outputs[0] = self.inputs[self.switch_state] diff --git a/src/pathsim/connection.py b/src/pathsim/connection.py index 85d72023..b021eb72 100644 --- a/src/pathsim/connection.py +++ b/src/pathsim/connection.py @@ -206,7 +206,7 @@ def _validate_dimensions(self): def _validate_ports(self): - """Check the existance of the input and output ports of + """Check the existence of the input and output ports of the defined source and target blocks. Utilizes the `PortReference._validate_output_ports` and diff --git a/src/pathsim/events/_event.py b/src/pathsim/events/_event.py index 3365a69e..1ebd1a9e 100644 --- a/src/pathsim/events/_event.py +++ b/src/pathsim/events/_event.py @@ -157,7 +157,7 @@ def estimate(self, t): def detect(self, t): - """Evaluate the event function and decide if an event has occured. + """Evaluate the event function and decide if an event has occurred. Can also use the history of the event function evaluation from before the timestep. @@ -201,4 +201,62 @@ def resolve(self, t): #action function for event resolution if self.func_act is not None: - self.func_act(t) \ No newline at end of file + self.func_act(t) + + + # checkpoint methods ---------------------------------------------------------------- + + def to_checkpoint(self, prefix): + """Serialize event state for checkpointing. + + Parameters + ---------- + prefix : str + key prefix for NPZ arrays (assigned by simulation) + + Returns + ------- + json_data : dict + JSON-serializable metadata + npz_data : dict + numpy arrays keyed by path + """ + #extract history eval value + hist_eval, hist_time = self._history + if hist_eval is not None and hasattr(hist_eval, 'item'): + hist_eval = float(hist_eval) + + json_data = { + "type": self.__class__.__name__, + "active": self._active, + "history_eval": hist_eval, + "history_time": hist_time, + } + + npz_data = {} + if self._times: + npz_data[f"{prefix}/times"] = np.array(self._times) + + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + """Restore event state from checkpoint. + + Parameters + ---------- + prefix : str + key prefix for NPZ arrays (assigned by simulation) + json_data : dict + event metadata from checkpoint JSON + npz : dict-like + numpy arrays from checkpoint NPZ + """ + self._active = json_data["active"] + self._history = json_data["history_eval"], json_data["history_time"] + + times_key = f"{prefix}/times" + if times_key in npz: + self._times = npz[times_key].tolist() + else: + self._times = [] diff --git a/src/pathsim/optim/anderson.py b/src/pathsim/optim/anderson.py index 68ddfc04..96a162eb 100644 --- a/src/pathsim/optim/anderson.py +++ b/src/pathsim/optim/anderson.py @@ -21,22 +21,44 @@ # CLASS ================================================================================ class Anderson: - """Class for accelerated fixed-point iteration through anderson acceleration. - Solves a nonlinear set of equations given in the fixed-point form: + """Anderson acceleration for fixed-point iteration. - x = g(x) + Solves nonlinear equations in fixed-point form :math:`x = g(x)` by + computing the next iterate as a linear combination of previous iterates + whose coefficients minimise the least-squares residual. - Anderson Accelerstion tracks the evolution of the solution from the previous - iterations. The next step in the iteration is computed as a linear combination - of the previous iterates. The coefficients are computed to minimize the least - squares error of the fixed-point problem. + .. math:: + + x_{k+1} = \\sum_{i=0}^{m_k} \\alpha_i^{(k)}\\, g(x_{k-m_k+i}) + \\quad\\text{with}\\quad + \\alpha^{(k)} = \\arg\\min \\bigl\\|\\sum_i \\alpha_i\\, r_{k-m_k+i}\\bigr\\| + + where :math:`r_k = g(x_k) - x_k` and :math:`m_k \\le m` is the current + buffer depth. + + In PathSim this class is the inner fixed-point solver used by the + simulation engine to resolve algebraic loops (cycles in the block + diagram). Each loop-closing ``ConnectionBooster`` owns an ``Anderson`` + instance that accelerates convergence of the fixed-point iteration + over the loop. The buffer depth ``m`` controls how many previous + iterates are retained; larger values improve convergence on difficult + loops at the cost of a small least-squares solve per iteration. Parameters ---------- m : int - buffer length + buffer depth (number of stored iterates) restart : bool - clear buffer when full + if True, clear the buffer once it reaches depth ``m`` + + References + ---------- + .. [1] Anderson, D. G. (1965). "Iterative Procedures for Nonlinear + Integral Equations". Journal of the ACM, 12(4), 547--560. + :doi:`10.1145/321296.321305` + .. [2] Walker, H. F., & Ni, P. (2011). "Anderson Acceleration for + Fixed-Point Iterations". SIAM Journal on Numerical Analysis, + 49(4), 1715--1735. :doi:`10.1137/10078356X` """ def __init__(self, m=OPT_HISTORY, restart=OPT_RESTART): @@ -196,12 +218,34 @@ def step(self, x, g): class NewtonAnderson(Anderson): - """Modified class for hybrid anderson acceleration that can use a jacobian 'jac' of - the function 'g' for a newton step before the fixed point step for the initial - estimate before applying the anderson acceleration. + """Hybrid Newton--Anderson fixed-point solver. + + Extends :class:`Anderson` by prepending a Newton step when a Jacobian + of :math:`g` is available. The Newton step - If a jacobian 'jac' is available, this significantly improves the convergence - (speed and robustness) of the solution. + .. math:: + + \\tilde{x} = x - (J_g - I)^{-1}\\,(g(x) - x) + + provides a quadratically convergent initial correction; the subsequent + Anderson mixing step then stabilises the iteration and damps + oscillations. + + In PathSim this solver is used inside every implicit ODE integration + engine (BDF, DIRK, ESDIRK). When a block provides a local Jacobian + (e.g. ``ODE`` or ``LTI`` blocks), the Newton pre-step yields much + faster convergence of the implicit update equation, reducing the + number of fixed-point iterations per timestep. Without a Jacobian the + solver falls back to pure Anderson acceleration. + + References + ---------- + .. [1] Anderson, D. G. (1965). "Iterative Procedures for Nonlinear + Integral Equations". Journal of the ACM, 12(4), 547--560. + :doi:`10.1145/321296.321305` + .. [2] Walker, H. F., & Ni, P. (2011). "Anderson Acceleration for + Fixed-Point Iterations". SIAM Journal on Numerical Analysis, + 49(4), 1715--1735. :doi:`10.1137/10078356X` """ diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index ba38e450..11f08aff 100644 --- a/src/pathsim/simulation.py +++ b/src/pathsim/simulation.py @@ -10,6 +10,9 @@ # IMPORTS =============================================================================== +import json +import warnings + import numpy as np import time @@ -34,6 +37,7 @@ from .utils.deprecation import deprecated from .utils.portreference import PortReference from .utils.progresstracker import ProgressTracker +from .utils.diagnostics import Diagnostics, ConvergenceTracker, StepTracker from .utils.logger import LoggerManager from .solvers import SSPRK22, SteadyState @@ -153,33 +157,34 @@ class Simulation: get attributes and access to intermediate evaluation stages logger : logging.Logger global simulation logger - _blocks_dyn : set[Block] - blocks with internal ´Solver´ instances (stateful) - _blocks_evt : set[Block] - blocks with internal events (discrete time, eventful) + _blocks_dyn : list[Block] + blocks with internal ´Solver´ instances (stateful) + _blocks_evt : list[Block] + blocks with internal events (discrete time, eventful) _active : bool flag for setting the simulation as active, used for interrupts """ def __init__( - self, - blocks=None, - connections=None, + self, + blocks=None, + connections=None, events=None, - dt=SIM_TIMESTEP, - dt_min=SIM_TIMESTEP_MIN, - dt_max=SIM_TIMESTEP_MAX, - Solver=SSPRK22, - tolerance_fpi=SIM_TOLERANCE_FPI, - iterations_max=SIM_ITERATIONS_MAX, + dt=SIM_TIMESTEP, + dt_min=SIM_TIMESTEP_MIN, + dt_max=SIM_TIMESTEP_MAX, + Solver=SSPRK22, + tolerance_fpi=SIM_TOLERANCE_FPI, + iterations_max=SIM_ITERATIONS_MAX, log=LOG_ENABLE, + diagnostics=False, **solver_kwargs ): #system definition - self.blocks = set() - self.connections = set() - self.events = set() + self.blocks = [] + self.connections = [] + self.events = [] #simulation timestep and bounds self.dt = dt @@ -194,6 +199,7 @@ def __init__( #internal system graph -> initialized later self.graph = None + self._graph_dirty = False #internal algebraic loop solvers -> initialized later self.boosters = None @@ -214,14 +220,25 @@ def __init__( self.time = 0.0 #collection of blocks with internal ODE solvers - self._blocks_dyn = set() + self._blocks_dyn = [] #collection of blocks with internal events - self._blocks_evt = set() + self._blocks_evt = [] #flag for setting the simulation active self._active = True + #convergence trackers for the three solver loops + self._loop_tracker = ConvergenceTracker() + self._solve_tracker = ConvergenceTracker() + self._step_tracker = StepTracker() + + #diagnostics snapshot (None when disabled) + self.diagnostics = Diagnostics() if diagnostics else None + + #diagnostics history (list of snapshots per timestep) + self._diagnostics_history = [] if diagnostics == "history" else None + #initialize logging logger_mgr = LoggerManager( enabled=bool(self.log), @@ -235,12 +252,12 @@ def __init__( #prepare and add blocks (including internal events) if blocks is not None: for block in blocks: - self.add_block(block, _defer_graph=True) + self.add_block(block) #check and add connections if connections is not None: for connection in connections: - self.add_connection(connection, _defer_graph=True) + self.add_connection(connection) #check and add events if events is not None: @@ -268,8 +285,8 @@ def __contains__(self, other): bool """ return ( - other in self.blocks or - other in self.connections or + other in self.blocks or + other in self.connections or other in self.events ) @@ -330,20 +347,183 @@ def plot(self, *args, **kwargs): if block: block.plot(*args, **kwargs) + # checkpoint methods ---------------------------------------------------------- + + @staticmethod + def _checkpoint_key(type_name, type_counts): + """Generate a deterministic checkpoint key from block/event type + and occurrence index (e.g. 'Integrator_0', 'Scope_1'). + + Parameters + ---------- + type_name : str + class name of the block or event + type_counts : dict + running counter per type name, mutated in place + + Returns + ------- + key : str + deterministic checkpoint key + """ + idx = type_counts.get(type_name, 0) + type_counts[type_name] = idx + 1 + return f"{type_name}_{idx}" + + + def save_checkpoint(self, path, recordings=True): + """Save simulation state to checkpoint files (JSON + NPZ). + + Creates two files: {path}.json (structure/metadata) and + {path}.npz (numerical data). Blocks and events are keyed by + type and insertion order for deterministic cross-instance matching. + + Parameters + ---------- + path : str + base path without extension + recordings : bool + include scope/spectrum recording data (default: True) + """ + #strip extension if provided + if path.endswith('.json') or path.endswith('.npz'): + path = path.rsplit('.', 1)[0] + + #simulation metadata + checkpoint = { + "version": "1.0.0", + "pathsim_version": __version__, + "created": datetime.datetime.now(datetime.timezone.utc).isoformat(), + "simulation": { + "time": self.time, + "dt": self.dt, + "dt_min": self.dt_min, + "dt_max": self.dt_max, + "solver": self.Solver.__name__, + "tolerance_fpi": self.tolerance_fpi, + "iterations_max": self.iterations_max, + }, + "blocks": [], + "events": [], + } + + npz_data = {} + + #checkpoint all blocks (keyed by type + insertion index) + type_counts = {} + for block in self.blocks: + key = self._checkpoint_key(block.__class__.__name__, type_counts) + b_json, b_npz = block.to_checkpoint(key, recordings=recordings) + b_json["_key"] = key + checkpoint["blocks"].append(b_json) + npz_data.update(b_npz) + + #checkpoint external events (keyed by type + insertion index) + type_counts = {} + for event in self.events: + key = self._checkpoint_key(event.__class__.__name__, type_counts) + e_json, e_npz = event.to_checkpoint(key) + e_json["_key"] = key + checkpoint["events"].append(e_json) + npz_data.update(e_npz) + + #write files + with open(f"{path}.json", "w", encoding="utf-8") as f: + json.dump(checkpoint, f, indent=2, ensure_ascii=False) + + np.savez(f"{path}.npz", **npz_data) + + + def load_checkpoint(self, path): + """Load simulation state from checkpoint files (JSON + NPZ). + + Restores simulation time and all block/event states from a + previously saved checkpoint. Matching is based on block/event + type and insertion order, so the simulation must be constructed + with the same block types in the same order. + + Parameters + ---------- + path : str + base path without extension + """ + #strip extension if provided + if path.endswith('.json') or path.endswith('.npz'): + path = path.rsplit('.', 1)[0] + + #read files + with open(f"{path}.json", "r", encoding="utf-8") as f: + checkpoint = json.load(f) + + npz = np.load(f"{path}.npz", allow_pickle=False) + + try: + #version check + cp_version = checkpoint.get("pathsim_version", "unknown") + if cp_version != __version__: + warnings.warn( + f"Checkpoint was saved with PathSim {cp_version}, " + f"current version is {__version__}" + ) + + #restore simulation state + sim_data = checkpoint["simulation"] + self.time = sim_data["time"] + self.dt = sim_data["dt"] + self.dt_min = sim_data["dt_min"] + self.dt_max = sim_data["dt_max"] + + #solver type check + if sim_data["solver"] != self.Solver.__name__: + warnings.warn( + f"Checkpoint solver '{sim_data['solver']}' differs from " + f"current solver '{self.Solver.__name__}'" + ) + + #index checkpoint blocks by key + block_data = {b["_key"]: b for b in checkpoint.get("blocks", [])} + + #restore blocks by type + insertion order + type_counts = {} + for block in self.blocks: + key = self._checkpoint_key(block.__class__.__name__, type_counts) + if key in block_data: + block.load_checkpoint(key, block_data[key], npz) + else: + warnings.warn( + f"Block '{key}' not found in checkpoint" + ) + + #index checkpoint events by key + event_data = {e["_key"]: e for e in checkpoint.get("events", [])} + + #restore external events by type + insertion order + type_counts = {} + for event in self.events: + key = self._checkpoint_key(event.__class__.__name__, type_counts) + if key in event_data: + event.load_checkpoint(key, event_data[key], npz) + else: + warnings.warn( + f"Event '{key}' not found in checkpoint" + ) + + finally: + npz.close() + + # adding system components ---------------------------------------------------- - def add_block(self, block, _defer_graph=False): - """Adds a new block to the simulation, initializes its local solver - instance and collects internal events of the new block. + def add_block(self, block): + """Adds a new block to the simulation, initializes its local solver + instance and collects internal events of the new block. This works dynamically for running simulations. Parameters ---------- - block : Block + block : Block block to add to the simulation - _defer_graph : bool - flag for defering graph construction to a later stage """ #check if block already in block list @@ -357,22 +537,56 @@ def add_block(self, block, _defer_graph=False): #add to dynamic list if solver was initialized if block.engine: - self._blocks_dyn.add(block) + self._blocks_dyn.append(block) #add to eventful list if internal events if block.events: - self._blocks_evt.add(block) + self._blocks_evt.append(block) #add block to global blocklist - self.blocks.add(block) + self.blocks.append(block) - #if graph already exists, it needs to be rebuilt - if not _defer_graph and self.graph: - self._assemble_graph() + #mark graph for rebuild + if self.graph: + self._graph_dirty = True + + + def remove_block(self, block): + """Removes a block from the simulation. + + This works dynamically for running simulations. The graph + is lazily rebuilt on the next simulation update. + + Parameters + ---------- + block : Block + block to remove from the simulation + """ + + #check if block is in block list + if block not in self.blocks: + _msg = f"block {block} not part of simulation" + self.logger.error(_msg) + raise ValueError(_msg) + + #remove from global blocklist + self.blocks.remove(block) + + #remove from dynamic list + if block in self._blocks_dyn: + self._blocks_dyn.remove(block) + + #remove from eventful list + if block in self._blocks_evt: + self._blocks_evt.remove(block) + #mark graph for rebuild + if self.graph: + self._graph_dirty = True - def add_connection(self, connection, _defer_graph=False): - """Adds a new connection to the simulaiton and checks if + + def add_connection(self, connection): + """Adds a new connection to the simulation and checks if the new connection overwrites any existing connections. This works dynamically for running simulations. @@ -381,8 +595,6 @@ def add_connection(self, connection, _defer_graph=False): ---------- connection : Connection connection to add to the simulation - _defer_graph : bool - flag for defering graph construction to a later stage """ #check if connection already in connection list @@ -392,11 +604,37 @@ def add_connection(self, connection, _defer_graph=False): raise ValueError(_msg) #add connection to global connection list - self.connections.add(connection) + self.connections.append(connection) - #if graph already exists, it needs to be rebuilt - if not _defer_graph and self.graph: - self._assemble_graph() + #mark graph for rebuild + if self.graph: + self._graph_dirty = True + + + def remove_connection(self, connection): + """Removes a connection from the simulation. + + This works dynamically for running simulations. The graph + is lazily rebuilt on the next simulation update. + + Parameters + ---------- + connection : Connection + connection to remove from the simulation + """ + + #check if connection is in connection list + if connection not in self.connections: + _msg = f"{connection} not part of simulation" + self.logger.error(_msg) + raise ValueError(_msg) + + #remove from global connection list + self.connections.remove(connection) + + #mark graph for rebuild + if self.graph: + self._graph_dirty = True def add_event(self, event): @@ -417,19 +655,45 @@ def add_event(self, event): raise ValueError(_msg) #add event to global event list - self.events.add(event) + self.events.append(event) + + + def remove_event(self, event): + """Removes an event from the simulation. + + This works dynamically for running simulations. + + Parameters + ---------- + event : Event + event to remove from the simulation + """ + + #check if event is in event list + if event not in self.events: + _msg = f"{event} not part of simulation" + self.logger.error(_msg) + raise ValueError(_msg) + + #remove from global event list + self.events.remove(event) # system assembly ------------------------------------------------------------- def _assemble_graph(self): - """Build the internal graph representation for fast system function + """Build the internal graph representation for fast system function evaluation and algebraic loop resolution. """ + #reset all block inputs to clear stale values from removed connections + for block in self.blocks: + block.inputs.reset() + #time the graph construction with Timer(verbose=False) as T: self.graph = Graph(self.blocks, self.connections) + self._graph_dirty = False #create boosters for loop closing connections if self.graph.has_loops: @@ -470,10 +734,11 @@ def _check_blocks_are_managed(self): conn_blocks.update(conn.get_blocks()) # Check subset actively managed - if not conn_blocks.issubset(self.blocks): - self.logger.warning( - f"{blk} in 'connections' but not in 'blocks'!" - ) + for blk in conn_blocks: + if blk not in self.blocks: + self.logger.warning( + f"{blk} in 'connections' but not in 'blocks'!" + ) # solver management ----------------------------------------------------------- @@ -504,13 +769,13 @@ def _set_solver(self, Solver=None, **solver_kwargs): self.engine = self.Solver() #iterate all blocks and set integration engines with tolerances - self._blocks_dyn = set() + self._blocks_dyn = [] for block in self.blocks: block.set_solver(self.Solver, self.engine, **self.solver_kwargs) - + #add dynamic blocks to list if block.engine: - self._blocks_dyn.add(block) + self._blocks_dyn.append(block) #logging message self.logger.info( @@ -563,6 +828,15 @@ def reset(self, time=0.0): for event in self.events: event.reset() + #reset convergence trackers and diagnostics + self._loop_tracker.reset() + self._solve_tracker.reset() + self._step_tracker.reset() + if self.diagnostics is not None: + self.diagnostics = Diagnostics() + if self._diagnostics_history is not None: + self._diagnostics_history.clear() + #evaluate system function self._update(self.time) @@ -711,11 +985,16 @@ def _update(self, t): evaluation time for system function """ + #lazy graph rebuild if dirty + if self._graph_dirty: + self._assemble_graph() + self._graph_dirty = False + #evaluate DAG self._dag(t) #algebraic loops -> solve them - if self.graph.has_loops: + if self.graph.has_loops: self._loops(t) @@ -769,19 +1048,20 @@ def _loops(self, t): if connection: connection.update() #step boosters of loop closing connections - max_err = 0.0 + self._loop_tracker.begin_iteration() for con_booster in self.boosters: - err = con_booster.update() - if err > max_err: - max_err = err - + self._loop_tracker.record(con_booster, con_booster.update()) + #check convergence - if max_err <= self.tolerance_fpi: + if self._loop_tracker.converged(self.tolerance_fpi): + self._loop_tracker.iterations = iteration return - #not converged -> error - _msg = "algebraic loop not converged (iters: {}, err: {})".format( - self.iterations_max, max_err + #not converged -> error with per-connection details + self._loop_tracker.iterations = self.iterations_max + details = self._loop_tracker.details(lambda b: str(b.connection)) + _msg = "algebraic loop not converged (iters: {}, err: {:.2e})\n{}".format( + self.iterations_max, self._loop_tracker.max_error, "\n".join(details) ) self.logger.error(_msg) raise RuntimeError(_msg) @@ -823,26 +1103,21 @@ def _solve(self, t, dt): #evaluate system equation (this is a fixed point loop) self._update(t) - total_evals += 1 + total_evals += 1 #advance solution of implicit solver - max_error = 0.0 + self._solve_tracker.begin_iteration() for block in self._blocks_dyn: - - #skip inactive blocks - if not block: + if not block: continue - - #advance solution (internal optimizer) - error = block.solve(t, dt) - if error > max_error: - max_error = error + self._solve_tracker.record(block, block.solve(t, dt)) - #check for convergence (only error) - if max_error <= self.tolerance_fpi: - return True, total_evals, it+1 + #check for convergence + if self._solve_tracker.converged(self.tolerance_fpi): + self._solve_tracker.iterations = it + 1 + return True, total_evals, it + 1 - #not converged in 'self.iterations_max' steps + self._solve_tracker.iterations = self.iterations_max return False, total_evals, self.iterations_max @@ -899,8 +1174,9 @@ def steadystate(self, reset=False): #catch non convergence if not success: - _msg = "STEADYSTATE -> FINISHED (success: {}, evals: {}, iters: {}, runtime: {})".format( - success, evals, iters, T) + details = self._solve_tracker.details(lambda b: b.__class__.__name__) + _msg = "STEADYSTATE -> FAILED (evals: {}, iters: {}, runtime: {})\n{}".format( + evals, iters, T, "\n".join(details)) self.logger.error(_msg) raise RuntimeError(_msg) @@ -1019,32 +1295,14 @@ def _step(self, t, dt): rescale factor for timestep """ - #initial timestep rescale and error estimate - success, max_error_norm, min_scale = True, 0.0, None + self._step_tracker.reset() - #step blocks and get error estimates if available for block in self._blocks_dyn: - - #skip inactive blocks if not block: continue - - #step the block suc, err_norm, scl = block.step(t, dt) + self._step_tracker.record(block, suc, err_norm, scl) - #check solver stepping success - if not suc: - success = False - - #update error tracking - if err_norm > max_error_norm: - max_error_norm = err_norm - - #track minimum relevant scale directly (avoids list allocation) - if scl is not None: - if min_scale is None or scl < min_scale: - min_scale = scl - - return success, max_error_norm, min_scale if min_scale is not None else 1.0 + return self._step_tracker.success, self._step_tracker.max_error, self._step_tracker.scale # timestepping ---------------------------------------------------------------- @@ -1212,10 +1470,19 @@ def timestep(self, dt=None, adaptive=True): total_evals += evals total_solver_its += solver_its - #adaptive implicit: revert if solver didn't converge - if not success and is_adaptive: - self._revert(self.time) - return False, 0.0, 0.5, total_evals + 1, total_solver_its + #implicit solver didn't converge + if not success: + details = self._solve_tracker.details(lambda b: b.__class__.__name__) + if is_adaptive: + self.logger.warning( + "implicit solver not converged, reverting step at t={:.6f}\n{}".format( + time_stage, "\n".join(details))) + self._revert(self.time) + return False, 0.0, 0.5, total_evals + 1, total_solver_its + else: + self.logger.warning( + "implicit solver not converged at t={:.6f} (iters: {})\n{}".format( + time_stage, solver_its, "\n".join(details))) else: #explicit: evaluate system equation self._update(time_stage) @@ -1254,6 +1521,19 @@ def timestep(self, dt=None, adaptive=True): self._update(time_dt) total_evals += 1 + #update diagnostics snapshot for this timestep + if self.diagnostics is not None: + self.diagnostics = Diagnostics( + time=time_dt, + loop_residuals=dict(self._loop_tracker.errors), + loop_iterations=self._loop_tracker.iterations, + solve_residuals=dict(self._solve_tracker.errors), + solve_iterations=self._solve_tracker.iterations, + step_errors=dict(self._step_tracker.errors), + ) + if self._diagnostics_history is not None: + self._diagnostics_history.append(self.diagnostics) + #sample data after successful timestep self._sample(time_dt, dt) diff --git a/src/pathsim/solvers/_solver.py b/src/pathsim/solvers/_solver.py index 2df7e6f6..d10bf16e 100644 --- a/src/pathsim/solvers/_solver.py +++ b/src/pathsim/solvers/_solver.py @@ -39,7 +39,7 @@ class Solver: Parameters ---------- - initial_value : float, array + initial_value : float, np.ndarray initial condition / integration constant tolerance_lte_abs : float absolute tolerance for local truncation error (for solvers with error estimate) @@ -50,9 +50,9 @@ class Solver: Attributes ---------- - x : numeric, array[numeric] + x : float, np.ndarray internal 'working' state - history : deque[numeric] + history : deque[float, np.ndarray] internal history of past results n : int order of integration scheme @@ -180,7 +180,7 @@ def get(self): Returns ------- - x : numeric, array[numeric] + x : float, np.ndarray current internal state of the solver """ return self.x @@ -194,7 +194,7 @@ def set(self, x): Parameters ---------- - x : numeric, array[numeric] + x : float, np.ndarray new internal state of the solver """ @@ -208,7 +208,7 @@ def state(self): Returns ------- - x : numeric, array[numeric] + x : float, np.ndarray current internal state of the solver """ return self.x @@ -220,14 +220,25 @@ def state(self, value): Parameters ---------- - value : numeric, array[numeric] + value : float, np.ndarray new internal state of the solver """ self.x = np.atleast_1d(value) - def reset(self): - """"Resets integration engine to initial value""" + def reset(self, initial_value=None): + """"Resets integration engine to initial value, + optionally provides new initial value + + Parameters + ---------- + initial_value : None | float | np.ndarray + new initial value of the engine, optional + """ + + #update initial value if provided + if initial_value is not None: + self.initial_value = initial_value #overwrite state with initial value (ensure array format) self.x = np.atleast_1d(self.initial_value).copy() @@ -342,6 +353,71 @@ def create(cls, initial_value, parent=None, from_engine=None, **solver_kwargs): return cls(initial_value, parent, **solver_kwargs) + # checkpoint methods --------------------------------------------------------------- + + def to_checkpoint(self, prefix): + """Serialize solver state for checkpointing. + + Parameters + ---------- + prefix : str + NPZ key prefix for this solver's arrays + + Returns + ------- + json_data : dict + JSON-serializable metadata + npz_data : dict + numpy arrays keyed by path + """ + json_data = { + "type": self.__class__.__name__, + "is_adaptive": self.is_adaptive, + "n": self.n, + "history_len": len(self.history), + "history_maxlen": self.history.maxlen, + } + + npz_data = { + f"{prefix}/x": np.atleast_1d(self.x), + f"{prefix}/initial_value": np.atleast_1d(self.initial_value), + } + + for i, h in enumerate(self.history): + npz_data[f"{prefix}/history_{i}"] = np.atleast_1d(h) + + return json_data, npz_data + + + def load_checkpoint(self, json_data, npz, prefix): + """Restore solver state from checkpoint. + + Parameters + ---------- + json_data : dict + solver metadata from checkpoint JSON + npz : dict-like + numpy arrays from checkpoint NPZ + prefix : str + NPZ key prefix for this solver's arrays + """ + self.x = npz[f"{prefix}/x"].copy() + self.initial_value = npz[f"{prefix}/initial_value"].copy() + self.n = json_data.get("n", self.n) + + #restore scalar format if needed + if self._scalar_initial and self.initial_value.size == 1: + self.initial_value = self.initial_value.item() + + #restore history + maxlen = json_data.get("history_maxlen", self.history.maxlen) + self.history = deque([], maxlen=maxlen) + for i in range(json_data.get("history_len", 0)): + key = f"{prefix}/history_{i}" + if key in npz: + self.history.append(npz[key].copy()) + + # methods for adaptive timestep solvers -------------------------------------------- def error_controller(self): diff --git a/src/pathsim/solvers/bdf.py b/src/pathsim/solvers/bdf.py index fafa382b..e84632b4 100644 --- a/src/pathsim/solvers/bdf.py +++ b/src/pathsim/solvers/bdf.py @@ -159,8 +159,19 @@ def stages(self, t, dt): yield _t - def reset(self): - """"Resets integration engine to initial state.""" + def reset(self, initial_value=None): + """"Resets integration engine to initial value, + optionally provides new initial value + + Parameters + ---------- + initial_value : None | float | np.ndarray + new initial value of the engine, optional + """ + + #update initial value if provided + if initial_value is not None: + self.initial_value = initial_value #clear history (BDF solution buffer) self.history.clear() @@ -169,7 +180,7 @@ def reset(self): self.x = np.atleast_1d(self.initial_value).copy() #reset startup solver - self.startup.reset() + self.startup.reset(initial_value) def buffer(self, dt): @@ -274,35 +285,37 @@ def step(self, f, dt): # SOLVERS ============================================================================== class BDF2(BDF): - """Fixed-step 2nd order Backward Differentiation Formula (BDF). + """Fixed-step 2nd order BDF method. A-stable. + + .. math:: - Implicit linear multistep method using the previous two solution points. A-stable, - making it excellent for stiff problems. Uses DIRK3 startup method for the first steps. + x_{n+1} = \\tfrac{4}{3}\\,x_n - \\tfrac{1}{3}\\,x_{n-1} + + \\tfrac{2}{3}\\,h\\,f(x_{n+1}, t_{n+1}) + + Uses ``DIRK3`` as startup solver for the first step. Characteristics --------------- * Order: 2 - * Implicit Multistep - * Fixed timestep only + * Implicit linear multistep, fixed timestep * A-stable - When to Use - ----------- - * **Stiff problems with fixed timestep**: Classic choice for stiff ODEs - * **Long-time integration**: Very stable for extended simulations - * **Known timestep**: When timestep is predetermined - * **Efficient stiff solver**: Lower overhead than higher-order BDFs - - **Recommended** for fixed-timestep stiff problems. For adaptive stepping, use GEAR21 - or ESDIRK methods. + Note + ---- + The workhorse fixed-step stiff solver. A-stability means no eigenvalue + in the left half-plane causes instability, regardless of the timestep. + Well suited for block diagrams with a fixed simulation clock and + moderately-to-very stiff dynamics. For adaptive stepping, use ``GEAR21`` + or ``ESDIRK43``. References ---------- .. [1] Gear, C. W. (1971). "Numerical Initial Value Problems in Ordinary Differential Equations". Prentice-Hall. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ @@ -317,36 +330,32 @@ def __init__(self, *solver_args, **solver_kwargs): class BDF3(BDF): - """Fixed-step 3rd order Backward Differentiation Formula (BDF). + """Fixed-step 3rd order BDF method. :math:`A(\\alpha)`-stable with + :math:`\\alpha \\approx 86°`. - Implicit linear multistep method using the previous three solution points. A(alpha)-stable - with :math:`\\alpha \\approx 86^\\circ`, providing excellent stability for stiff problems. - Uses DIRK3 startup method for initial steps. + Uses ``DIRK3`` as startup solver for the first two steps. Characteristics --------------- * Order: 3 - * Implicit Multistep - * Fixed timestep only - * A(alpha)-stable (:math:`\\alpha \\approx 86^\\circ`) - - When to Use - ----------- - * **Stiff problems with higher accuracy**: 3rd order for better accuracy than BDF2 - * **Fixed-timestep applications**: When timestep is predetermined - * **Good stability/accuracy balance**: Better accuracy with still-excellent stability - * **Chemical kinetics**: Common in reaction-diffusion problems + * Implicit linear multistep, fixed timestep + * :math:`A(\\alpha)`-stable, :math:`\\alpha \\approx 86°` - **Trade-off**: Slightly less stable than BDF2, but more accurate. For adaptive stepping, - use GEAR32 or ESDIRK43. + Note + ---- + Higher accuracy than ``BDF2`` with only a slight reduction in the + stability wedge. The :math:`86°` sector covers nearly the entire left + half-plane, so most stiff block diagrams remain well-handled. For + adaptive stepping, use ``GEAR32``. References ---------- .. [1] Gear, C. W. (1971). "Numerical Initial Value Problems in Ordinary Differential Equations". Prentice-Hall. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ @@ -361,36 +370,33 @@ def __init__(self, *solver_args, **solver_kwargs): class BDF4(BDF): - """Fixed-step 4th order Backward Differentiation Formula (BDF). + """Fixed-step 4th order BDF method. :math:`A(\\alpha)`-stable with + :math:`\\alpha \\approx 73°`. - Implicit linear multistep method using the previous four solution points. A(alpha)-stable - with :math:`\\alpha \\approx 73^\\circ`. Good for stiff problems requiring moderate-to-high - accuracy. Uses DIRK3 startup method for initial steps. + Uses ``DIRK3`` as startup solver for the first three steps. Characteristics --------------- * Order: 4 - * Implicit Multistep - * Fixed timestep only - * A(alpha)-stable (:math:`\\alpha \\approx 73^\\circ`) + * Implicit linear multistep, fixed timestep + * :math:`A(\\alpha)`-stable, :math:`\\alpha \\approx 73°` - When to Use - ----------- - * **Moderate-to-high accuracy on stiff problems**: 4th order with good stability - * **Fixed timestep**: When timestep is predetermined - * **Accurate stiff solver**: Higher accuracy than BDF3 - * **Scientific computing**: Common in engineering simulations - - **Note**: Stability angle is smaller than BDF3. For very stiff problems, BDF2 or BDF3 - may be more robust. For adaptive stepping, use GEAR43 or ESDIRK43. + Note + ---- + The narrower stability wedge compared to ``BDF2``/``BDF3`` means + eigenvalues close to the imaginary axis may be poorly damped. Safe for + block diagrams whose stiff modes are strongly dissipative (well inside + the left half-plane). For problems with near-imaginary eigenvalues (e.g. + lightly damped oscillators), prefer lower-order BDF or an ESDIRK solver. References ---------- .. [1] Gear, C. W. (1971). "Numerical Initial Value Problems in Ordinary Differential Equations". Prentice-Hall. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ @@ -405,36 +411,33 @@ def __init__(self, *solver_args, **solver_kwargs): class BDF5(BDF): - """Fixed-step 5th order Backward Differentiation Formula (BDF). + """Fixed-step 5th order BDF method. :math:`A(\\alpha)`-stable with + :math:`\\alpha \\approx 51°`. - Implicit linear multistep method using the previous five solution points. A(alpha)-stable - with :math:`\\alpha \\approx 51^\\circ`. Suitable for stiff problems requiring high accuracy, - but with reduced stability angle. Uses DIRK3 startup method for initial steps. + Uses ``DIRK3`` as startup solver for the first four steps. Characteristics --------------- * Order: 5 - * Implicit Multistep - * Fixed timestep only - * A(alpha)-stable (:math:`\\alpha \\approx 51^\\circ`) - - When to Use - ----------- - * **High accuracy on mildly stiff problems**: 5th order when stability angle is sufficient - * **Fixed timestep applications**: When timestep is predetermined - * **Smooth stiff problems**: Problems without extreme stiffness - * **High-precision requirements**: Better accuracy than BDF4 + * Implicit linear multistep, fixed timestep + * :math:`A(\\alpha)`-stable, :math:`\\alpha \\approx 51°` - **Warning**: Reduced stability compared to lower-order BDFs. For very stiff problems, - use BDF2 or BDF3. For adaptive stepping, use GEAR54 or ESDIRK54. + Note + ---- + The stability wedge is noticeably smaller than ``BDF3`` or ``BDF4``. + Only appropriate when the stiff eigenvalues of the block diagram are + concentrated well inside the left half-plane and high accuracy per step + is essential. For most stiff systems, ``BDF2`` or ``BDF3`` with a + smaller timestep is more robust. References ---------- .. [1] Gear, C. W. (1971). "Numerical Initial Value Problems in Ordinary Differential Equations". Prentice-Hall. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ @@ -449,39 +452,33 @@ def __init__(self, *solver_args, **solver_kwargs): class BDF6(BDF): - """Fixed-step 6th order Backward Differentiation Formula (BDF). + """Fixed-step 6th order BDF method. **Not** A-stable + (:math:`\\alpha \\approx 18°`). - Implicit linear multistep method using the previous six solution points. Not A-stable; - stability region does not contain the entire left half-plane (stability angle only - :math:`\\approx 18^\\circ`), severely limiting its use for stiff problems. Uses DIRK3 - startup method for initial steps. + Uses ``DIRK3`` as startup solver for the first five steps. Characteristics --------------- * Order: 6 - * Implicit Multistep - * Fixed timestep only - * Not A-stable (stability angle approx :math:`18^\\circ`) - - When to Use - ----------- - * **Very smooth, mildly stiff problems**: Only when stiffness is minimal - * **High accuracy priority**: When 6th order accuracy justifies poor stability - * **Specialized applications**: Rarely used in practice + * Implicit linear multistep, fixed timestep + * :math:`A(\\alpha)`-stable, :math:`\\alpha \\approx 18°` (not A-stable) - **Warning**: Very limited stability. Generally not recommended for stiff problems. - For most applications requiring 6th order accuracy, use explicit methods like RKV65 - on non-stiff problems, or lower-order BDFs with smaller timesteps on stiff problems. + Note + ---- + The very narrow stability wedge means that most stiff problems will be + unstable at practical timestep sizes. Provided mainly for completeness. + For 6th order accuracy on non-stiff systems, the explicit ``RKV65`` is + cheaper. For stiff systems, ``BDF2``--``BDF4`` with a smaller timestep + or an ESDIRK solver are far more robust. References ---------- .. [1] Gear, C. W. (1971). "Numerical Initial Value Problems in Ordinary Differential Equations". Prentice-Hall. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. - .. [3] Curtiss, C. F., & Hirschfelder, J. O. (1952). "Integration of stiff equations". - Proceedings of the National Academy of Sciences, 38(3), 235-243. + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ diff --git a/src/pathsim/solvers/dirk2.py b/src/pathsim/solvers/dirk2.py index 0ab3bde1..bbceb2b9 100644 --- a/src/pathsim/solvers/dirk2.py +++ b/src/pathsim/solvers/dirk2.py @@ -15,37 +15,33 @@ # SOLVERS ============================================================================== class DIRK2(DiagonallyImplicitRungeKutta): - """Two-stage, 2nd order Diagonally Implicit Runge-Kutta (DIRK) method. - - This specific method is SSP-optimal (largest radius of absolute monotonicity - for a 2-stage, 2nd order DIRK), symplectic, and A-stable. It's a robust choice - for moderately stiff problems where second-order accuracy is sufficient. + """Two-stage, 2nd order DIRK method. L-stable, SSP-optimal, symplectic. Characteristics --------------- * Order: 2 - * Stages: 2 (Implicit) - * Implicit (DIRK) - * Fixed timestep only - * A-stable, SSP-optimal, Symplectic - - When to Use - ----------- - * **Moderately stiff problems**: Good entry-level implicit method for stiff ODEs - * **SSP requirements with stiffness**: Combines strong stability preservation with A-stability - * **Symplectic integration**: Preserves geometric structure in Hamiltonian systems - * **Low-order implicit needs**: When 2nd order implicit accuracy is sufficient - - **Trade-off**: Lower order than ESDIRK or BDF methods but has SSP and symplectic - properties. For higher accuracy on stiff problems, consider ESDIRK43 or BDF methods. + * Stages: 2 (implicit) + * Fixed timestep + * L-stable, SSP-optimal, symplectic + + Note + ---- + The simplest multi-stage implicit Runge-Kutta method. L-stability + fully damps parasitic high-frequency modes, and the symplectic property + preserves Hamiltonian structure when the dynamics are conservative. Two + implicit stages per step is relatively cheap. For higher accuracy on + stiff systems, use ``DIRK3`` or the adaptive ``ESDIRK43``. References ---------- - .. [1] Ferracina, L., & Spijker, M. N. (2008). "Strong stability of singly-diagonally- - implicit Runge-Kutta methods". Applied Numerical Mathematics, 58(11), 1675-1686. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [1] Ferracina, L., & Spijker, M. N. (2008). "Strong stability of + singly-diagonally-implicit Runge-Kutta methods". Applied Numerical + Mathematics, 58(11), 1675-1686. + :doi:`10.1016/j.apnum.2007.10.004` + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ diff --git a/src/pathsim/solvers/dirk3.py b/src/pathsim/solvers/dirk3.py index 9f3fe252..56108791 100644 --- a/src/pathsim/solvers/dirk3.py +++ b/src/pathsim/solvers/dirk3.py @@ -15,42 +15,37 @@ # SOLVERS ============================================================================== class DIRK3(DiagonallyImplicitRungeKutta): - """Four-stage, 3rd order L-stable Diagonally Implicit Runge-Kutta (DIRK) method. + """Four-stage, 3rd order L-stable DIRK method. Stiffly accurate. - L-stability (A-stability and stiffly accurate, i.e., :math:`|R(\\infty)| = 0`) makes - this method suitable for stiff problems where damping of high-frequency components - is desired. The stiffly accurate property ensures good behavior for problems with - singular perturbations and differential-algebraic equations. + L-stability (:math:`|R(\\infty)| = 0`) fully damps parasitic + high-frequency modes. The stiffly accurate property ensures the last + stage equals the step output, which is beneficial for + differential-algebraic systems. Characteristics --------------- * Order: 3 - * Stages: 4 (Implicit) - * Implicit (DIRK) - * Fixed timestep only - * L-stable (and thus A-stable) - * Stiffly accurate - - When to Use - ----------- - * **Stiff problems**: Excellent stability for very stiff ODEs - * **Damping required**: L-stability damps high-frequency oscillations - * **Differential-algebraic equations**: Stiffly accurate property helps with DAEs - * **3rd order implicit**: Moderate accuracy with strong stability - - **Recommended** for stiff problems requiring 3rd order accuracy. For higher order, - consider ESDIRK54. For variable timestep, use adaptive ESDIRK methods. + * Stages: 4 (implicit) + * Fixed timestep + * L-stable, stiffly accurate + + Note + ---- + A robust fixed-step solver for stiff block diagrams. L-stability makes + it well-suited for systems with widely separated time scales, such as a + fast electrical subsystem driving a slow thermal or mechanical model. + Also used internally as the startup method for ``BDF`` solvers. For + adaptive stepping on stiff problems, prefer ``ESDIRK43``. References ---------- - .. [1] Crouzeix, M. (1975). "Sur l'approximation des équations différentielles - opérationnelles linéaires par des méthodes de Runge-Kutta". PhD thesis, - Université Paris VI. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. - .. [3] Alexander, R. (1977). "Diagonally implicit Runge-Kutta methods for stiff O.D.E.'s". - SIAM Journal on Numerical Analysis, 14(6), 1006-1021. + .. [1] Alexander, R. (1977). "Diagonally implicit Runge-Kutta methods + for stiff O.D.E.'s". SIAM Journal on Numerical Analysis, 14(6), + 1006-1021. :doi:`10.1137/0714068` + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ diff --git a/src/pathsim/solvers/esdirk32.py b/src/pathsim/solvers/esdirk32.py index b4c83e6f..9c190b41 100644 --- a/src/pathsim/solvers/esdirk32.py +++ b/src/pathsim/solvers/esdirk32.py @@ -15,41 +15,39 @@ # SOLVERS ============================================================================== class ESDIRK32(DiagonallyImplicitRungeKutta): - """Four-stage, 3rd order Embedded Singly Diagonally Implicit Runge-Kutta (ESDIRK) method. - - Features an embedded 2nd order method for adaptive step size control. The first stage - is explicit, making the method suitable for problems with explicit first stages. Designed - to be applicable to index-2 Differential Algebraic Equations (DAEs). + """Four-stage, 3rd order ESDIRK method with embedded 2nd order error + estimate. L-stable and stiffly accurate. Characteristics --------------- - * Order: 3 - * Embedded Order: 2 - * Stages: 4 (1 Explicit, 3 Implicit) - * Implicit (ESDIRK) + * Order: 3 (propagating) / 2 (embedded) + * Stages: 4 (1 explicit, 3 implicit) * Adaptive timestep - * A-stable - - When to Use - ----------- - * **Moderately stiff problems with adaptivity**: Good for stiff ODEs needing timestep control - * **Differential-algebraic equations**: Suitable for DAEs of index up to 2 - * **Entry-level adaptive implicit**: Lower-order adaptive implicit method - * **Testing stiffness**: Explore if a problem requires implicit methods - - **Trade-off**: Lower accuracy than ESDIRK43 or ESDIRK54, but computationally cheaper. - For higher accuracy, use ESDIRK54. + * L-stable, stiffly accurate + * Stage order 2 (:math:`\\gamma = 1/2`) + + Note + ---- + The cheapest adaptive implicit Runge-Kutta solver in this library, + yet remarkably robust. L-stability and stiff accuracy guarantee that + high-frequency parasitic modes are fully damped regardless of + timestep, and the optimal stage order of 2 (from :math:`\\gamma = 1/2`) + minimises order reduction on stiff problems. Three implicit stages + per step keeps the cost well below ``ESDIRK43`` while still providing + adaptive step-size control. For even lower per-step cost the + ``GEAR`` multistep solvers require only one implicit solve per step. + Also used internally as the startup method for ``GEAR`` solvers. References ---------- - .. [1] Williams, D. M., Houzeaux, G., Aubry, R., Vázquez, M., & Calmet, H. (2013). - "Efficiency of a matrix-free linearly implicit time integration strategy". - Computers & Fluids, 83, 77-89. - .. [2] Kennedy, C. A., & Carpenter, M. H. (2016). "Diagonally implicit Runge-Kutta - methods for ordinary differential equations. A review". NASA Technical Report. - .. [3] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [1] Kennedy, C. A., & Carpenter, M. H. (2019). "Diagonally implicit + Runge-Kutta methods for stiff ODEs". Applied Numerical + Mathematics, 146, 221-244. + :doi:`10.1016/j.apnum.2019.07.008` + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ diff --git a/src/pathsim/solvers/esdirk4.py b/src/pathsim/solvers/esdirk4.py index a5589c89..b52dd152 100644 --- a/src/pathsim/solvers/esdirk4.py +++ b/src/pathsim/solvers/esdirk4.py @@ -15,37 +15,37 @@ # SOLVERS ============================================================================== class ESDIRK4(DiagonallyImplicitRungeKutta): - """Six-stage, 4th order Singly Diagonally Implicit Runge-Kutta (ESDIRK) method. + """Six-stage, 4th order ESDIRK method. L-stable and stiffly accurate. - Features an explicit first stage (making it ESDIRK). This specific tableau is designed - for handling stiff problems and potentially Differential Algebraic Equations (DAEs) of - index up to two or three. Does not have an embedded method for error estimation in this - implementation (fixed step only). + No embedded error estimator; fixed timestep only. Characteristics --------------- * Order: 4 - * Stages: 6 (1 Explicit, 5 Implicit) - * Implicit (ESDIRK) - * Fixed timestep only - * A-stable - - When to Use - ----------- - * **Stiff problems with fixed timestep**: 4th order accuracy for stiff ODEs - * **Differential-algebraic equations**: Suitable for DAEs of index 2-3 - * **Moderate-to-high accuracy on stiff problems**: Better than 3rd order methods - * **Known stable timestep**: When adaptive stepping is not needed - - **Note**: For adaptive timestepping on stiff problems, use ESDIRK43 or ESDIRK54 instead. + * Stages: 6 (1 explicit, 5 implicit) + * Fixed timestep + * L-stable, stiffly accurate + * Stage order 2 + + Note + ---- + Provides 4th order accuracy on stiff block diagrams when the timestep is + predetermined (e.g. real-time or hardware-in-the-loop contexts). The + explicit first stage reuses the last function evaluation from the + previous step, saving one implicit solve per step compared to a fully + implicit DIRK. L-stability and stiff accuracy ensure full damping of + parasitic high-frequency modes. For adaptive stepping, use ``ESDIRK43`` + which adds an embedded error estimator at the same stage count. References ---------- - .. [1] Kennedy, C. A., & Carpenter, M. H. (2016). "Diagonally implicit Runge-Kutta - methods for ordinary differential equations. A review". NASA Technical Report. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [1] Kennedy, C. A., & Carpenter, M. H. (2016). "Diagonally implicit + Runge-Kutta methods for ordinary differential equations. A + review". NASA/TM-2016-219173. + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ diff --git a/src/pathsim/solvers/esdirk43.py b/src/pathsim/solvers/esdirk43.py index 43dc491e..38db4c09 100644 --- a/src/pathsim/solvers/esdirk43.py +++ b/src/pathsim/solvers/esdirk43.py @@ -17,39 +17,38 @@ # SOLVERS ============================================================================== class ESDIRK43(DiagonallyImplicitRungeKutta): - """Six-stage, 4th order Embedded Singly Diagonally Implicit Runge-Kutta (ESDIRK) method. - - Features an embedded 3rd order method for adaptive step size control. The first stage is - explicit. L-stable and stiffly accurate, making it excellent for stiff problems requiring - moderate-to-high accuracy with adaptive timestepping. + """Six-stage, 4th order ESDIRK method with embedded 3rd order error + estimate. L-stable and stiffly accurate. Characteristics --------------- - * Order: 4 - * Embedded Order: 3 - * Stages: 6 (1 Explicit, 5 Implicit) - * Implicit (ESDIRK) + * Order: 4 (propagating) / 3 (embedded) + * Stages: 6 (1 explicit, 5 implicit) * Adaptive timestep - * L-stable - * Stiffly accurate - - When to Use - ----------- - * **Stiff problems with adaptive stepping**: Excellent default for stiff ODEs - * **Moderate-to-high accuracy**: 4th order with good error control - * **Damping high frequencies**: L-stability damps spurious oscillations - * **General-purpose stiff solver**: Reliable choice for most stiff applications - - **Recommended** as a default adaptive stiff solver. For very high accuracy, use ESDIRK54. - For non-stiff problems, RKDP54 is more efficient. + * L-stable, stiffly accurate + * Stage order 2 + + Note + ---- + Recommended default for stiff block diagrams. L-stability damps + high-frequency parasitic modes that arise from stiff subsystems (e.g. + PID controllers with large derivative gain, fast electrical or chemical + dynamics). The adaptive step-size control concentrates computational + effort where the solution changes rapidly. For non-stiff systems, + ``RKDP54`` avoids the implicit solve cost and is more efficient. For + tighter tolerances on stiff problems, ``ESDIRK54`` provides 5th order + accuracy. References ---------- - .. [1] Kennedy, C. A., & Carpenter, M. H. (2019). "Diagonally implicit Runge-Kutta - methods for stiff ODEs". Applied Numerical Mathematics, 146, 221-244. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [1] Kennedy, C. A., & Carpenter, M. H. (2019). "Diagonally implicit + Runge-Kutta methods for stiff ODEs". Applied Numerical + Mathematics, 146, 221-244. + :doi:`10.1016/j.apnum.2019.07.008` + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ diff --git a/src/pathsim/solvers/esdirk54.py b/src/pathsim/solvers/esdirk54.py index aa1cd089..860be7a5 100644 --- a/src/pathsim/solvers/esdirk54.py +++ b/src/pathsim/solvers/esdirk54.py @@ -15,39 +15,36 @@ # SOLVERS ============================================================================== class ESDIRK54(DiagonallyImplicitRungeKutta): - """Seven-stage, 5th order L-stable Embedded Singly Diagonally Implicit Runge-Kutta method. - - Features an embedded 4th order method for adaptive step size control. The first stage is - explicit. L-stable and stiffly accurate, making it excellent for stiff problems requiring - high accuracy with adaptive timestepping. This is the ESDIRK5(4)7L[2]SA2 method. + """Seven-stage, 5th order ESDIRK method with embedded 4th order error + estimate. L-stable and stiffly accurate (ESDIRK5(4)7L[2]SA2). Characteristics --------------- - * Order: 5 - * Embedded Order: 4 - * Stages: 7 (1 Explicit, 6 Implicit) - * Implicit (ESDIRK) + * Order: 5 (propagating) / 4 (embedded) + * Stages: 7 (1 explicit, 6 implicit) * Adaptive timestep - * L-stable, Stiffly accurate - - When to Use - ----------- - * **High-accuracy stiff problems**: When 5th order is needed for stiff ODEs - * **Demanding stiff applications**: Chemical kinetics, combustion, atmospheric chemistry - * **Tight error tolerances**: Better accuracy than 4th order methods - * **Production stiff solver**: High-quality method for serious applications - - **Recommended** for high-accuracy stiff problems. This is a state-of-the-art adaptive - implicit method. For very stiff problems with less stringent accuracy, ESDIRK43 may be - more efficient. + * L-stable, stiffly accurate + * Stage order 2 + + Note + ---- + The highest-accuracy L-stable single-step solver in this library before + the much more expensive ``ESDIRK85``. Use when tight tolerances are + needed on a stiff block diagram (e.g. multi-rate systems combining fast + electrical and slow thermal dynamics). At moderate tolerances, + ``ESDIRK43`` achieves similar results with fewer implicit solves per + step. References ---------- - .. [1] Kennedy, C. A., & Carpenter, M. H. (2019). "Diagonally implicit Runge-Kutta - methods for stiff ODEs". Applied Numerical Mathematics, 146, 221-244. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [1] Kennedy, C. A., & Carpenter, M. H. (2019). "Diagonally implicit + Runge-Kutta methods for stiff ODEs". Applied Numerical + Mathematics, 146, 221-244. + :doi:`10.1016/j.apnum.2019.07.008` + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ diff --git a/src/pathsim/solvers/esdirk85.py b/src/pathsim/solvers/esdirk85.py index d0b04478..8225fe04 100644 --- a/src/pathsim/solvers/esdirk85.py +++ b/src/pathsim/solvers/esdirk85.py @@ -15,43 +15,37 @@ # SOLVERS ============================================================================== class ESDIRK85(DiagonallyImplicitRungeKutta): - """Sixteen-stage, 8th order L-stable Embedded Singly Diagonally Implicit Runge-Kutta method. - - Features an embedded 5th order method for adaptive step size control. The first stage is - explicit. Designed for very stiff problems requiring very high accuracy. Computationally - expensive due to 16 stages, but can take very large timesteps with tight tolerances. - This is the ESDIRK(16,8)[2]SAL-[(16,5)] method. + """Sixteen-stage, 8th order ESDIRK method with embedded 5th order error + estimate. L-stable and stiffly accurate (ESDIRK(16,8)[2]SAL-[(16,5)]). Characteristics --------------- - * Order: 8 - * Embedded Order: 5 - * Stages: 16 (1 Explicit, 15 Implicit) - * Implicit (ESDIRK) + * Order: 8 (propagating) / 5 (embedded) + * Stages: 16 (1 explicit, 15 implicit) * Adaptive timestep - * L-stable, Stiffly accurate - - When to Use - ----------- - * **Extremely high accuracy on stiff problems**: When very tight tolerances are essential - * **Expensive right-hand sides**: When large timesteps justify the 16 stages - * **Benchmark computations**: Reference solutions for stiff problems - * **Specialized applications**: Only when 8th order accuracy is truly needed - - **Warning**: Very expensive (16 implicit stages). Only use when extremely high accuracy - is essential and large timesteps are possible. For most applications, ESDIRK54 is more - practical. + * L-stable, stiffly accurate + * Stage order 2 + + Note + ---- + Fifteen implicit solves per step make this very expensive. It is only + justified when the right-hand side evaluation is itself costly (large + state dimension, expensive ``ODE`` blocks) and very tight tolerances are + required so that the 8th order convergence compensates through much + larger steps. For generating stiff reference solutions to validate other + solvers. In almost all practical block-diagram simulations, ``ESDIRK54`` + is the better choice. References ---------- - .. [1] Alamri, Y., & Ketcheson, D. I. (2019). "Very high-order A-stable stiffly accurate - diagonally implicit Runge-Kutta methods with error estimators". arXiv preprint - arXiv:1905.11370. - .. [2] Kennedy, C. A., & Carpenter, M. H. (2019). "Diagonally implicit Runge-Kutta - methods for stiff ODEs". Applied Numerical Mathematics, 146, 221-244. - .. [3] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [1] Alamri, Y., & Ketcheson, D. I. (2024). "Very high-order A-stable + stiffly accurate diagonally implicit Runge-Kutta methods with + error estimators". Journal of Scientific Computing, 100, + Article 84. :doi:`10.1007/s10915-024-02627-w` + .. [2] Kennedy, C. A., & Carpenter, M. H. (2019). "Diagonally implicit + Runge-Kutta methods for stiff ODEs". Applied Numerical + Mathematics, 146, 221-244. + :doi:`10.1016/j.apnum.2019.07.008` """ diff --git a/src/pathsim/solvers/euler.py b/src/pathsim/solvers/euler.py index 4c0e463b..14af26a4 100644 --- a/src/pathsim/solvers/euler.py +++ b/src/pathsim/solvers/euler.py @@ -13,45 +13,37 @@ # SOLVERS ============================================================================== class EUF(ExplicitSolver): - """Explicit Forward Euler (FE) integration method. - - This is the simplest explicit numerical integration method. It is first-order - accurate (:math:`O(h)`) and generally not suitable for stiff problems due to its - limited stability region. - - Method: + """Explicit forward Euler method. First-order, single-stage. .. math:: - x_{n+1} = x_n + dt \\cdot f(x_n, t_n) + x_{n+1} = x_n + h \\, f(x_n, t_n) Characteristics --------------- * Order: 1 * Stages: 1 - * Explicit - * Fixed timestep only + * Explicit, fixed timestep * Not A-stable - * Low accuracy and stability, but computationally very cheap. - - When to Use - ----------- - * **Educational purposes**: Ideal for teaching basic numerical integration concepts - * **Very smooth problems**: When the function is extremely smooth and well-behaved - * **Rapid prototyping**: Quick initial testing before applying more sophisticated methods - * **Resource-constrained scenarios**: When computational cost must be minimized - **Not recommended** for production use, stiff problems, or when accuracy is important. + Note + ---- + The cheapest solver per step but also the least accurate. Its small stability + region requires very small timesteps for moderately dynamic block diagrams, + which usually makes higher-order methods more efficient overall. Prefer + ``RK4`` for fixed-step or ``RKDP54`` for adaptive integration of non-stiff + systems. Only practical when computational cost per step must be absolute + minimum and accuracy is secondary. References ---------- - .. [1] Euler, L. (1768). "Institutionum calculi integralis". Impensis Academiae - Imperialis Scientiarum, Vol. 1. - .. [2] Butcher, J. C. (2016). "Numerical Methods for Ordinary Differential Equations". - John Wiley & Sons, 3rd Edition. - .. [3] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary - Differential Equations I: Nonstiff Problems". Springer Series in Computational - Mathematics, Vol. 8. + .. [1] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary + Differential Equations I: Nonstiff Problems". Springer Series in + Computational Mathematics, Vol. 8. + :doi:`10.1007/978-3-540-78862-1` + .. [2] Butcher, J. C. (2016). "Numerical Methods for Ordinary Differential + Equations". John Wiley & Sons, 3rd Edition. + :doi:`10.1002/9781119121534` """ @@ -87,49 +79,40 @@ def step(self, f, dt): class EUB(ImplicitSolver): - """Implicit Backward Euler (BE) integration method. - - This is the simplest implicit numerical integration method. It is first-order - accurate (:math:`O(h)`) and is A-stable and L-stable, making it suitable for very - stiff problems where stability is paramount, although its low order limits - accuracy for non-stiff problems or when high precision is required. - - Method: + """Implicit backward Euler method. First-order, A-stable and L-stable. .. math:: - x_{n+1} = x_n + dt \\cdot f(x_{n+1}, t_{n+1}) + x_{n+1} = x_n + h \\, f(x_{n+1}, t_{n+1}) - This implicit equation is solved iteratively using the internal optimizer. + The implicit equation is solved iteratively by the internal optimizer. Characteristics --------------- * Order: 1 - * Stages: 1 (Implicit) - * Implicit - * Fixed timestep only + * Stages: 1 (implicit) + * Fixed timestep * A-stable, L-stable - * Very stable, suitable for stiff problems, but low accuracy. - - When to Use - ----------- - * **Highly stiff problems**: Excellent stability for very stiff ODEs - * **Robustness over accuracy**: When stability is more critical than precision - * **Long-time integration**: For simulations over very long time periods where stability matters - * **Initial testing of stiff systems**: Simple method to verify problem setup - **Trade-off**: Sacrifices accuracy for exceptional stability. For higher accuracy on - stiff problems, consider BDF or ESDIRK methods. + Note + ---- + Maximum stability at the cost of accuracy. L-stability fully damps + parasitic high-frequency modes, making this a safe fallback for very stiff + block diagrams (e.g. high-gain PID loops or fast electrical dynamics coupled + to slow mechanical plant). Because each step requires solving a nonlinear + equation, the cost per step is higher than explicit methods. For better + accuracy on stiff systems, use ``BDF2`` (fixed-step) or ``ESDIRK43`` + (adaptive). References ---------- - .. [1] Curtiss, C. F., & Hirschfelder, J. O. (1952). "Integration of stiff equations". - Proceedings of the National Academy of Sciences, 38(3), 235-243. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. - .. [3] Butcher, J. C. (2016). "Numerical Methods for Ordinary Differential Equations". - John Wiley & Sons, 3rd Edition. + .. [1] Curtiss, C. F., & Hirschfelder, J. O. (1952). "Integration of stiff + equations". Proceedings of the National Academy of Sciences, 38(3), + 235-243. :doi:`10.1073/pnas.38.3.235` + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ diff --git a/src/pathsim/solvers/gear.py b/src/pathsim/solvers/gear.py index dbc259a2..22968194 100644 --- a/src/pathsim/solvers/gear.py +++ b/src/pathsim/solvers/gear.py @@ -210,6 +210,50 @@ def create(cls, initial_value, parent=None, from_engine=None, **solver_kwargs): return cls(initial_value, parent, **solver_kwargs) + def to_checkpoint(self, prefix): + """Serialize GEAR solver state including startup solver and timestep history.""" + json_data, npz_data = super().to_checkpoint(prefix) + + json_data["_needs_startup"] = self._needs_startup + json_data["history_dt_len"] = len(self.history_dt) + + #timestep history + for i, dt in enumerate(self.history_dt): + npz_data[f"{prefix}/history_dt_{i}"] = np.atleast_1d(dt) + + #startup solver state + if self.startup: + s_json, s_npz = self.startup.to_checkpoint(f"{prefix}/startup") + json_data["startup"] = s_json + npz_data.update(s_npz) + + return json_data, npz_data + + + def load_checkpoint(self, json_data, npz, prefix): + """Restore GEAR solver state including startup solver and timestep history.""" + super().load_checkpoint(json_data, npz, prefix) + + self._needs_startup = json_data.get("_needs_startup", True) + + #restore timestep history + self.history_dt.clear() + for i in range(json_data.get("history_dt_len", 0)): + key = f"{prefix}/history_dt_{i}" + if key in npz: + self.history_dt.append(npz[key].item()) + + #restore startup solver + if self.startup and "startup" in json_data: + self.startup.load_checkpoint(json_data["startup"], npz, f"{prefix}/startup") + + #recompute BDF coefficients from restored history + if not self._needs_startup and len(self.history_dt) > 0: + self.F, self.K = {}, {} + for k, _ in enumerate(self.history_dt, 1): + self.F[k], self.K[k] = compute_bdf_coefficients(k, np.array(self.history_dt)) + + def stages(self, t, dt): """Generator that yields the intermediate evaluation time during the timestep 't + ratio * dt'. @@ -231,8 +275,19 @@ def stages(self, t, dt): yield _t - def reset(self): - """"Resets integration engine to initial state.""" + def reset(self, initial_value=None): + """"Resets integration engine to initial value, + optionally provides new initial value + + Parameters + ---------- + initial_value : None | float | np.ndarray + new initial value of the engine, optional + """ + + #update initial value if provided + if initial_value is not None: + self.initial_value = initial_value #clear buffers self.history.clear() @@ -242,7 +297,7 @@ def reset(self): self.x = np.atleast_1d(self.initial_value).copy() #reset startup solver - self.startup.reset() + self.startup.reset(initial_value) def buffer(self, dt): @@ -424,40 +479,34 @@ def step(self, f, dt): # SOLVERS ============================================================================== class GEAR21(GEAR): - """Adaptive-step GEAR integrator using 2nd order BDF with variable timesteps. + """Variable-step 2nd order BDF with 1st order error estimate. A-stable. - Uses 2nd order BDF for timestepping and 1st order BDF (Backward Euler) for truncation - error estimation. Dynamically computes BDF coefficients for variable timesteps. Excellent - for moderately stiff problems where adaptive timestepping is beneficial. Uses ESDIRK32 - for startup. + BDF coefficients are recomputed each step to account for variable + timesteps. Uses ``ESDIRK32`` as startup solver. Characteristics --------------- - * Stepping Order: 2 (max) - * Error Estimation Order: 1 - * Implicit Variable-Step Multistep + * Order: 2 (stepping) / 1 (error estimate) + * Implicit variable-step multistep * Adaptive timestep - * A-stable (based on BDF2) - - When to Use - ----------- - * **Stiff problems with adaptive stepping**: Classic adaptive stiff solver - * **Variable dynamics**: When solution changes character over time - * **Efficient stiff integration**: Good balance of stability and accuracy - * **Long-time simulations**: Stable for extended integrations - + * A-stable + Note ---- - Good choice as a default adaptive stiff solver. For higher accuracy, use GEAR32 or - ESDIRK43. For fixed timestep, use BDF2. + The simplest adaptive multistep stiff solver. A-stability makes it safe + for any stiff block diagram. The multistep approach reuses past solution + values, so per-step cost is lower than single-step implicit methods + (ESDIRK), but a startup phase is needed to fill the history buffer. For + higher accuracy, use ``GEAR32`` or ``ESDIRK43``. References ---------- .. [1] Gear, C. W. (1971). "Numerical Initial Value Problems in Ordinary Differential Equations". Prentice-Hall. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ @@ -474,39 +523,33 @@ def __init__(self, *solver_args, **solver_kwargs): class GEAR32(GEAR): - """Adaptive-step GEAR integrator using 3rd order BDF with variable timesteps. + """Variable-step 3rd order BDF with 2nd order error estimate. + :math:`A(\\alpha)`-stable. - Uses 3rd order BDF for timestepping and 2nd order BDF for truncation error estimation. - Dynamically computes BDF coefficients for variable timesteps. Suitable for stiff problems - requiring higher accuracy than GEAR21. Uses ESDIRK32 for startup. + Uses ``ESDIRK32`` as startup solver. Characteristics --------------- - * Stepping Order: 3 (max) - * Error Estimation Order: 2 - * Implicit Variable-Step Multistep + * Order: 3 (stepping) / 2 (error estimate) + * Implicit variable-step multistep * Adaptive timestep - * A(alpha)-stable (based on BDF3) - - When to Use - ----------- - * **Higher accuracy stiff problems**: 3rd order with adaptive stepping - * **Good stability/accuracy balance**: Better accuracy with excellent stability - * **Chemical reactions**: Common in kinetics problems - * **Engineering simulations**: Widely used in practice - + * :math:`A(\\alpha)`-stable (BDF3 stability wedge) + Note ---- - Slightly less stable than GEAR21, but more accurate. For very high accuracy, - use GEAR43 or ESDIRK54. + Good balance of accuracy and stability for stiff block diagrams. The + stability wedge is nearly as wide as ``GEAR21`` (:math:`\\approx 86°`) + while providing an extra order of accuracy. For most stiff systems this + is a practical default when a multistep solver is preferred over ESDIRK. References ---------- .. [1] Gear, C. W. (1971). "Numerical Initial Value Problems in Ordinary Differential Equations". Prentice-Hall. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ @@ -523,39 +566,33 @@ def __init__(self, *solver_args, **solver_kwargs): class GEAR43(GEAR): - """Adaptive-step GEAR integrator using 4th order BDF with variable timesteps. + """Variable-step 4th order BDF with 3rd order error estimate. + :math:`A(\\alpha)`-stable. - Uses 4th order BDF for timestepping and 3rd order BDF for truncation error estimation. - Dynamically computes BDF coefficients for variable timesteps. Suitable for stiff problems - requiring good accuracy. Uses ESDIRK32 for startup. + Uses ``ESDIRK32`` as startup solver. Characteristics --------------- - * Stepping Order: 4 (max) - * Error Estimation Order: 3 - * Implicit Variable-Step Multistep + * Order: 4 (stepping) / 3 (error estimate) + * Implicit variable-step multistep * Adaptive timestep - * A(alpha)-stable (based on BDF4) - - When to Use - ----------- - * **High-accuracy stiff problems**: 4th order with adaptive stepping - * **Demanding applications**: When higher accuracy is needed - * **Smooth stiff dynamics**: Problems with smooth solutions - * **Scientific computing**: Common in research applications - + * :math:`A(\\alpha)`-stable (BDF4 stability wedge, :math:`\\approx 73°`) + Note ---- - Smaller stability angle than GEAR32. For very stiff problems, GEAR21 or GEAR32 - may be more robust. For very high accuracy, use GEAR54 or ESDIRK54. + Narrower stability wedge than ``GEAR32``. Eigenvalues near the imaginary + axis may be poorly damped. Use only when the stiff modes are strongly + dissipative and 4th order accuracy is needed. Otherwise, ``GEAR32`` or + ``ESDIRK43`` are safer choices. References ---------- .. [1] Gear, C. W. (1971). "Numerical Initial Value Problems in Ordinary Differential Equations". Prentice-Hall. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ @@ -572,40 +609,33 @@ def __init__(self, *solver_args, **solver_kwargs): class GEAR54(GEAR): - """Adaptive-step GEAR integrator using 5th order BDF with variable timesteps. + """Variable-step 5th order BDF with 4th order error estimate. + :math:`A(\\alpha)`-stable. - Uses 5th order BDF for timestepping and 4th order BDF for truncation error estimation. - Dynamically computes BDF coefficients for variable timesteps. Suitable for stiff problems - requiring high accuracy, but stability region is smaller than lower-order GEAR methods. - Uses ESDIRK32 for startup. + Uses ``ESDIRK32`` as startup solver. Characteristics --------------- - * Stepping Order: 5 (max) - * Error Estimation Order: 4 - * Implicit Variable-Step Multistep + * Order: 5 (stepping) / 4 (error estimate) + * Implicit variable-step multistep * Adaptive timestep - * A(alpha)-stable (based on BDF5) - - When to Use - ----------- - * **Very high accuracy on mildly stiff problems**: 5th order when stability angle sufficient - * **Smooth stiff problems**: Problems without extreme stiffness - * **High-precision requirements**: Better accuracy than GEAR43 - * **Research applications**: Specialized high-accuracy needs - - Warn + * :math:`A(\\alpha)`-stable (BDF5 stability wedge, :math:`\\approx 51°`) + + Note ---- - Reduced stability compared to lower-order GEAR methods. For very stiff problems, - use GEAR21 or GEAR32. Consider ESDIRK54 as an alternative high-accuracy stiff solver. + The stability wedge is significantly narrower than lower-order GEAR + variants. Only justified for mildly stiff problems where 5th order + accuracy yields a clear efficiency gain. For strongly stiff systems, + ``GEAR21``/``GEAR32`` or ``ESDIRK54`` are more robust. References ---------- .. [1] Gear, C. W. (1971). "Numerical Initial Value Problems in Ordinary Differential Equations". Prentice-Hall. - .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [2] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ @@ -622,47 +652,40 @@ def __init__(self, *solver_args, **solver_kwargs): class GEAR52A(GEAR): - """Adaptive-order, adaptive-stepsize GEAR integrator (Variable-Step Variable-Order BDF). - - This method dynamically adjusts both timestep and BDF order (between 2 and 5) based on - error estimates from lower and higher order predictors. Optimizes step size by using - higher orders for smooth regions and lower, more stable orders for stiff or rapidly - changing regions. Dynamically computes BDF coefficients for variable timesteps and orders. - Uses ESDIRK32 for startup. + """Variable-step, variable-order BDF (orders 2--5). Adapts both timestep + and order automatically. - Error estimation compares the current order solution with predictions from - order n-1 and n+1 formulas to select the optimal order. + At each step the error controller compares estimates from orders + :math:`n-1` and :math:`n+1` and selects the order that minimises the + normalised error, allowing larger steps. Analogous to MATLAB's + ``ode15s``. Uses ``ESDIRK32`` as startup solver. Characteristics --------------- - * Stepping Order: Variable (2 to 5) - * Error Estimation Orders: n-1 and n+1 (relative to current n) - * Implicit Variable-Step, Variable-Order Multistep + * Order: variable, 2--5 + * Implicit variable-step, variable-order multistep * Adaptive timestep and order - * Stability varies with the currently selected order (A-stable or A(alpha)-stable) - - When to Use - ----------- - * **Problems with varying character**: Automatically adapts to changing dynamics - * **Black-box applications**: Minimal tuning required - * **Efficiency priority**: Optimizes order for efficiency - * **General-purpose adaptive stiff solver**: Robust default choice + * Stability: A-stable at order 2, :math:`A(\\alpha)`-stable at orders 3--5 Note ---- - Recommended for problems where the optimal order is unknown. This is similar to - MATLAB's ode15s. Can be more efficient than fixed-order methods for problems with - varying smoothness. + The most autonomous stiff solver in this library. Automatically selects + higher orders in smooth regions for larger steps and drops to low order + in stiff or transient regions for stability. A good default when the + character of the block diagram is unknown or changes during the + simulation (e.g. switching events, varying loads). References ---------- .. [1] Gear, C. W. (1971). "Numerical Initial Value Problems in Ordinary Differential Equations". Prentice-Hall. - .. [2] Shampine, L. F., & Reichelt, M. W. (1997). "The MATLAB ODE Suite". - SIAM Journal on Scientific Computing, 18(1), 1-22. - .. [3] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential Equations II: - Stiff and Differential-Algebraic Problems". Springer Series in Computational - Mathematics, Vol. 14. + .. [2] Shampine, L. F., & Reichelt, M. W. (1997). "The MATLAB ODE + Suite". SIAM Journal on Scientific Computing, 18(1), 1-22. + :doi:`10.1137/S1064827594276424` + .. [3] Hairer, E., & Wanner, G. (1996). "Solving Ordinary Differential + Equations II: Stiff and Differential-Algebraic Problems". Springer + Series in Computational Mathematics, Vol. 14. + :doi:`10.1007/978-3-642-05221-7` """ diff --git a/src/pathsim/solvers/rk4.py b/src/pathsim/solvers/rk4.py index 8d616b06..0ae9f882 100644 --- a/src/pathsim/solvers/rk4.py +++ b/src/pathsim/solvers/rk4.py @@ -17,41 +17,40 @@ class RK4(ExplicitRungeKutta): """Classical four-stage, 4th order explicit Runge-Kutta method. - The most well-known Runge-Kutta method. It provides a good balance - between accuracy and computational cost for non-stiff problems. This is - the standard textbook Runge-Kutta method, often simply called "RK4" or - "the Runge-Kutta method." + .. math:: + + \\begin{aligned} + k_1 &= f(x_n,\\; t_n) \\\\ + k_2 &= f\\!\\left(x_n + \\tfrac{h}{2}\\,k_1,\\; t_n + \\tfrac{h}{2}\\right) \\\\ + k_3 &= f\\!\\left(x_n + \\tfrac{h}{2}\\,k_2,\\; t_n + \\tfrac{h}{2}\\right) \\\\ + k_4 &= f(x_n + h\\,k_3,\\; t_n + h) \\\\ + x_{n+1} &= x_n + \\tfrac{h}{6}(k_1 + 2k_2 + 2k_3 + k_4) + \\end{aligned} Characteristics --------------- * Order: 4 * Stages: 4 - * Explicit - * Fixed timestep only - * Not SSP - * Widely used, good general-purpose explicit solver - - When to Use - ----------- - * **General-purpose integration**: Excellent default choice for smooth, non-stiff problems - * **Fixed timestep applications**: When adaptive stepping is not required - * **Moderate accuracy needs**: Good balance of accuracy and computational cost - * **Educational/reference**: Standard method for comparison and teaching - + * Explicit, fixed timestep + Note ---- - Not suitable for stiff problems. For adaptive timestepping, consider - RKDP54 or RKF45. For problems requiring TVD/SSP properties, use SSPRK methods. + The standard fixed-step explicit solver. Provides a good cost-to-accuracy + ratio for non-stiff block diagrams where the timestep is known a priori + (e.g. real-time or hardware-in-the-loop simulation with a fixed clock). + Not suitable for stiff systems. When accuracy demands vary during a run, + adaptive methods like ``RKDP54`` are more efficient because they + concentrate steps where the dynamics change rapidly. References ---------- .. [1] Kutta, W. (1901). "Beitrag zur näherungsweisen Integration totaler - Differentialgleichungen". Zeitschrift für Mathematik und Physik, 46, 435-453. - .. [2] Butcher, J. C. (2016). "Numerical Methods for Ordinary Differential Equations". - John Wiley & Sons, 3rd Edition. - .. [3] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary - Differential Equations I: Nonstiff Problems". Springer Series in Computational - Mathematics, Vol. 8. + Differentialgleichungen". Zeitschrift für Mathematik und Physik, + 46, 435-453. + .. [2] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary + Differential Equations I: Nonstiff Problems". Springer Series in + Computational Mathematics, Vol. 8. + :doi:`10.1007/978-3-540-78862-1` """ diff --git a/src/pathsim/solvers/rkbs32.py b/src/pathsim/solvers/rkbs32.py index 576655da..b0f759b7 100644 --- a/src/pathsim/solvers/rkbs32.py +++ b/src/pathsim/solvers/rkbs32.py @@ -15,44 +15,34 @@ # SOLVERS ============================================================================== class RKBS32(ExplicitRungeKutta): - """Four-stage, 3rd order explicit Runge-Kutta method by Bogacki and Shampine. + """Bogacki-Shampine 3(2) pair. Four-stage, 3rd order with FSAL property. - Features an embedded 2nd order method for adaptive step size control with FSAL - (First Same As Last) property. The 3rd order result is used for propagation. - Commonly used in software packages (e.g., MATLAB's ode23). Good for problems - requiring low to moderate accuracy with efficiency. + The underlying method of MATLAB's ``ode23``. The First-Same-As-Last + (FSAL) property makes the effective cost three stages per accepted step. Characteristics --------------- - * Order: 3 (Propagating solution) - * Embedded Order: 2 (Error estimation) - * Stages: 4 (3 effective due to FSAL) - * Explicit - * Adaptive timestep - * Efficient low-to-moderate accuracy solver - - When to Use - ----------- - * **Low-to-moderate accuracy needs**: When stringent accuracy is not required - * **Efficiency-focused applications**: Cheaper than 5th order methods - * **Smooth non-stiff problems**: Well-suited for mildly nonlinear problems - * **Default low-order adaptive solver**: Good general-purpose choice for less demanding problems - + * Order: 3 (propagating) / 2 (embedded) + * Stages: 4 (3 effective with FSAL) + * Explicit, adaptive timestep + Note ---- - More efficient than 5th order methods but less accurate. For higher - accuracy requirements, use RKDP54 or RKCK54. Nonetheless a good default - explicit adaptive timestep solver. + A good default when moderate accuracy suffices and per-step cost matters + more than large step sizes. Fewer stages than 5th order pairs, so faster + per step but needs more steps for the same global error. In a PathSim + block diagram with smooth, non-stiff dynamics and relaxed tolerances this + is often the most efficient explicit choice. Switch to ``RKDP54`` when + tighter tolerances are required. References ---------- - .. [1] Bogacki, P., & Shampine, L. F. (1989). "A 3(2) pair of Runge-Kutta formulas". - Applied Mathematics Letters, 2(4), 321-325. - .. [2] Shampine, L. F., & Reichelt, M. W. (1997). "The MATLAB ODE Suite". - SIAM Journal on Scientific Computing, 18(1), 1-22. - .. [3] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary - Differential Equations I: Nonstiff Problems". Springer Series in Computational - Mathematics, Vol. 8. + .. [1] Bogacki, P., & Shampine, L. F. (1989). "A 3(2) pair of + Runge-Kutta formulas". Applied Mathematics Letters, 2(4), + 321-325. :doi:`10.1016/0893-9659(89)90079-7` + .. [2] Shampine, L. F., & Reichelt, M. W. (1997). "The MATLAB ODE + Suite". SIAM Journal on Scientific Computing, 18(1), 1-22. + :doi:`10.1137/S1064827594276424` """ diff --git a/src/pathsim/solvers/rkck54.py b/src/pathsim/solvers/rkck54.py index 425beee8..6d9e6152 100644 --- a/src/pathsim/solvers/rkck54.py +++ b/src/pathsim/solvers/rkck54.py @@ -15,46 +15,35 @@ # SOLVERS ============================================================================== class RKCK54(ExplicitRungeKutta): - """Six-stage, 5th order explicit Runge-Kutta method by Cash and Karp. + """Cash-Karp 5(4) pair. Six stages, 5th order with embedded 4th order + error estimate. - Features an embedded 4th order method. The difference between the 5th and 4th order - results provides a 5th order error estimate, typically used to control the step size - while propagating the 5th order solution (local extrapolation). Known for better - stability properties compared to RKF45. + Designed to improve on the stability properties of the Fehlberg pair + (``RKF45``) while keeping the same stage count. Characteristics --------------- - * Order: 5 (Propagating solution) - * Embedded Order: 4 + * Order: 5 (propagating) / 4 (embedded) * Stages: 6 - * Explicit - * Adaptive timestep - * Good stability, suitable for moderate-to-high accuracy requirements - - When to Use - ----------- - * **Improved stability over RKF45**: When RKF45 exhibits stability issues - * **Moderate-to-high accuracy needs**: 5th order for better accuracy than 3rd order methods - * **Non-stiff problems**: Excellent for smooth, non-stiff ODEs - * **Alternative to RKDP54**: Similar performance, sometimes better for specific problems + * Explicit, adaptive timestep Note ---- - RKDP54 is generally more efficient when first-same-as-last (FSAL) property is implemented - (which it currently is not!), but RKCK54 can have better stability for certain problems. - Both are excellent 5th order adaptive methods. + Comparable to ``RKDP54`` in cost and accuracy for most non-stiff block + diagrams. Can exhibit slightly better stability on problems with + eigenvalues near the imaginary axis. Both pairs are solid 5th order + choices; ``RKDP54`` is the more commonly used default. References ---------- - .. [1] Cash, J. R., & Karp, A. H. (1990). "A variable order Runge-Kutta method for - initial value problems with rapidly varying right-hand sides". ACM Transactions - on Mathematical Software, 16(3), 201-222. - .. [2] Press, W. H., Teukolsky, S. A., Vetterling, W. T., & Flannery, B. P. (2007). - "Numerical Recipes: The Art of Scientific Computing" (3rd ed.). Cambridge - University Press. - .. [3] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary - Differential Equations I: Nonstiff Problems". Springer Series in Computational - Mathematics, Vol. 8. + .. [1] Cash, J. R., & Karp, A. H. (1990). "A variable order Runge-Kutta + method for initial value problems with rapidly varying right-hand + sides". ACM Transactions on Mathematical Software, 16(3), 201-222. + :doi:`10.1145/79505.79507` + .. [2] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving + Ordinary Differential Equations I: Nonstiff Problems". Springer + Series in Computational Mathematics, Vol. 8. + :doi:`10.1007/978-3-540-78862-1` """ diff --git a/src/pathsim/solvers/rkdp54.py b/src/pathsim/solvers/rkdp54.py index 20ef15fa..93b22933 100644 --- a/src/pathsim/solvers/rkdp54.py +++ b/src/pathsim/solvers/rkdp54.py @@ -15,43 +15,38 @@ # SOLVERS ============================================================================== class RKDP54(ExplicitRungeKutta): - """Seven-stage, 5th order explicit Runge-Kutta method by Dormand and Prince (DOPRI5). + """Dormand-Prince 5(4) pair (DOPRI5). Seven stages, 5th order with + embedded 4th order error estimate. - Features an embedded 4th order method. Widely considered one of the most efficient - general-purpose adaptive step size solvers for non-stiff problems requiring moderate - to high accuracy. The 5th order result is used for propagation. Used as the basis for - MATLAB's ode45. FSAL property (not available in this implementation). + The industry-standard adaptive explicit solver and the basis of MATLAB's + ``ode45``. Has the FSAL property (not exploited in this implementation, + so all seven stages are evaluated each step). Characteristics --------------- - * Order: 5 (Propagating solution) - * Embedded Order: 4 - * Stages: 7 (6 effective due to FSAL, not here though) - * Explicit - * Adaptive timestep - * Industry-standard adaptive solver - - When to Use - ----------- - * **Default adaptive solver**: Excellent first choice for most non-stiff problems - * **Moderate-to-high accuracy**: 5th order provides good accuracy efficiently - * **General-purpose integration**: Reliable for a wide variety of ODE systems - * **Industry standard**: Well-tested and widely used in production software - + * Order: 5 (propagating) / 4 (embedded) + * Stages: 7 + * Explicit, adaptive timestep + Note ---- - Recommended as the primary adaptive solver for non-stiff problems. For stiff - problems, use BDF or ESDIRK methods. For very high accuracy, consider RKV65 or RKDP87. + Recommended default for non-stiff block diagrams. Handles smooth + nonlinear dynamics, coupled oscillators, and signal-processing chains + efficiently. If the simulation warns about excessive step rejections or + very small timesteps, the system is likely stiff and an implicit solver + (``ESDIRK43``, ``GEAR52A``) should be used instead. For very tight + tolerances on smooth problems, ``RKV65`` or ``RKDP87`` can be more + efficient per unit accuracy. References ---------- - .. [1] Dormand, J. R., & Prince, P. J. (1980). "A family of embedded Runge-Kutta - formulae". Journal of Computational and Applied Mathematics, 6(1), 19-26. - .. [2] Shampine, L. F., & Reichelt, M. W. (1997). "The MATLAB ODE Suite". - SIAM Journal on Scientific Computing, 18(1), 1-22. - .. [3] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary - Differential Equations I: Nonstiff Problems". Springer Series in Computational - Mathematics, Vol. 8. + .. [1] Dormand, J. R., & Prince, P. J. (1980). "A family of embedded + Runge-Kutta formulae". Journal of Computational and Applied + Mathematics, 6(1), 19-26. + :doi:`10.1016/0771-050X(80)90013-3` + .. [2] Shampine, L. F., & Reichelt, M. W. (1997). "The MATLAB ODE + Suite". SIAM Journal on Scientific Computing, 18(1), 1-22. + :doi:`10.1137/S1064827594276424` """ diff --git a/src/pathsim/solvers/rkdp87.py b/src/pathsim/solvers/rkdp87.py index 9090adf6..42e59ff6 100644 --- a/src/pathsim/solvers/rkdp87.py +++ b/src/pathsim/solvers/rkdp87.py @@ -15,44 +15,39 @@ # SOLVERS ============================================================================== class RKDP87(ExplicitRungeKutta): - """Thirteen-stage, 8th order explicit Runge-Kutta method by Dormand and Prince (DOP853). + """Dormand-Prince 8(7) pair (DOP853). Thirteen stages, 8th order with + embedded 7th order error estimate. - Features an embedded 7th order method for adaptive step size control. Designed for - problems requiring very high accuracy with excellent error estimation. This is one - of the most efficient 8th order methods available. FSAL property (not available in - this implementation). + The highest-order general-purpose explicit pair in this library. Has the + FSAL property (not exploited in this implementation). Characteristics --------------- - * Order: 8 (Propagating solution) - * Embedded Order: 7 - * Stages: 13 (12 effective due to FSAL) - * Explicit - * Adaptive timestep - * State-of-the-art very high-order solver - - When to Use - ----------- - * **Extremely high accuracy**: When very tight error tolerances are required - * **Smooth high-dimensional problems**: Excellent for smooth ODEs in many dimensions - * **Long-time precision integration**: Orbital mechanics, celestial mechanics - * **Benchmark computations**: Reference solutions for method comparison - + * Order: 8 (propagating) / 7 (embedded) + * Stages: 13 + * Explicit, adaptive timestep + Note ---- - Generally recommended as the highest-order general-purpose explicit method. More - efficient than RKF78 for the same accuracy level. Only use when very high accuracy - justifies the 13-stage computational cost. + Only worthwhile when the dynamics are very smooth and tolerances are + extremely tight (roughly :math:`10^{-10}` or below). The 13 function + evaluations per step are expensive, but the 8th order convergence means + the step size can be much larger than with lower-order methods at the + same error. Suitable for generating reference solutions to validate other + solvers in a block diagram. For typical engineering tolerances + (:math:`10^{-4}`--:math:`10^{-8}`), ``RKDP54`` or ``RKV65`` are more + efficient. References ---------- - .. [1] Dormand, J. R., & Prince, P. J. (1981). "High order embedded Runge-Kutta - formulae". Journal of Computational and Applied Mathematics, 7(1), 67-75. - .. [2] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary - Differential Equations I: Nonstiff Problems". Springer Series in Computational - Mathematics, Vol. 8. - .. [3] Prince, P. J., & Dormand, J. R. (1981). "High order embedded Runge-Kutta - formulae". Journal of Computational and Applied Mathematics, 7(1), 67-75. + .. [1] Prince, P. J., & Dormand, J. R. (1981). "High order embedded + Runge-Kutta formulae". Journal of Computational and Applied + Mathematics, 7(1), 67-75. + :doi:`10.1016/0771-050X(81)90010-3` + .. [2] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving + Ordinary Differential Equations I: Nonstiff Problems". Springer + Series in Computational Mathematics, Vol. 8. + :doi:`10.1007/978-3-540-78862-1` """ diff --git a/src/pathsim/solvers/rkf21.py b/src/pathsim/solvers/rkf21.py index 9e2c307e..a7b6ddef 100644 --- a/src/pathsim/solvers/rkf21.py +++ b/src/pathsim/solvers/rkf21.py @@ -15,42 +15,33 @@ # SOLVERS ============================================================================== class RKF21(ExplicitRungeKutta): - """Three-stage, 2nd order embedded Runge-Kutta-Fehlberg method. - - Features an embedded 1st order method for adaptive step size control. This is a - classic low-order adaptive method. The three stages make it computationally cheap, - but the low order limits accuracy. The error estimate is also less accurate than - higher-order methods. + """Three-stage, 2nd order Runge-Kutta-Fehlberg method with embedded 1st order error estimate. Characteristics --------------- - * Order: 2 (Propagating solution) - * Embedded Order: 1 (Error estimation) + * Order: 2 (propagating) / 1 (embedded) * Stages: 3 - * Explicit - * Adaptive timestep - * Efficient but low accuracy - - When to Use - ----------- - * **Computationally cheap adaptive stepping**: When you need some adaptive control but minimal cost - * **Coarse integration**: Problems where high accuracy is not required - * **Event detection**: When timestep is limited by events rather than truncation error - * **Initial exploration**: Quick preliminary runs before using higher-order methods + * Explicit, adaptive timestep Note ---- - Low accuracy. For most applications requiring adaptive stepping, RKBS32 or RKDP54 are - better choices. + The cheapest adaptive explicit method available. The low order means the + error estimate itself is coarse, so step-size control is less reliable + than with higher-order pairs. Useful for rough exploratory runs of a new + block diagram or when step size is dominated by discrete events (zero + crossings, scheduled triggers) rather than truncation error. For + production simulations, ``RKBS32`` or ``RKDP54`` are almost always + preferable. References ---------- - .. [1] Fehlberg, E. (1969). "Low-order classical Runge-Kutta formulas with stepsize - control and their application to some heat transfer problems". NASA Technical - Report TR R-315. - .. [2] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary - Differential Equations I: Nonstiff Problems". Springer Series in Computational - Mathematics, Vol. 8. + .. [1] Fehlberg, E. (1969). "Low-order classical Runge-Kutta formulas + with stepsize control and their application to some heat transfer + problems". NASA Technical Report TR R-315. + .. [2] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving + Ordinary Differential Equations I: Nonstiff Problems". Springer + Series in Computational Mathematics, Vol. 8. + :doi:`10.1007/978-3-540-78862-1` """ diff --git a/src/pathsim/solvers/rkf45.py b/src/pathsim/solvers/rkf45.py index 5a787d15..fb568517 100644 --- a/src/pathsim/solvers/rkf45.py +++ b/src/pathsim/solvers/rkf45.py @@ -15,45 +15,35 @@ # SOLVERS ============================================================================== class RKF45(ExplicitRungeKutta): - """Six-stage, 4th order explicit Runge-Kutta-Fehlberg method. + """Runge-Kutta-Fehlberg 4(5) pair. Six stages, 4th order propagation with + 5th order error estimate. - Features an embedded 5th order method. The difference between the 5th and 4th order - results provides a 5th order error estimate. Typically, the 4th order solution is - propagated (local extrapolation available). A classic adaptive step size method, - though often superseded in efficiency by Dormand-Prince methods. + The historically first widely-used embedded pair for automatic step-size + control. The 4th order solution is propagated; the difference to the 5th + order solution provides a local error estimate. Characteristics --------------- - * Order: 4 (Propagating solution) - * Embedded Order: 5 (Error estimation) + * Order: 4 (propagating) / 5 (error estimate) * Stages: 6 - * Explicit - * Adaptive timestep - * Classic adaptive method, good for moderate accuracy - - When to Use - ----------- - * **Moderate accuracy requirements**: Good balance for many engineering applications - * **Well-established benchmarks**: When comparing against historical results - * **Non-stiff smooth problems**: Standard choice for a wide range of ODEs + * Explicit, adaptive timestep Note ---- - While this is a classic method, RKDP54 or RKCK54 generally offer better efficiency - for the same computational cost. Consider RKDP54 or RKCK54 for new applications unless - specific properties of RKF45 are required. + Largely superseded by the Dormand-Prince (``RKDP54``) and Cash-Karp + (``RKCK54``) pairs, which achieve better accuracy per function evaluation + on most problems. Still useful for reproducing legacy results or when + comparing against published benchmarks that used RKF45. References ---------- - .. [1] Fehlberg, E. (1969). "Low-order classical Runge-Kutta formulas with stepsize - control and their application to some heat transfer problems". NASA Technical - Report TR R-315. - .. [2] Fehlberg, E. (1970). "Klassische Runge-Kutta-Formeln vierter und niedrigerer - Ordnung mit Schrittweiten-Kontrolle und ihre Anwendung auf Wärmeleitungsprobleme". - Computing, 6(1-2), 61-71. - .. [3] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary - Differential Equations I: Nonstiff Problems". Springer Series in Computational - Mathematics, Vol. 8. + .. [1] Fehlberg, E. (1969). "Low-order classical Runge-Kutta formulas + with stepsize control and their application to some heat transfer + problems". NASA Technical Report TR R-315. + .. [2] Fehlberg, E. (1970). "Klassische Runge-Kutta-Formeln vierter und + niedrigerer Ordnung mit Schrittweiten-Kontrolle und ihre Anwendung + auf Wärmeleitungsprobleme". Computing, 6(1-2), 61-71. + :doi:`10.1007/BF02241732` """ diff --git a/src/pathsim/solvers/rkf78.py b/src/pathsim/solvers/rkf78.py index a13a87c9..3f5978a3 100644 --- a/src/pathsim/solvers/rkf78.py +++ b/src/pathsim/solvers/rkf78.py @@ -15,42 +15,31 @@ # SOLVERS ============================================================================== class RKF78(ExplicitRungeKutta): - """Thirteen-stage, 7th order explicit Runge-Kutta-Fehlberg method. - - Features an embedded 8th order method for error estimation. The difference provides - an 8th order error estimate. The 7th order solution is typically propagated. Designed - for very high accuracy requirements and long-time integration where precision is critical. + """Runge-Kutta-Fehlberg 7(8) pair. Thirteen stages, 7th order propagation + with 8th order error estimate. Characteristics --------------- - * Order: 7 (Propagating solution) - * Embedded Order: 8 (Error estimation) + * Order: 7 (propagating) / 8 (error estimate) * Stages: 13 - * Explicit - * Adaptive timestep - * Very high accuracy, nearly symplectic properties - - When to Use - ----------- - * **Very high accuracy needs**: When stringent error tolerances are essential - * **Long-time integration**: Problems requiring stable, accurate integration over long periods - * **Smooth dynamics**: Highly smooth problems where high order is efficient - * **Scientific precision**: Astronomical calculations, molecular dynamics, precision engineering - + * Explicit, adaptive timestep + Note ---- - Expensive per step (13 stages), but can take very large steps with tight tolerances. - Not suitable for non-smooth problems or when function evaluations are expensive. + One of the earliest very-high-order embedded pairs. At the same stage + count, the Dormand-Prince pair (``RKDP87``) generally provides better + error constants. Consider ``RKDP87`` for new work unless Fehlberg-pair + compatibility is required. References ---------- - .. [1] Fehlberg, E. (1968). "Classical fifth-, sixth-, seventh-, and eighth-order - Runge-Kutta formulas with stepsize control". NASA Technical Report TR R-287. - .. [2] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary - Differential Equations I: Nonstiff Problems". Springer Series in Computational - Mathematics, Vol. 8. - .. [3] Prince, P. J., & Dormand, J. R. (1981). "High order embedded Runge-Kutta - formulae". Journal of Computational and Applied Mathematics, 7(1), 67-75. + .. [1] Fehlberg, E. (1968). "Classical fifth-, sixth-, seventh-, and + eighth-order Runge-Kutta formulas with stepsize control". NASA + Technical Report TR R-287. + .. [2] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving + Ordinary Differential Equations I: Nonstiff Problems". Springer + Series in Computational Mathematics, Vol. 8. + :doi:`10.1007/978-3-540-78862-1` """ diff --git a/src/pathsim/solvers/rkv65.py b/src/pathsim/solvers/rkv65.py index 259e85a2..edd7169b 100644 --- a/src/pathsim/solvers/rkv65.py +++ b/src/pathsim/solvers/rkv65.py @@ -15,42 +15,33 @@ # SOLVERS ============================================================================== class RKV65(ExplicitRungeKutta): - """Nine-stage, 6th order explicit Runge-Kutta method by Verner. - - Features an embedded 5th order method for adaptive step size control. This is the - 'most robust' 9-stage 6(5) pair from Jim Verner's collection, designed for efficient - high-accuracy integration of non-stiff problems. Offers better accuracy than 5th - order methods while being more efficient than 8th order methods. + """Verner 6(5) "most robust" pair. Nine stages, 6th order with + embedded 5th order error estimate. Characteristics --------------- - * Order: 6 (Propagating solution) - * Embedded Order: 5 + * Order: 6 (propagating) / 5 (embedded) * Stages: 9 - * Explicit - * Adaptive timestep - * Efficient high-order method for non-stiff problems + * Explicit, adaptive timestep - When to Use - ----------- - * **High-accuracy requirements**: When 5th order is insufficient but 8th order is overkill - * **Smooth non-stiff problems**: Excellent for problems with smooth solutions - * **Stringent error tolerances**: When tight tolerances are needed efficiently - * **Scientific computing**: Common in astronomical and molecular dynamics simulations - Note ---- - More expensive per step than 5th order methods, but can take larger steps for the same - accuracy. For very high accuracy, consider RKF78 or RKDP87. + Fills the gap between 5th order pairs (``RKDP54``) and the expensive 8th + order ``RKDP87``. The extra stages pay off when the dynamics are smooth + and tolerances are tight (roughly :math:`10^{-8}` or below), because the + higher order allows much larger steps. For tolerances in the + :math:`10^{-4}`--:math:`10^{-6}` range, ``RKDP54`` is usually cheaper + overall due to fewer stages. References ---------- - .. [1] Verner, J. H. (2010). "Numerically optimal Runge-Kutta pairs with interpolants". - Numerical Algorithms, 53(2-3), 383-396. - .. [2] Verner's Refuge for Runge-Kutta Pairs: https://www.sfu.ca/~jverner/ - .. [3] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving Ordinary - Differential Equations I: Nonstiff Problems". Springer Series in Computational - Mathematics, Vol. 8. + .. [1] Verner, J. H. (2010). "Numerically optimal Runge-Kutta pairs + with interpolants". Numerical Algorithms, 53(2-3), 383-396. + :doi:`10.1007/s11075-009-9290-3` + .. [2] Hairer, E., Nørsett, S. P., & Wanner, G. (1993). "Solving + Ordinary Differential Equations I: Nonstiff Problems". Springer + Series in Computational Mathematics, Vol. 8. + :doi:`10.1007/978-3-540-78862-1` """ diff --git a/src/pathsim/solvers/ssprk22.py b/src/pathsim/solvers/ssprk22.py index 25c507e2..dc99a7ad 100644 --- a/src/pathsim/solvers/ssprk22.py +++ b/src/pathsim/solvers/ssprk22.py @@ -15,44 +15,37 @@ # SOLVERS ============================================================================== class SSPRK22(ExplicitRungeKutta): - """Two-stage, 2nd order Strong Stability Preserving (SSP) explicit Runge-Kutta method. + """Two-stage, 2nd order Strong Stability Preserving (SSP) Runge-Kutta method. - Also known as Heun's method or the explicit midpoint method. SSP methods are designed - to preserve stability properties (like total variation diminishing - TVD) when solving - hyperbolic PDEs with spatial discretizations that have strong stability properties. - Also effective as a general-purpose low-order explicit method. + Also known as Heun's method. SSP methods preserve monotonicity and total + variation diminishing (TVD) properties of the spatial discretisation under + a timestep restriction scaled by the SSP coefficient. Characteristics --------------- * Order: 2 * Stages: 2 - * Explicit (SSP) - * Fixed timestep only - * SSP coefficient: :math:`C = 1` - * Good balance of simplicity, cost, and stability - - When to Use - ----------- - * **Hyperbolic PDEs**: Ideal for shock-capturing schemes and conservation laws - * **TVD/SSP requirements**: When preserving monotonicity or boundedness is critical - * **Discontinuous solutions**: Shocks, contact discontinuities, rarefactions - * **Method of lines**: Time integration of spatially discretized PDEs - + * Explicit, fixed timestep + * SSP coefficient :math:`\\mathcal{C} = 1` + Note ---- - Computational fluid dynamics, shallow water equations, traffic flow, - Burgers' equation, Euler equations. + Relevant when a block diagram wraps a method-of-lines discretisation of a + hyperbolic PDE (e.g. shallow water, compressible Euler) inside an ``ODE`` + block and the spatial operator is TVD under forward Euler. For typical + ODE-based block diagrams without such structure, ``RK4`` or ``RKDP54`` + are more appropriate choices. References ---------- - .. [1] Shu, C. W., & Osher, S. (1988). "Efficient implementation of essentially - non-oscillatory shock-capturing schemes". Journal of Computational Physics, - 77(2), 439-471. - .. [2] Gottlieb, S., Shu, C. W., & Tadmor, E. (2001). "Strong stability-preserving - high-order time discretization methods". SIAM Review, 43(1), 89-112. - .. [3] Ketcheson, D. I. (2008). "Highly efficient strong stability-preserving - Runge-Kutta methods with low-storage implementations". SIAM Journal on - Scientific Computing, 30(4), 2113-2136. + .. [1] Shu, C.-W., & Osher, S. (1988). "Efficient implementation of + essentially non-oscillatory shock-capturing schemes". Journal of + Computational Physics, 77(2), 439-471. + :doi:`10.1016/0021-9991(88)90177-5` + .. [2] Gottlieb, S., Shu, C.-W., & Tadmor, E. (2001). "Strong + stability-preserving high-order time discretization methods". + SIAM Review, 43(1), 89-112. + :doi:`10.1137/S003614450036757X` """ diff --git a/src/pathsim/solvers/ssprk33.py b/src/pathsim/solvers/ssprk33.py index f474740a..8dee3b5b 100644 --- a/src/pathsim/solvers/ssprk33.py +++ b/src/pathsim/solvers/ssprk33.py @@ -15,40 +15,40 @@ # SOLVERS ============================================================================== class SSPRK33(ExplicitRungeKutta): - """Three-stage, 3rd order Strong Stability Preserving (SSP) explicit Runge-Kutta method. - - Offers higher accuracy than SSPRK22 while maintaining the SSP property. This is the - optimal 3-stage 3rd order SSP method. A popular choice for problems where TVD - properties are important or when a simple, stable 3rd order explicit method is needed. - - Characteristics: - * Order: 3 - * Stages: 3 - * Explicit (SSP) - * Fixed timestep only - * SSP coefficient: :math:`C = 1` - * Optimal 3-stage SSP method - * Good stability properties for an explicit 3rd order method - - When to Use - ----------- - * **Hyperbolic conservation laws**: Standard choice for higher-order TVD schemes - * **Higher accuracy than SSPRK22**: When 3rd order accuracy is needed with SSP - * **WENO schemes**: Common pairing with weighted essentially non-oscillatory methods - * **Compressible flow**: Euler and Navier-Stokes equations with shocks - - **Recommended** as the standard SSP method for most applications requiring 3rd order - accuracy. For enhanced stability, consider SSPRK34 (4 stages). + """Three-stage, 3rd order optimal SSP Runge-Kutta method. + + The unique optimal three-stage, 3rd order SSP scheme. Commonly paired + with WENO and ENO spatial discretisations for hyperbolic conservation + laws. + + Characteristics + --------------- + * Order: 3 + * Stages: 3 + * Explicit, fixed timestep + * SSP coefficient :math:`\\mathcal{C} = 1` + + Note + ---- + The standard SSP time integrator for method-of-lines PDE discretisations + inside ``ODE`` blocks. If the spatial operator is TVD under forward Euler, + this method preserves that property at the same timestep restriction. + When stability is borderline, ``SSPRK34`` allows roughly twice the + timestep at the cost of one extra stage. References ---------- - .. [1] Shu, C. W., & Osher, S. (1988). "Efficient implementation of essentially - non-oscillatory shock-capturing schemes". Journal of Computational Physics, - 77(2), 439-471. - .. [2] Gottlieb, S., Shu, C. W., & Tadmor, E. (2001). "Strong stability-preserving - high-order time discretization methods". SIAM Review, 43(1), 89-112. - .. [3] Gottlieb, S., Ketcheson, D. I., & Shu, C. W. (2011). "Strong Stability - Preserving Runge-Kutta and Multistep Time Discretizations". World Scientific. + .. [1] Shu, C.-W., & Osher, S. (1988). "Efficient implementation of + essentially non-oscillatory shock-capturing schemes". Journal of + Computational Physics, 77(2), 439-471. + :doi:`10.1016/0021-9991(88)90177-5` + .. [2] Gottlieb, S., Shu, C.-W., & Tadmor, E. (2001). "Strong + stability-preserving high-order time discretization methods". + SIAM Review, 43(1), 89-112. + :doi:`10.1137/S003614450036757X` + .. [3] Gottlieb, S., Ketcheson, D. I., & Shu, C.-W. (2011). "Strong + Stability Preserving Runge-Kutta and Multistep Time + Discretizations". World Scientific. :doi:`10.1142/7498` """ diff --git a/src/pathsim/solvers/ssprk34.py b/src/pathsim/solvers/ssprk34.py index f81e7506..a38cb62f 100644 --- a/src/pathsim/solvers/ssprk34.py +++ b/src/pathsim/solvers/ssprk34.py @@ -15,40 +15,35 @@ # SOLVERS ============================================================================== class SSPRK34(ExplicitRungeKutta): - """Four-stage, 3rd order Strong Stability Preserving (SSP) explicit Runge-Kutta method. + """Four-stage, 3rd order SSP Runge-Kutta method with SSP coefficient 2. - Provides a larger stability region and higher SSP coefficient compared to SSPRK33, - particularly along the negative real axis, at the cost of an additional stage. Useful - when stability is more critical than computational cost for a 3rd order explicit method. + An extra stage compared to ``SSPRK33`` doubles the allowable SSP timestep + (:math:`\\mathcal{C} = 2`), giving a larger effective stability region + along the negative real axis. Characteristics --------------- * Order: 3 * Stages: 4 - * Explicit (SSP) - * Fixed timestep only - * SSP coefficient: :math:`C = 2` - * Enhanced stability compared to SSPRK33 + * Explicit, fixed timestep + * SSP coefficient :math:`\\mathcal{C} = 2` - When to Use - ----------- - * **Larger timesteps**: SSP coefficient of 2 allows larger stable timesteps - * **Difficult hyperbolic problems**: More robust than SSPRK33 for challenging cases - * **Extra stability needed**: When SSPRK33 exhibits instabilities - * **Worth extra stage**: When the improved stability justifies 4 stages vs 3 - - **Trade-off**: More expensive than SSPRK33 but allows larger timesteps and better - stability. Use when stability is critical. + Note + ---- + Preferable over ``SSPRK33`` when a method-of-lines ``ODE`` block is close + to the SSP timestep limit and the cost of one additional stage per step is + acceptable in exchange for a factor-of-two relaxation in the CFL + constraint. References ---------- - .. [1] Spiteri, R. J., & Ruuth, S. J. (2002). "A new class of optimal high-order - strong-stability-preserving time discretization methods". SIAM Journal on - Numerical Analysis, 40(2), 469-491. - .. [2] Gottlieb, S., Shu, C. W., & Tadmor, E. (2001). "Strong stability-preserving - high-order time discretization methods". SIAM Review, 43(1), 89-112. - .. [3] Gottlieb, S., Ketcheson, D. I., & Shu, C. W. (2011). "Strong Stability - Preserving Runge-Kutta and Multistep Time Discretizations". World Scientific. + .. [1] Spiteri, R. J., & Ruuth, S. J. (2002). "A new class of optimal + high-order strong-stability-preserving time discretization methods". + SIAM Journal on Numerical Analysis, 40(2), 469-491. + :doi:`10.1137/S0036142901389025` + .. [2] Gottlieb, S., Ketcheson, D. I., & Shu, C.-W. (2011). "Strong + Stability Preserving Runge-Kutta and Multistep Time + Discretizations". World Scientific. :doi:`10.1142/7498` """ diff --git a/src/pathsim/solvers/steadystate.py b/src/pathsim/solvers/steadystate.py index ee38eb86..81df804c 100644 --- a/src/pathsim/solvers/steadystate.py +++ b/src/pathsim/solvers/steadystate.py @@ -17,18 +17,25 @@ # SOLVERS ============================================================================== class SteadyState(ImplicitSolver): - """Pseudo-solver that finds the time-independent steady-state solution (DC operating point). + """Pseudo-solver for computing the DC operating point (steady state). - This works by modifying the fixed-point iteration target. Instead of solving - :math:`x_{n+1} = G(x_{n+1})` for an implicit step, it aims to solve the algebraic equation - :math:`f(x, u, t_{steady}) = 0` by finding the fixed point of :math:`x = x + f(x, u, t_{steady})`. - It uses the same internal optimizer (e.g., NewtonAnderson) as other implicit solvers. + Solves :math:`f(x, u, t) = 0` by iterating the fixed-point map + :math:`x \\leftarrow x + f(x, u, t)` using the internal optimizer + (Newton-Anderson). Not a time-stepping method. Characteristics --------------- - * Purpose: Find steady-state (:math:`dx/dt = 0`) + * Purpose: find :math:`\\dot{x} = 0` * Implicit (uses optimizer) - * Not a time-stepping method. + * No time integration + + Note + ---- + Used by ``Simulation.steady_state()`` to initialise block diagrams at + their equilibrium before a transient run. Particularly useful when + dynamic blocks (``Integrator``, ``ODE``, ``LTI``) have non-trivial + equilibria that depend on the surrounding algebraic network. + """ def solve(self, f, J, dt): diff --git a/src/pathsim/subsystem.py b/src/pathsim/subsystem.py index 3ec211dc..6dec9691 100644 --- a/src/pathsim/subsystem.py +++ b/src/pathsim/subsystem.py @@ -176,31 +176,30 @@ def __init__(self, #internal graph representation -> initialized later self.graph = None + self._graph_dirty = False #internal algebraic loop solvers -> initialized later self.boosters = None #internal connecions - self.connections = set() - if connections: - self.connections.update(connections) - + self.connections = list(connections) if connections else [] + #collect and organize internal blocks - self.blocks = set() + self.blocks = [] self.interface = None if blocks: for block in blocks: - if isinstance(block, Interface): - + if isinstance(block, Interface): + if self.interface is not None: #interface block is already defined raise ValueError("Subsystem can only have one 'Interface' block!") - + self.interface = block - else: + else: #regular blocks - self.blocks.add(block) + self.blocks.append(block) #check if interface is defined if self.interface is None: @@ -254,13 +253,139 @@ def __contains__(self, other): return other in self.blocks or other in self.connections + # adding and removing system components --------------------------------------------------- + + def add_block(self, block): + """Adds a new block to the subsystem. + + This works dynamically for running simulations. + + Parameters + ---------- + block : Block + block to add to the subsystem + """ + if block in self.blocks: + raise ValueError(f"block {block} already part of subsystem") + + #initialize solver if available + if hasattr(self, '_Solver'): + block.set_solver(self._Solver, self._solver_parent, **self._solver_args) + if block.engine: + self._blocks_dyn.append(block) + + self.blocks.append(block) + + if self.graph: + self._graph_dirty = True + + + def remove_block(self, block): + """Removes a block from the subsystem. + + This works dynamically for running simulations. + + Parameters + ---------- + block : Block + block to remove from the subsystem + """ + if block not in self.blocks: + raise ValueError(f"block {block} not part of subsystem") + + self.blocks.remove(block) + + #remove from dynamic list + if hasattr(self, '_blocks_dyn') and block in self._blocks_dyn: + self._blocks_dyn.remove(block) + + if self.graph: + self._graph_dirty = True + + + def add_connection(self, connection): + """Adds a new connection to the subsystem. + + This works dynamically for running simulations. + + Parameters + ---------- + connection : Connection + connection to add to the subsystem + """ + if connection in self.connections: + raise ValueError(f"{connection} already part of subsystem") + + self.connections.append(connection) + + if self.graph: + self._graph_dirty = True + + + def remove_connection(self, connection): + """Removes a connection from the subsystem. + + This works dynamically for running simulations. + + Parameters + ---------- + connection : Connection + connection to remove from the subsystem + """ + if connection not in self.connections: + raise ValueError(f"{connection} not part of subsystem") + + self.connections.remove(connection) + + if self.graph: + self._graph_dirty = True + + + def add_event(self, event): + """Adds an event to the subsystem. + + This works dynamically for running simulations. + + Parameters + ---------- + event : Event + event to add to the subsystem + """ + if event in self._events: + raise ValueError(f"{event} already part of subsystem") + + self._events.append(event) + + + def remove_event(self, event): + """Removes an event from the subsystem. + + This works dynamically for running simulations. + + Parameters + ---------- + event : Event + event to remove from the subsystem + """ + if event not in self._events: + raise ValueError(f"{event} not part of subsystem") + + self._events.remove(event) + + # subsystem graph assembly -------------------------------------------------------------- def _assemble_graph(self): - """Assemble internal graph of subsystem for fast + """Assemble internal graph of subsystem for fast algebraic evaluation during simulation. """ - self.graph = Graph({*self.blocks, self.interface}, self.connections) + + #reset all block inputs to clear stale values from removed connections + for block in self.blocks: + block.inputs.reset() + + self.graph = Graph([*self.blocks, self.interface], self.connections) + self._graph_dirty = False #create boosters for loop closing connections if self.graph.has_loops: @@ -330,6 +455,106 @@ def reset(self): block.reset() + @staticmethod + def _checkpoint_key(type_name, type_counts): + """Generate a deterministic checkpoint key from block/event type + and occurrence index (e.g. 'Integrator_0', 'Scope_1'). + """ + idx = type_counts.get(type_name, 0) + type_counts[type_name] = idx + 1 + return f"{type_name}_{idx}" + + + def to_checkpoint(self, prefix, recordings=False): + """Serialize subsystem state by recursively checkpointing internal blocks. + + Parameters + ---------- + prefix : str + key prefix for NPZ arrays (assigned by simulation) + recordings : bool + include recording data (for Scope blocks) + + Returns + ------- + json_data : dict + JSON-serializable metadata + npz_data : dict + numpy arrays keyed by path + """ + json_data = { + "type": self.__class__.__name__, + "active": self._active, + "blocks": [], + } + npz_data = {} + + #checkpoint interface block + if_json, if_npz = self.interface.to_checkpoint(f"{prefix}/interface", recordings=recordings) + json_data["interface"] = if_json + npz_data.update(if_npz) + + #checkpoint internal blocks by type + insertion order + type_counts = {} + for block in self.blocks: + key = f"{prefix}/{self._checkpoint_key(block.__class__.__name__, type_counts)}" + b_json, b_npz = block.to_checkpoint(key, recordings=recordings) + b_json["_key"] = key + json_data["blocks"].append(b_json) + npz_data.update(b_npz) + + #checkpoint subsystem-level events + if self._events: + evt_jsons = [] + for i, event in enumerate(self._events): + evt_prefix = f"{prefix}/evt_{i}" + e_json, e_npz = event.to_checkpoint(evt_prefix) + evt_jsons.append(e_json) + npz_data.update(e_npz) + json_data["events"] = evt_jsons + + return json_data, npz_data + + + def load_checkpoint(self, prefix, json_data, npz): + """Restore subsystem state by recursively loading internal blocks. + + Parameters + ---------- + prefix : str + key prefix for NPZ arrays (assigned by simulation) + json_data : dict + subsystem metadata from checkpoint JSON + npz : dict-like + numpy arrays from checkpoint NPZ + """ + #verify type + if json_data["type"] != self.__class__.__name__: + raise ValueError( + f"Checkpoint type mismatch: expected '{self.__class__.__name__}', " + f"got '{json_data['type']}'" + ) + + self._active = json_data["active"] + + #restore interface block + if "interface" in json_data: + self.interface.load_checkpoint(f"{prefix}/interface", json_data["interface"], npz) + + #restore internal blocks by type + insertion order + block_data = {b["_key"]: b for b in json_data.get("blocks", [])} + type_counts = {} + for block in self.blocks: + key = f"{prefix}/{self._checkpoint_key(block.__class__.__name__, type_counts)}" + if key in block_data: + block.load_checkpoint(key, block_data[key], npz) + + #restore subsystem-level events + if self._events and "events" in json_data: + for i, (event, evt_data) in enumerate(zip(self._events, json_data["events"])): + event.load_checkpoint(f"{prefix}/evt_{i}", evt_data, npz) + + def on(self): """Activate the subsystem and all internal blocks, sets the boolean evaluation flag to 'True'. @@ -354,8 +579,8 @@ def linearize(self, t): This is done by linearizing the internal 'Operator' and 'DynamicOperator' instances of all the internal blocks of the subsystem in the current system - operating point. The operators create 1st order tayler approximations - internally and use them on subsequent calls after linarization. + operating point. The operators create 1st order taylor approximations + internally and use them on subsequent calls after linearization. Recursively traverses down the hierarchy for nested subsystems and linearizes all of them. @@ -422,20 +647,24 @@ def sample(self, t, dt): # methods for block output and state updates -------------------------------------------- def update(self, t): - """Update the instant time components of the internal blocks + """Update the instant time components of the internal blocks to evaluate the (distributed) system equation. Parameters ---------- t : float - evaluation time + evaluation time """ + #lazy graph rebuild if dirty + if self._graph_dirty: + self._assemble_graph() + #evaluate DAG self._dag(t) #algebraic loops -> solve them - if self.graph.has_loops: + if self.graph.has_loops: self._loops(t) @@ -607,6 +836,11 @@ def set_solver(self, Solver, parent, **solver_args): args to initialize solver with """ + #cache solver info for dynamic block additions + self._Solver = Solver + self._solver_parent = parent + self._solver_args = solver_args + #set integration engines and assemble list of dynamic blocks self._blocks_dyn = [] for block in self.blocks: diff --git a/src/pathsim/utils/__init__.py b/src/pathsim/utils/__init__.py index 4e586b8c..ce95ea7c 100644 --- a/src/pathsim/utils/__init__.py +++ b/src/pathsim/utils/__init__.py @@ -1 +1,2 @@ from .deprecation import deprecated +from .mutable import mutable diff --git a/src/pathsim/utils/adaptivebuffer.py b/src/pathsim/utils/adaptivebuffer.py index fe6c5755..5b37fa05 100644 --- a/src/pathsim/utils/adaptivebuffer.py +++ b/src/pathsim/utils/adaptivebuffer.py @@ -10,6 +10,8 @@ # IMPORTS ============================================================================== +import numpy as np + from collections import deque from bisect import bisect_left @@ -34,7 +36,7 @@ class AdaptiveBuffer: buffer_v : deque deque that collects the value data for buffering ns : int - savety for buffer truncation + safety for buffer truncation """ def __init__(self, delay): @@ -44,7 +46,7 @@ def __init__(self, delay): self.buffer_t = deque() self.buffer_v = deque() - #savety for buffer truncation + #safety for buffer truncation self.ns = 5 @@ -67,7 +69,7 @@ def add(self, t, value): self.buffer_t.append(t) self.buffer_v.append(value) - #remove values after savety from buffer -> enable interpolation + #remove values after safety from buffer -> enable interpolation if len(self.buffer_t) > self.ns: while t - self.buffer_t[self.ns] > self.delay: self.buffer_t.popleft() @@ -120,4 +122,45 @@ def get(self, t): def clear(self): """clear the buffer, reset everything""" self.buffer_t.clear() - self.buffer_v.clear() \ No newline at end of file + self.buffer_v.clear() + + + def to_checkpoint(self, prefix): + """Serialize buffer state for checkpointing. + + Parameters + ---------- + prefix : str + NPZ key prefix + + Returns + ------- + npz_data : dict + numpy arrays keyed by path + """ + npz_data = {} + if self.buffer_t: + npz_data[f"{prefix}/buffer_t"] = np.array(list(self.buffer_t)) + npz_data[f"{prefix}/buffer_v"] = np.array(list(self.buffer_v)) + return npz_data + + + def load_checkpoint(self, npz, prefix): + """Restore buffer state from checkpoint. + + Parameters + ---------- + npz : dict-like + numpy arrays from checkpoint NPZ + prefix : str + NPZ key prefix + """ + self.clear() + t_key = f"{prefix}/buffer_t" + v_key = f"{prefix}/buffer_v" + if t_key in npz and v_key in npz: + times = npz[t_key] + values = npz[v_key] + for t, v in zip(times, values): + self.buffer_t.append(float(t)) + self.buffer_v.append(v) diff --git a/src/pathsim/utils/diagnostics.py b/src/pathsim/utils/diagnostics.py new file mode 100644 index 00000000..b58c86da --- /dev/null +++ b/src/pathsim/utils/diagnostics.py @@ -0,0 +1,244 @@ +######################################################################################### +## +## CONVERGENCE TRACKING AND DIAGNOSTICS +## (utils/diagnostics.py) +## +## Convergence tracker classes for the simulation solver loops +## and optional per-timestep diagnostics snapshot. +## +######################################################################################### + +# IMPORTS =============================================================================== + +from dataclasses import dataclass, field + + +# CONVERGENCE TRACKER =================================================================== + +class ConvergenceTracker: + """Tracks per-object scalar errors and convergence for fixed-point loops. + + Used by the algebraic loop solver (keyed by ConnectionBooster) and + the implicit ODE solver (keyed by Block). + + Attributes + ---------- + errors : dict + object -> float, per-object error from most recent iteration + max_error : float + maximum error across all objects in current iteration + iterations : int + number of iterations taken + """ + + __slots__ = ('errors', 'max_error', 'iterations') + + def __init__(self): + self.errors = {} + self.max_error = 0.0 + self.iterations = 0 + + + def reset(self): + """Clear all state.""" + self.errors.clear() + self.max_error = 0.0 + self.iterations = 0 + + + def begin_iteration(self): + """Reset per-iteration state before sweeping objects.""" + self.errors.clear() + self.max_error = 0.0 + + + def record(self, obj, error): + """Record a single object's error and update the running max.""" + self.errors[obj] = error + if error > self.max_error: + self.max_error = error + + + def converged(self, tolerance): + """Check if max error is within tolerance.""" + return self.max_error <= tolerance + + + def details(self, label_fn): + """Format per-object error breakdown for error messages. + + Parameters + ---------- + label_fn : callable + obj -> str, produces a human-readable label + + Returns + ------- + list[str] + formatted lines like " Integrator: 1.23e-04" + """ + return [f" {label_fn(obj)}: {err:.2e}" for obj, err in self.errors.items()] + + +# STEP TRACKER ========================================================================== + +class StepTracker: + """Tracks per-block adaptive step results. + + Used by the adaptive error control loop. Each block produces a tuple + (success, err_norm, scale) and this tracker aggregates them. + + Attributes + ---------- + errors : dict + block -> (success, err_norm, scale) from most recent step + success : bool + AND of all block successes + max_error : float + maximum error norm across all blocks + min_scale : float | None + minimum scale factor (None if no block provides one) + """ + + __slots__ = ('errors', 'success', 'max_error', 'min_scale') + + def __init__(self): + self.errors = {} + self.success = True + self.max_error = 0.0 + self.min_scale = None + + + def reset(self): + """Clear state for a new step.""" + self.errors.clear() + self.success = True + self.max_error = 0.0 + self.min_scale = None + + + def record(self, block, success, err_norm, scale): + """Record a single block's step result.""" + self.errors[block] = (success, err_norm, scale) + if not success: + self.success = False + if err_norm > self.max_error: + self.max_error = err_norm + if scale is not None: + if self.min_scale is None or scale < self.min_scale: + self.min_scale = scale + + + @property + def scale(self): + """Effective scale factor (1.0 when no block provides one).""" + return self.min_scale if self.min_scale is not None else 1.0 + + +# DIAGNOSTICS SNAPSHOT ================================================================== + +@dataclass +class Diagnostics: + """Per-timestep convergence diagnostics snapshot. + + Populated by the simulation engine after each successful timestep + from the three convergence trackers. Provides read-only accessors + for the worst offending block or connection. + + Attributes + ---------- + time : float + simulation time + loop_residuals : dict + per-booster algebraic loop residuals (booster -> residual) + loop_iterations : int + number of algebraic loop iterations taken + solve_residuals : dict + per-block implicit solver residuals (block -> residual) + solve_iterations : int + number of implicit solver iterations taken + step_errors : dict + per-block adaptive step data (block -> (success, err_norm, scale)) + """ + time: float = 0.0 + loop_residuals: dict = field(default_factory=dict) + loop_iterations: int = 0 + solve_residuals: dict = field(default_factory=dict) + solve_iterations: int = 0 + step_errors: dict = field(default_factory=dict) + + + @staticmethod + def _label(obj): + """Human-readable label for a block or booster.""" + if hasattr(obj, 'connection'): + return str(obj.connection) + return obj.__class__.__name__ + + + def worst_block(self): + """Block with the highest residual across solve and step errors. + + Returns + ------- + tuple[str, float] or None + (label, error) or None if no data + """ + worst, worst_err = None, -1.0 + + for obj, err in self.solve_residuals.items(): + if err > worst_err: + worst, worst_err = obj, err + + for obj, (_, err_norm, _) in self.step_errors.items(): + if err_norm > worst_err: + worst, worst_err = obj, err_norm + + if worst is None: + return None + return self._label(worst), worst_err + + + def worst_booster(self): + """Connection booster with the highest algebraic loop residual. + + Returns + ------- + tuple[str, float] or None + (label, residual) or None if no data + """ + if not self.loop_residuals: + return None + + worst = max(self.loop_residuals, key=self.loop_residuals.get) + return self._label(worst), self.loop_residuals[worst] + + + def summary(self): + """Formatted summary of this diagnostics snapshot. + + Returns + ------- + str + human-readable diagnostics summary + """ + lines = [f"Diagnostics at t = {self.time:.6f}"] + + if self.step_errors: + lines.append(f"\n Adaptive step errors:") + for obj, (suc, err, scl) in self.step_errors.items(): + status = "OK" if suc else "FAIL" + scl_str = f"{scl:.3f}" if scl is not None else "N/A" + lines.append(f" {status} {self._label(obj)}: err={err:.2e}, scale={scl_str}") + + if self.solve_residuals: + lines.append(f"\n Implicit solver residuals ({self.solve_iterations} iterations):") + for obj, err in self.solve_residuals.items(): + lines.append(f" {self._label(obj)}: {err:.2e}") + + if self.loop_residuals: + lines.append(f"\n Algebraic loop residuals ({self.loop_iterations} iterations):") + for obj, err in self.loop_residuals.items(): + lines.append(f" {self._label(obj)}: {err:.2e}") + + return "\n".join(lines) diff --git a/src/pathsim/utils/gilbert.py b/src/pathsim/utils/gilbert.py index b2cd9bd3..18df641e 100644 --- a/src/pathsim/utils/gilbert.py +++ b/src/pathsim/utils/gilbert.py @@ -23,13 +23,13 @@ def gilbert_realization(Poles=[], Residues=[], Const=0.0, tolerance=1e-9): .. math:: - \\mathbf{H}(s) = \\mathmf{D} + \\sum_{n=1}^N \\frac{\\mathbf{R}_n}{s - p_n} ) + \\mathbf{H}(s) = \\mathbf{D} + \\sum_{n=1}^N \\frac{\\mathbf{R}_n}{s - p_n} statespace form: .. math:: - \\mathbf{H}(s) = \\mathbf{C} (s \\mathbf{I} - \\mathbf{A})^{-1} * \\mathbf{B} + \\mathbf{H} + \\mathbf{H}(s) = \\mathbf{C} (s \\mathbf{I} - \\mathbf{A})^{-1} \\mathbf{B} + \\mathbf{D} Notes ----- diff --git a/src/pathsim/utils/mutable.py b/src/pathsim/utils/mutable.py new file mode 100644 index 00000000..97888908 --- /dev/null +++ b/src/pathsim/utils/mutable.py @@ -0,0 +1,177 @@ +######################################################################################### +## +## MUTABLE PARAMETER DECORATOR +## (utils/mutable.py) +## +## Class decorator that enables runtime parameter mutation with automatic +## reinitialization. When a decorated parameter is changed, the block's +## __init__ is re-run with updated values while preserving engine state. +## +######################################################################################### + +# IMPORTS =============================================================================== + +import inspect +import functools + +import numpy as np + + +# REINIT HELPER ========================================================================= + +def _do_reinit(block): + """Re-run __init__ with current parameter values, preserving engine state. + + Uses ``type(block).__init__`` to always reinit from the most derived class, + ensuring that subclass overrides (e.g. operator replacements) are preserved. + + Parameters + ---------- + block : Block + the block instance to reinitialize + """ + + actual_cls = type(block) + + # gather current values for ALL init params of the actual class + sig = inspect.signature(actual_cls.__init__) + kwargs = {} + for name in sig.parameters: + if name == "self": + continue + if hasattr(block, name): + kwargs[name] = getattr(block, name) + + # save engine + engine = block.engine if hasattr(block, 'engine') else None + + # re-run init through the wrapped __init__ (handles depth counting) + block._param_locked = False + actual_cls.__init__(block, **kwargs) + # _param_locked is set to True by the outermost new_init wrapper + + # restore engine + if engine is not None: + old_dim = len(engine) + new_dim = len(np.atleast_1d(block.initial_value)) if hasattr(block, 'initial_value') else 0 + + if old_dim == new_dim: + # same dimension - restore the entire engine + block.engine = engine + else: + # dimension changed - create new engine inheriting settings + block.engine = type(engine).create( + block.initial_value, + parent=engine.parent, + ) + block.engine.tolerance_lte_abs = engine.tolerance_lte_abs + block.engine.tolerance_lte_rel = engine.tolerance_lte_rel + + +# DECORATOR ============================================================================= + +def mutable(cls): + """Class decorator that makes all ``__init__`` parameters trigger automatic + reinitialization when changed at runtime. + + Parameters are auto-detected from the ``__init__`` signature. When any parameter + is changed at runtime, the block's ``__init__`` is re-executed with updated values. + The integration engine state is preserved across reinitialization. + + A ``set(**kwargs)`` method is also generated for batched parameter updates that + triggers only a single reinitialization. + + Supports inheritance: if both a parent and child class use ``@mutable``, the init + guard uses a depth counter to ensure reinitialization only triggers after the + outermost ``__init__`` completes. + + Example + ------- + .. code-block:: python + + @mutable + class PT1(StateSpace): + def __init__(self, K=1.0, T=1.0): + self.K = K + self.T = T + super().__init__( + A=np.array([[-1.0 / T]]), + B=np.array([[K / T]]), + C=np.array([[1.0]]), + D=np.array([[0.0]]) + ) + + pt1 = PT1(K=2.0, T=0.5) + pt1.K = 5.0 # auto reinitializes + pt1.set(K=5.0, T=0.3) # single reinitialization + """ + + original_init = cls.__init__ + + # auto-detect all __init__ parameters + params = [ + name for name in inspect.signature(original_init).parameters + if name != "self" + ] + + # -- install property descriptors for all params ------------------------------- + + for name in params: + storage = f"_p_{name}" + + def _make_property(s): + def getter(self): + return getattr(self, s) + + def setter(self, value): + setattr(self, s, value) + if getattr(self, '_param_locked', False): + _do_reinit(self) + + return property(getter, setter) + + setattr(cls, name, _make_property(storage)) + + # -- wrap __init__ with depth counter ------------------------------------------ + + @functools.wraps(original_init) + def new_init(self, *args, **kwargs): + self._init_depth = getattr(self, '_init_depth', 0) + 1 + try: + original_init(self, *args, **kwargs) + finally: + self._init_depth -= 1 + if self._init_depth == 0: + self._param_locked = True + + cls.__init__ = new_init + + # -- generate batched set() method --------------------------------------------- + + def set(self, **kwargs): + """Set multiple parameters and reinitialize once. + + Parameters + ---------- + kwargs : dict + parameter names and their new values + + Example + ------- + .. code-block:: python + + block.set(K=5.0, T=0.3) + """ + self._param_locked = False + for key, value in kwargs.items(): + setattr(self, key, value) + _do_reinit(self) + + cls.set = set + + # -- store metadata for introspection ------------------------------------------ + + existing = getattr(cls, '_mutable_params', ()) + cls._mutable_params = existing + tuple(p for p in params if p not in existing) + + return cls diff --git a/src/pathsim/utils/portreference.py b/src/pathsim/utils/portreference.py index 38d6adab..8224b93e 100644 --- a/src/pathsim/utils/portreference.py +++ b/src/pathsim/utils/portreference.py @@ -80,7 +80,7 @@ def _get_input_indices(self): self.block.inputs._map(p) for p in self.ports ], dtype=np.intp) - # Resize register to accomodate indices + # Resize register to accommodate indices max_idx = self._input_indices.max() self.block.inputs.resize(max_idx + 1) @@ -98,7 +98,7 @@ def _get_output_indices(self): self.block.outputs._map(p) for p in self.ports ], dtype=np.intp) - # Resize register to accomodate indices + # Resize register to accommodate indices max_idx = self._output_indices.max() self.block.outputs.resize(max_idx + 1) diff --git a/tests/evals/conftest.py b/tests/evals/conftest.py new file mode 100644 index 00000000..4b9777e1 --- /dev/null +++ b/tests/evals/conftest.py @@ -0,0 +1,7 @@ +import pytest + +def pytest_collection_modifyitems(items): + """Auto-mark all tests in the evals directory as slow.""" + for item in items: + if "/evals/" in item.nodeid or "\\evals\\" in item.nodeid: + item.add_marker(pytest.mark.slow) diff --git a/tests/evals/test_counter_comparator_system.py b/tests/evals/test_counter_comparator_system.py new file mode 100644 index 00000000..3d729f46 --- /dev/null +++ b/tests/evals/test_counter_comparator_system.py @@ -0,0 +1,246 @@ +######################################################################################## +## +## Testing counter and comparator systems +## +## Verifies event-driven digital-like blocks (Counter, CounterUp, CounterDown, +## Comparator) correctly detect threshold crossings in simulation. +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim import Simulation, Connection +from pathsim.blocks import ( + SinusoidalSource, + Source, + Counter, + CounterUp, + CounterDown, + Comparator, + Scope + ) + +from pathsim.solvers import RKCK54, RKDP87 + + +# TESTCASE ============================================================================= + +class TestCounterSystem(unittest.TestCase): + """ + Test counter blocks counting zero crossings of a sinusoidal signal. + + A sine wave of frequency f crosses zero 2*f times per second. + CounterUp counts only rising crossings (f per second). + CounterDown counts only falling crossings (f per second). + Counter counts both (2*f per second). + """ + + def setUp(self): + + self.freq = 2.0 # Hz + self.duration = 5.0 + + Src = SinusoidalSource(amplitude=1.0, frequency=self.freq) + + self.Cnt = Counter(threshold=0.0) + self.CntUp = CounterUp(threshold=0.0) + self.CntDown = CounterDown(threshold=0.0) + + self.Sco = Scope(labels=["signal", "count", "count_up", "count_down"]) + + blocks = [Src, self.Cnt, self.CntUp, self.CntDown, self.Sco] + + connections = [ + Connection(Src, self.Cnt, self.CntUp, self.CntDown, self.Sco[0]), + Connection(self.Cnt, self.Sco[1]), + Connection(self.CntUp, self.Sco[2]), + Connection(self.CntDown, self.Sco[3]) + ] + + self.Sim = Simulation( + blocks, + connections, + dt=0.001, + log=False + ) + + + def test_counter_total_crossings(self): + """Counter should count all zero crossings""" + + self.Sim.run(duration=self.duration, reset=True) + + time, [sig, cnt, cnt_up, cnt_down] = self.Sco.read() + + #expected total crossings: 2 * freq * duration + expected_total = int(2 * self.freq * self.duration) + + #allow +-1 tolerance for boundary effects + self.assertAlmostEqual(cnt[-1], expected_total, delta=2, + msg=f"Total crossings: {cnt[-1]}, expected ~{expected_total}") + + + def test_counter_up_counts_rising(self): + """CounterUp should count only rising crossings""" + + self.Sim.run(duration=self.duration, reset=True) + + time, [sig, cnt, cnt_up, cnt_down] = self.Sco.read() + + #expected rising crossings: freq * duration + expected_up = int(self.freq * self.duration) + + self.assertAlmostEqual(cnt_up[-1], expected_up, delta=2, + msg=f"Rising crossings: {cnt_up[-1]}, expected ~{expected_up}") + + + def test_counter_down_counts_falling(self): + """CounterDown should count only falling crossings""" + + self.Sim.run(duration=self.duration, reset=True) + + time, [sig, cnt, cnt_up, cnt_down] = self.Sco.read() + + #expected falling crossings: freq * duration + expected_down = int(self.freq * self.duration) + + self.assertAlmostEqual(cnt_down[-1], expected_down, delta=2, + msg=f"Falling crossings: {cnt_down[-1]}, expected ~{expected_down}") + + + def test_counter_with_adaptive_solver(self): + """CounterUp works with adaptive solvers (short run to avoid slowness)""" + + #separate minimal setup - single counter, short duration + Src = SinusoidalSource(amplitude=1.0, frequency=1.0) + Cnt = CounterUp(threshold=0.0) + Sco = Scope(labels=["signal", "count"]) + + Sim = Simulation( + blocks=[Src, Cnt, Sco], + connections=[ + Connection(Src, Cnt, Sco[0]), + Connection(Cnt, Sco[1]) + ], + Solver=RKCK54, + tolerance_lte_abs=1e-4, + log=False + ) + + Sim.run(duration=2.0, reset=True) + + time, [sig, cnt] = Sco.read() + + #1 Hz signal, 2 seconds -> ~2 rising crossings + self.assertAlmostEqual(cnt[-1], 2, delta=1) + + +class TestComparatorSystem(unittest.TestCase): + """ + Test comparator producing a square wave from a sinusoidal input. + + A sine wave through a zero-threshold comparator should produce + a square wave with the same frequency. + """ + + def setUp(self): + + self.freq = 1.0 + + Src = SinusoidalSource(amplitude=1.0, frequency=self.freq) + self.Cmp = Comparator(threshold=0.0, span=[-1, 1]) + self.Sco = Scope(labels=["input", "comparator"]) + + blocks = [Src, self.Cmp, self.Sco] + + connections = [ + Connection(Src, self.Cmp, self.Sco[0]), + Connection(self.Cmp, self.Sco[1]) + ] + + self.Sim = Simulation( + blocks, + connections, + dt=0.001, + log=False + ) + + + def test_comparator_output_values(self): + """Comparator output should only be +1 or -1""" + + self.Sim.run(duration=5.0, reset=True) + + time, [inp, cmp] = self.Sco.read() + + #after initial settling (t>0.1), check output values + mask = time > 0.1 + cmp_steady = cmp[mask] + + unique_values = np.unique(np.round(cmp_steady, 1)) + + #should only contain -1 and +1 (within tolerance) + self.assertTrue(np.any(cmp_steady > 0.5), "Should have positive output") + self.assertTrue(np.any(cmp_steady < -0.5), "Should have negative output") + + + def test_comparator_frequency_preserved(self): + """Comparator output should have same switching frequency as input""" + + self.Sim.run(duration=10.0, reset=True) + + time, [inp, cmp] = self.Sco.read() + + #count zero crossings of comparator output (transitions) + mask = time > 0.5 + cmp_steady = cmp[mask] + t_steady = time[mask] + + crossings = np.sum(np.abs(np.diff(np.sign(cmp_steady))) > 0) + duration = t_steady[-1] - t_steady[0] + + #crossings per second should be ~2*freq (once up, once down) + crossing_rate = crossings / duration + expected_rate = 2 * self.freq + + self.assertAlmostEqual(crossing_rate, expected_rate, delta=1.0, + msg=f"Crossing rate: {crossing_rate:.1f}, expected ~{expected_rate}") + + +class TestCounterWithCustomThreshold(unittest.TestCase): + """Test counter with non-zero threshold""" + + def test_threshold_crossing_count(self): + """Count crossings of a ramp through a specified threshold""" + + #sawtooth-like source that crosses threshold multiple times + Src = SinusoidalSource(amplitude=2.0, frequency=1.0) + Cnt = CounterUp(threshold=1.0) # count when signal rises through 1.0 + Sco = Scope(labels=["signal", "count"]) + + Sim = Simulation( + blocks=[Src, Cnt, Sco], + connections=[ + Connection(Src, Cnt, Sco[0]), + Connection(Cnt, Sco[1]) + ], + dt=0.001, + log=False + ) + + Sim.run(duration=5.0, reset=True) + + time, [sig, cnt] = Sco.read() + + #signal of amplitude 2, freq 1 crosses threshold 1.0 upward once per cycle + expected = int(1.0 * 5.0) + self.assertAlmostEqual(cnt[-1], expected, delta=2) + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/evals/test_dynamical_system_ivp.py b/tests/evals/test_dynamical_system_ivp.py new file mode 100644 index 00000000..81df04ce --- /dev/null +++ b/tests/evals/test_dynamical_system_ivp.py @@ -0,0 +1,247 @@ +######################################################################################## +## +## Testing DynamicalSystem block in simulation +## +## Verifies the general nonlinear state-space block (DynamicalSystem) produces +## correct results for known analytical solutions when embedded in a simulation. +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim import Simulation, Connection +from pathsim.blocks import Source, Adder, Amplifier, Scope, DynamicalSystem, ODE + +from pathsim.solvers import ( + RKBS32, RKCK54, RKDP54, RKV65, RKDP87, + ESDIRK32, ESDIRK43 + ) + + +# TESTCASE ============================================================================= + +class TestDynamicalSystemDecay(unittest.TestCase): + """ + Test DynamicalSystem block with first-order linear decay. + + System: dx/dt = -a*x, y = x, x(0) = x0 + Analytical: x(t) = x0 * exp(-a*t) + """ + + def setUp(self): + + self.a = 2.0 + self.x0 = 3.0 + + self.DS = DynamicalSystem( + func_dyn=lambda x, u, t: -self.a * x, + func_alg=lambda x, u, t: x, + initial_value=self.x0 + ) + + self.Sco = Scope(labels=["state"]) + + self.Sim = Simulation( + blocks=[self.DS, self.Sco], + connections=[Connection(self.DS, self.Sco)], + log=False + ) + + + def _reference(self, t): + return self.x0 * np.exp(-self.a * t) + + + def test_eval_explicit_solvers(self): + + for SOL in [RKBS32, RKCK54, RKV65, RKDP87]: + + for tol in [1e-4, 1e-6, 1e-8]: + + with self.subTest(SOL=str(SOL), tol=tol): + + self.Sim.reset() + self.Sim._set_solver(SOL, tolerance_lte_abs=tol, tolerance_lte_rel=0.0) + self.Sim.run(5) + + time, [res] = self.Sco.read() + ref = self._reference(time) + + self.assertAlmostEqual(np.max(abs(ref - res)), tol, 2) + + + def test_eval_implicit_solvers(self): + + for SOL in [ESDIRK32, ESDIRK43]: + + for tol in [1e-4, 1e-6]: + + with self.subTest(SOL=str(SOL), tol=tol): + + self.Sim.reset() + self.Sim._set_solver(SOL, tolerance_lte_abs=tol, tolerance_lte_rel=0.0) + self.Sim.run(5) + + time, [res] = self.Sco.read() + ref = self._reference(time) + + self.assertAlmostEqual(np.max(abs(ref - res)), tol, 2) + + +class TestDynamicalSystemDriven(unittest.TestCase): + """ + Test DynamicalSystem with external forcing input. + + System: dx/dt = -x + u, y = 2*x, u = 1 (step input) + Analytical: x(t) = 1 - exp(-t), y(t) = 2*(1 - exp(-t)) + """ + + def setUp(self): + + Src = Source(lambda t: 1.0) + + self.DS = DynamicalSystem( + func_dyn=lambda x, u, t: -x + u, + func_alg=lambda x, u, t: 2 * x, + initial_value=0.0 + ) + + self.Sco = Scope(labels=["output"]) + + self.Sim = Simulation( + blocks=[Src, self.DS, self.Sco], + connections=[ + Connection(Src, self.DS), + Connection(self.DS, self.Sco) + ], + log=False + ) + + + def test_step_response(self): + """Verify step response matches analytical solution""" + + for SOL in [RKCK54, RKDP87]: + + with self.subTest(SOL=str(SOL)): + + self.Sim.reset() + self.Sim._set_solver(SOL, tolerance_lte_abs=1e-6, tolerance_lte_rel=0.0) + self.Sim.run(8) + + time, [res] = self.Sco.read() + ref = 2.0 * (1.0 - np.exp(-time)) + + error = np.max(np.abs(ref - res)) + self.assertLess(error, 1e-4, + f"Step response error: {error:.2e}") + + +class TestODEBlockInSimulation(unittest.TestCase): + """ + Test the ODE block in a feedback simulation. + + System: dx/dt = -x + u, y = x, with u from a source + This is similar to an integrator with feedback, but using the + general ODE block. + """ + + def setUp(self): + + Src = Source(lambda t: np.sin(t)) + + self.Ode = ODE( + func=lambda x, u, t: -x + u, + initial_value=0.0 + ) + + self.Sco = Scope(labels=["ode_output", "source"]) + + self.Sim = Simulation( + blocks=[Src, self.Ode, self.Sco], + connections=[ + Connection(Src, self.Ode, self.Sco[1]), + Connection(self.Ode, self.Sco[0]) + ], + log=False + ) + + + def test_sinusoidal_response(self): + """Test ODE response to sinusoidal input""" + + self.Sim._set_solver(RKCK54, tolerance_lte_abs=1e-8, tolerance_lte_rel=0.0) + self.Sim.run(duration=20, reset=True) + + time, [ode_out, src_out] = self.Sco.read() + + #analytical solution for dx/dt = -x + sin(t), x(0) = 0: + # x(t) = 0.5*(sin(t) - cos(t)) + 0.5*exp(-t) + ref = 0.5 * (np.sin(time) - np.cos(time)) + 0.5 * np.exp(-time) + + error = np.max(np.abs(ref - ode_out)) + self.assertLess(error, 1e-5, + f"ODE sinusoidal response error: {error:.2e}") + + +class TestDynamicalSystemFeedback(unittest.TestCase): + """ + Test DynamicalSystem in a feedback loop with an integrator. + + This tests a more complex topology where DynamicalSystem interacts + with other blocks through connections. + """ + + def test_coupled_system(self): + """Two coupled first-order systems""" + + #system 1: dx1/dt = -x1 + u1, y1 = x1 + DS1 = DynamicalSystem( + func_dyn=lambda x, u, t: -x + u, + func_alg=lambda x, u, t: x, + initial_value=1.0 + ) + + #system 2: dx2/dt = -2*x2 + u2, y2 = x2 + DS2 = DynamicalSystem( + func_dyn=lambda x, u, t: -2*x + u, + func_alg=lambda x, u, t: x, + initial_value=0.0 + ) + + Sco = Scope(labels=["x1", "x2"]) + + #DS1 output feeds DS2, DS2 output feeds DS1 + Sim = Simulation( + blocks=[DS1, DS2, Sco], + connections=[ + Connection(DS1, DS2, Sco[0]), + Connection(DS2, DS1, Sco[1]) + ], + Solver=RKCK54, + tolerance_lte_abs=1e-8, + log=False + ) + + Sim.run(duration=10, reset=True) + + time, [x1, x2] = Sco.read() + + #both states should decay toward 0 (coupled decay) + self.assertAlmostEqual(x1[-1], 0.0, 1) + self.assertAlmostEqual(x2[-1], 0.0, 1) + + #x1 should start at 1.0 + self.assertAlmostEqual(x1[0], 1.0, 4) + + #x2 should start at 0.0 + self.assertAlmostEqual(x2[0], 0.0, 4) + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/evals/test_logic_system.py b/tests/evals/test_logic_system.py new file mode 100644 index 00000000..9d6f56cf --- /dev/null +++ b/tests/evals/test_logic_system.py @@ -0,0 +1,265 @@ +######################################################################################## +## +## Testing logic and comparison block systems +## +## Verifies comparison (GreaterThan, LessThan, Equal) and boolean logic +## (LogicAnd, LogicOr, LogicNot) blocks in full simulation context. +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim import Simulation, Connection +from pathsim.blocks import ( + Source, + Constant, + Scope, + ) + +from pathsim.blocks.logic import ( + GreaterThan, + LessThan, + Equal, + LogicAnd, + LogicOr, + LogicNot, + ) + + +# TESTCASE ============================================================================= + +class TestComparisonSystem(unittest.TestCase): + """ + Test comparison blocks in a simulation that compares a sine wave + against a constant threshold. + + System: Source(sin(t)) → GT/LT/EQ → Scope + Constant(0) ↗ + + Verify: GT outputs 1 when sin(t) > 0, LT outputs 1 when sin(t) < 0 + """ + + def setUp(self): + + Src = Source(lambda t: np.sin(2 * np.pi * t)) + Thr = Constant(0.0) + + self.GT = GreaterThan() + self.LT = LessThan() + + self.Sco = Scope(labels=["signal", "gt_zero", "lt_zero"]) + + blocks = [Src, Thr, self.GT, self.LT, self.Sco] + + connections = [ + Connection(Src, self.GT["a"], self.LT["a"], self.Sco[0]), + Connection(Thr, self.GT["b"], self.LT["b"]), + Connection(self.GT, self.Sco[1]), + Connection(self.LT, self.Sco[2]), + ] + + self.Sim = Simulation( + blocks, + connections, + dt=0.01, + log=False + ) + + + def test_gt_lt_complementary(self): + """GT and LT should be complementary (sum to 1) away from zero crossings""" + + self.Sim.run(duration=3.0, reset=True) + + time, [sig, gt, lt] = self.Sco.read() + + #away from zero crossings, GT + LT should be 1 (exactly one is true) + mask = np.abs(sig) > 0.1 + result = gt[mask] + lt[mask] + + self.assertTrue(np.allclose(result, 1.0), + "GT and LT should be complementary away from zero crossings") + + + def test_gt_matches_positive(self): + """GT output should be 1 when signal is clearly positive""" + + self.Sim.run(duration=3.0, reset=True) + + time, [sig, gt, lt] = self.Sco.read() + + mask_pos = sig > 0.2 + self.assertTrue(np.all(gt[mask_pos] == 1.0), + "GT should be 1 when signal is positive") + + mask_neg = sig < -0.2 + self.assertTrue(np.all(gt[mask_neg] == 0.0), + "GT should be 0 when signal is negative") + + +class TestLogicGateSystem(unittest.TestCase): + """ + Test logic gates combining two comparison outputs. + + System: Two sine waves at different frequencies compared against 0, + then combined with AND/OR/NOT. + + Verify: Logic truth tables hold across the simulation. + """ + + def setUp(self): + + #two signals with different frequencies so they go in and out of phase + Src1 = Source(lambda t: np.sin(2 * np.pi * 1.0 * t)) + Src2 = Source(lambda t: np.sin(2 * np.pi * 1.5 * t)) + Zero = Constant(0.0) + + GT1 = GreaterThan() + GT2 = GreaterThan() + + self.AND = LogicAnd() + self.OR = LogicOr() + self.NOT = LogicNot() + + self.Sco = Scope(labels=["gt1", "gt2", "and", "or", "not1"]) + + blocks = [Src1, Src2, Zero, GT1, GT2, + self.AND, self.OR, self.NOT, self.Sco] + + connections = [ + Connection(Src1, GT1["a"]), + Connection(Src2, GT2["a"]), + Connection(Zero, GT1["b"], GT2["b"]), + Connection(GT1, self.AND["a"], self.OR["a"], self.NOT, self.Sco[0]), + Connection(GT2, self.AND["b"], self.OR["b"], self.Sco[1]), + Connection(self.AND, self.Sco[2]), + Connection(self.OR, self.Sco[3]), + Connection(self.NOT, self.Sco[4]), + ] + + self.Sim = Simulation( + blocks, + connections, + dt=0.01, + log=False + ) + + + def test_and_gate(self): + """AND should only be 1 when both inputs are 1""" + + self.Sim.run(duration=5.0, reset=True) + + time, [gt1, gt2, and_out, or_out, not_out] = self.Sco.read() + + #where both are 1, AND should be 1 + both_true = (gt1 == 1.0) & (gt2 == 1.0) + if np.any(both_true): + self.assertTrue(np.all(and_out[both_true] == 1.0)) + + #where either is 0, AND should be 0 + either_false = (gt1 == 0.0) | (gt2 == 0.0) + if np.any(either_false): + self.assertTrue(np.all(and_out[either_false] == 0.0)) + + + def test_or_gate(self): + """OR should be 1 when either input is 1""" + + self.Sim.run(duration=5.0, reset=True) + + time, [gt1, gt2, and_out, or_out, not_out] = self.Sco.read() + + #where both are 0, OR should be 0 + both_false = (gt1 == 0.0) & (gt2 == 0.0) + if np.any(both_false): + self.assertTrue(np.all(or_out[both_false] == 0.0)) + + #where either is 1, OR should be 1 + either_true = (gt1 == 1.0) | (gt2 == 1.0) + if np.any(either_true): + self.assertTrue(np.all(or_out[either_true] == 1.0)) + + + def test_not_gate(self): + """NOT should invert its input""" + + self.Sim.run(duration=5.0, reset=True) + + time, [gt1, gt2, and_out, or_out, not_out] = self.Sco.read() + + #NOT should be inverse of GT1 + self.assertTrue(np.allclose(not_out + gt1, 1.0), + "NOT should invert its input") + + +class TestEqualSystem(unittest.TestCase): + """ + Test Equal block detecting when two signals are close. + + System: Source(sin(t)) → Equal ← Source(sin(t + small_offset)) + """ + + def test_equal_detects_match(self): + """Equal should output 1 when signals match within tolerance""" + + Src1 = Constant(3.14) + Src2 = Constant(3.14) + + Eq = Equal(tolerance=0.01) + Sco = Scope() + + Sim = Simulation( + blocks=[Src1, Src2, Eq, Sco], + connections=[ + Connection(Src1, Eq["a"]), + Connection(Src2, Eq["b"]), + Connection(Eq, Sco), + ], + dt=0.1, + log=False + ) + + Sim.run(duration=1.0, reset=True) + + time, [eq_out] = Sco.read() + + self.assertTrue(np.all(eq_out == 1.0), + "Equal should output 1 for identical signals") + + + def test_equal_detects_mismatch(self): + """Equal should output 0 when signals differ""" + + Src1 = Constant(1.0) + Src2 = Constant(2.0) + + Eq = Equal(tolerance=0.01) + Sco = Scope() + + Sim = Simulation( + blocks=[Src1, Src2, Eq, Sco], + connections=[ + Connection(Src1, Eq["a"]), + Connection(Src2, Eq["b"]), + Connection(Eq, Sco), + ], + dt=0.1, + log=False + ) + + Sim.run(duration=1.0, reset=True) + + time, [eq_out] = Sco.read() + + self.assertTrue(np.all(eq_out == 0.0), + "Equal should output 0 for different signals") + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/evals/test_relay_thermostat_system.py b/tests/evals/test_relay_thermostat_system.py new file mode 100644 index 00000000..ccef167a --- /dev/null +++ b/tests/evals/test_relay_thermostat_system.py @@ -0,0 +1,151 @@ +######################################################################################## +## +## Testing relay-controlled thermostat system +## +## Thermal plant with relay hysteresis controller. Verifies event-driven +## switching behavior produces correct temperature oscillation pattern. +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim import Simulation, Connection +from pathsim.blocks import Integrator, Amplifier, Adder, Constant, Relay, Scope + +from pathsim.solvers import ( + RKBS32, RKCK54, RKDP54, RKV65, RKDP87, + ESDIRK32, ESDIRK43, ESDIRK54 + ) + + +# TESTCASE ============================================================================= + +class TestRelayThermostatSystem(unittest.TestCase): + """ + Thermostat system: relay controller with hysteresis driving a first-order + thermal plant. + + System: + heater = Relay(threshold_up=22, threshold_down=18, value_up=0, value_down=50) + dT/dt = -alpha*(T - T_ambient) + heater_output / C + + When temperature rises above 22 -> heater OFF (value_up=0) + When temperature drops below 18 -> heater ON (value_down=50) + + The system should oscillate between the two thresholds. + """ + + def setUp(self): + + #thermal parameters + self.alpha = 0.5 # heat loss coefficient + self.T_amb = 10.0 # ambient temperature + self.C = 5.0 # thermal capacity + + #initial temperature (between thresholds) + self.T0 = 20.0 + + #blocks + self.Int = Integrator(self.T0) # temperature state + Amp = Amplifier(-self.alpha) # heat loss: -alpha * T + Amb = Constant(self.alpha * self.T_amb) # ambient contribution: alpha * T_amb + Htr = Amplifier(1.0 / self.C) # heater gain: heater / C + Add = Adder() # sum: -alpha*T + alpha*T_amb + heater/C + + self.Rly = Relay( + threshold_up=22.0, + threshold_down=18.0, + value_up=0.0, # heater off when T > 22 + value_down=50.0 # heater on when T < 18 + ) + + self.Sco = Scope(labels=["temperature", "heater"]) + + blocks = [self.Int, Amp, Amb, Htr, Add, self.Rly, self.Sco] + + #connections: T -> Amp, Amp -> Add[0], Amb -> Add[1], Rly -> Htr -> Add[2], Add -> Int + connections = [ + Connection(self.Int, Amp, self.Rly, self.Sco[0]), + Connection(Amp, Add[0]), + Connection(Amb, Add[1]), + Connection(self.Rly, Htr, self.Sco[1]), + Connection(Htr, Add[2]), + Connection(Add, self.Int) + ] + + self.Sim = Simulation( + blocks, + connections, + dt=0.01, + log=False + ) + + + def test_thermostat_oscillation(self): + """Test that temperature oscillates between thresholds""" + + self.Sim.run(duration=30, reset=True) + + time, [temp, heater] = self.Sco.read() + + #after initial transient (t>5), temperature should stay within bounds + mask = time > 5 + temp_steady = temp[mask] + + #temperature should oscillate within reasonable bounds around thresholds + self.assertTrue(np.min(temp_steady) > 16.0, + f"Temperature dropped too low: {np.min(temp_steady):.2f}") + self.assertTrue(np.max(temp_steady) < 24.0, + f"Temperature rose too high: {np.max(temp_steady):.2f}") + + #heater should have switched multiple times + heater_steady = heater[mask] + switches = np.sum(np.abs(np.diff(heater_steady)) > 1) + self.assertGreater(switches, 2, "Heater should have switched multiple times") + + + def test_thermostat_with_adaptive_solvers(self): + """Test thermostat with different adaptive solvers""" + + for SOL in [RKBS32, RKCK54, RKDP87]: + + with self.subTest(SOL=str(SOL)): + + self.Sim.reset() + self.Sim._set_solver(SOL, tolerance_lte_abs=1e-6) + self.Sim.run(duration=20, reset=True) + + time, [temp, _] = self.Sco.read() + + #temperature should stay bounded + mask = time > 5 + self.assertTrue(np.min(temp[mask]) > 16.0) + self.assertTrue(np.max(temp[mask]) < 24.0) + + + def test_thermostat_with_implicit_solvers(self): + """Test thermostat with implicit adaptive solvers""" + + for SOL in [ESDIRK32, ESDIRK43]: + + with self.subTest(SOL=str(SOL)): + + self.Sim.reset() + self.Sim._set_solver(SOL, tolerance_lte_abs=1e-6) + self.Sim.run(duration=20, reset=True) + + time, [temp, _] = self.Sco.read() + + #temperature should stay bounded + mask = time > 5 + self.assertTrue(np.min(temp[mask]) > 16.0) + self.assertTrue(np.max(temp[mask]) < 24.0) + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/evals/test_rescale_delay_system.py b/tests/evals/test_rescale_delay_system.py new file mode 100644 index 00000000..60c97be4 --- /dev/null +++ b/tests/evals/test_rescale_delay_system.py @@ -0,0 +1,277 @@ +######################################################################################## +## +## Testing Rescale, Atan2, Alias, and discrete Delay systems +## +## Verifies new math blocks and discrete delay mode in full simulation context. +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim import Simulation, Connection +from pathsim.blocks import ( + Source, + SinusoidalSource, + Constant, + Delay, + Scope, + ) + +from pathsim.blocks.math import Atan2, Rescale, Alias + + +# TESTCASE ============================================================================= + +class TestRescaleSystem(unittest.TestCase): + """ + Test Rescale block mapping a sine wave from [-1, 1] to [0, 10]. + + System: Source(sin(t)) → Rescale → Scope + Verify: output is linearly mapped to target range + """ + + def setUp(self): + + Src = SinusoidalSource(amplitude=1.0, frequency=1.0) + + self.Rsc = Rescale(i0=-1.0, i1=1.0, o0=0.0, o1=10.0) + self.Sco = Scope(labels=["input", "rescaled"]) + + blocks = [Src, self.Rsc, self.Sco] + + connections = [ + Connection(Src, self.Rsc, self.Sco[0]), + Connection(self.Rsc, self.Sco[1]), + ] + + self.Sim = Simulation( + blocks, + connections, + dt=0.01, + log=False + ) + + + def test_rescale_range(self): + """Output should be in [0, 10] for input in [-1, 1]""" + + self.Sim.run(duration=3.0, reset=True) + + time, [inp, rsc] = self.Sco.read() + + #check output stays within target range (with small tolerance) + self.assertTrue(np.all(rsc >= -0.1), "Rescaled output below lower bound") + self.assertTrue(np.all(rsc <= 10.1), "Rescaled output above upper bound") + + + def test_rescale_linearity(self): + """Output should be linear mapping of input""" + + self.Sim.run(duration=3.0, reset=True) + + time, [inp, rsc] = self.Sco.read() + + #expected: 5 + 5 * sin(t) + expected = 5.0 + 5.0 * inp + error = np.max(np.abs(rsc - expected)) + + self.assertLess(error, 0.01, f"Rescale linearity error: {error:.4f}") + + +class TestRescaleSaturationSystem(unittest.TestCase): + """ + Test Rescale with saturation enabled. + + System: Source(ramp) → Rescale(saturate=True) → Scope + Verify: output is clamped to target range + """ + + def test_saturation_clamps_output(self): + + #ramp from -2 to 2 over 4 seconds, mapped [0,1] -> [0,10] + Src = Source(lambda t: t - 2.0) + Rsc = Rescale(i0=0.0, i1=1.0, o0=0.0, o1=10.0, saturate=True) + Sco = Scope(labels=["input", "rescaled"]) + + Sim = Simulation( + blocks=[Src, Rsc, Sco], + connections=[ + Connection(Src, Rsc, Sco[0]), + Connection(Rsc, Sco[1]), + ], + dt=0.01, + log=False + ) + + Sim.run(duration=4.0, reset=True) + + time, [inp, rsc] = Sco.read() + + #output should never exceed [0, 10] + self.assertTrue(np.all(rsc >= -0.01), "Saturated output below 0") + self.assertTrue(np.all(rsc <= 10.01), "Saturated output above 10") + + #input in valid range [0, 1] should map normally + mask_valid = (inp >= 0.0) & (inp <= 1.0) + if np.any(mask_valid): + expected = 10.0 * inp[mask_valid] + error = np.max(np.abs(rsc[mask_valid] - expected)) + self.assertLess(error, 0.1) + + +class TestAtan2System(unittest.TestCase): + """ + Test Atan2 block computing the angle of a rotating vector. + + System: Source(sin(t)) → Atan2 ← Source(cos(t)) + Verify: output recovers the angle t (mod 2pi) + """ + + def setUp(self): + + self.SrcY = Source(lambda t: np.sin(t)) + self.SrcX = Source(lambda t: np.cos(t)) + + self.At2 = Atan2() + self.Sco = Scope(labels=["angle"]) + + blocks = [self.SrcY, self.SrcX, self.At2, self.Sco] + + connections = [ + Connection(self.SrcY, self.At2["a"]), + Connection(self.SrcX, self.At2["b"]), + Connection(self.At2, self.Sco), + ] + + self.Sim = Simulation( + blocks, + connections, + dt=0.01, + log=False + ) + + + def test_atan2_recovers_angle(self): + """atan2(sin(t), cos(t)) should equal t for t in [0, pi)""" + + self.Sim.run(duration=3.0, reset=True) + + time, [angle] = self.Sco.read() + + #check in first half period where atan2 is monotonic + mask = time < np.pi - 0.1 + expected = time[mask] + actual = angle[mask] + + error = np.max(np.abs(actual - expected)) + self.assertLess(error, 0.02, + f"Atan2 angle recovery error: {error:.4f}") + + +class TestAliasSystem(unittest.TestCase): + """ + Test Alias block as a transparent pass-through. + + System: Source(sin(t)) → Alias → Scope + Verify: output is identical to input + """ + + def test_alias_transparent(self): + + Src = SinusoidalSource(amplitude=1.0, frequency=2.0) + Als = Alias() + Sco = Scope(labels=["input", "alias"]) + + Sim = Simulation( + blocks=[Src, Als, Sco], + connections=[ + Connection(Src, Als, Sco[0]), + Connection(Als, Sco[1]), + ], + dt=0.01, + log=False + ) + + Sim.run(duration=2.0, reset=True) + + time, [inp, als] = Sco.read() + + self.assertTrue(np.allclose(inp, als), + "Alias output should be identical to input") + + +class TestDiscreteDelaySystem(unittest.TestCase): + """ + Test discrete-time delay using sampling_period parameter. + + System: Source(ramp) → Delay(tau, sampling_period) → Scope + Verify: output is a staircase-delayed version of input + """ + + def setUp(self): + + self.tau = 0.1 + self.T = 0.01 + + Src = Source(lambda t: t) + self.Dly = Delay(tau=self.tau, sampling_period=self.T) + self.Sco = Scope(labels=["input", "delayed"]) + + blocks = [Src, self.Dly, self.Sco] + + connections = [ + Connection(Src, self.Dly, self.Sco[0]), + Connection(self.Dly, self.Sco[1]), + ] + + self.Sim = Simulation( + blocks, + connections, + dt=0.001, + log=False + ) + + + def test_discrete_delay_offset(self): + """Delayed signal should trail input by approximately tau""" + + self.Sim.run(duration=1.0, reset=True) + + time, [inp, delayed] = self.Sco.read() + + #after initial fill (t > tau + settling), check delay offset + mask = time > self.tau + 0.2 + t_check = time[mask] + delayed_check = delayed[mask] + + #the delayed ramp should be approximately (t - tau) + #with staircase quantization from sampling + expected = t_check - self.tau + error = np.mean(np.abs(delayed_check - expected)) + + self.assertLess(error, self.T + 0.01, + f"Discrete delay mean error: {error:.4f}") + + + def test_discrete_delay_zero_initial(self): + """Output should be zero during initial fill period""" + + self.Sim.run(duration=0.5, reset=True) + + time, [inp, delayed] = self.Sco.read() + + #during first tau seconds, output should be 0 + mask = time < self.tau * 0.5 + early_output = delayed[mask] + + self.assertTrue(np.all(early_output == 0.0), + "Discrete delay output should be zero before buffer fills") + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/evals/test_signal_processing_system.py b/tests/evals/test_signal_processing_system.py new file mode 100644 index 00000000..693abe5f --- /dev/null +++ b/tests/evals/test_signal_processing_system.py @@ -0,0 +1,192 @@ +######################################################################################## +## +## Testing signal processing system +## +## Tests delay, sample-hold, and filter blocks in a signal processing chain. +## Verifies correct time delay, periodic sampling, and filtering behavior. +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim import Simulation, Connection +from pathsim.blocks import ( + SinusoidalSource, + Source, + Delay, + SampleHold, + Scope + ) + +from pathsim.solvers import RKCK54 + + +# TESTCASE ============================================================================= + +class TestDelaySystem(unittest.TestCase): + """ + Test that a delay block correctly delays a signal by tau. + + System: Source(sin(t)) → Delay(tau) → Scope + Verify: output(t) = input(t - tau) for t >= tau + """ + + def setUp(self): + + self.tau = 0.5 + self.freq = 1.0 + + Src = SinusoidalSource(amplitude=1.0, frequency=self.freq) + Dly = Delay(tau=self.tau) + self.Sco = Scope(labels=["input", "delayed"]) + + blocks = [Src, Dly, self.Sco] + + connections = [ + Connection(Src, Dly, self.Sco[0]), + Connection(Dly, self.Sco[1]) + ] + + self.Sim = Simulation( + blocks, + connections, + dt=0.001, + log=False + ) + + + def test_delay_matches_shifted_signal(self): + """Verify delayed signal matches time-shifted input""" + + self.Sim.run(duration=5.0, reset=True) + + time, [inp, delayed] = self.Sco.read() + + #only check after delay has filled (t > tau + settling) + mask = time > self.tau + 0.5 + t_check = time[mask] + + #expected delayed signal: sin(2*pi*freq*(t - tau)) + expected = np.sin(2 * np.pi * self.freq * (t_check - self.tau)) + actual = delayed[mask] + + #should match within reasonable tolerance + error = np.max(np.abs(expected - actual)) + self.assertLess(error, 0.05, + f"Delay error too large: {error:.4f}") + + +class TestSampleHoldSystem(unittest.TestCase): + """ + Test that sample-hold block correctly samples at fixed intervals. + + System: Source(ramp) → SampleHold(T) → Scope + Verify: output is piecewise constant, changing every T seconds + """ + + def setUp(self): + + self.T = 0.5 # sampling period + + Src = Source(lambda t: t) # ramp + self.SH = SampleHold(T=self.T) + self.Sco = Scope(labels=["input", "sampled"]) + + blocks = [Src, self.SH, self.Sco] + + connections = [ + Connection(Src, self.SH, self.Sco[0]), + Connection(self.SH, self.Sco[1]) + ] + + self.Sim = Simulation( + blocks, + connections, + dt=0.01, + log=False + ) + + + def test_sample_hold_piecewise_constant(self): + """Verify sample-hold output is piecewise constant""" + + self.Sim.run(duration=5.0, reset=True) + + time, [inp, sampled] = self.Sco.read() + + #check that sampled output changes at sampling intervals + #between samples, the output should be constant + for k in range(1, 8): + t_start = k * self.T + 0.01 + t_end = (k + 1) * self.T - 0.01 + mask = (time >= t_start) & (time <= t_end) + + if np.sum(mask) > 2: + segment = sampled[mask] + #within a hold period, all values should be equal + self.assertAlmostEqual( + np.max(segment) - np.min(segment), 0.0, 2, + f"Sample-hold not constant in period {k}" + ) + + + def test_sample_hold_captures_correct_value(self): + """Verify sample-hold captures the input value at sample times""" + + self.Sim.run(duration=3.0, reset=True) + + time, [inp, sampled] = self.Sco.read() + + #just after each sample time, the held value should match the ramp at sample time + for k in range(1, 5): + t_sample = k * self.T + #find index just after sample time + idx = np.searchsorted(time, t_sample + 0.02) + if idx < len(sampled): + held_value = sampled[idx] + #held value should be close to t_sample (the ramp value at sample time) + self.assertAlmostEqual(held_value, t_sample, 1, + f"Held value {held_value:.3f} != expected {t_sample:.3f}") + + +class TestDelayAdaptive(unittest.TestCase): + """Test delay block with adaptive solver""" + + def test_delay_with_adaptive_solver(self): + + tau = 0.3 + Src = SinusoidalSource(amplitude=1.0, frequency=2.0) + Dly = Delay(tau=tau) + Sco = Scope(labels=["input", "delayed"]) + + Sim = Simulation( + blocks=[Src, Dly, Sco], + connections=[ + Connection(Src, Dly, Sco[0]), + Connection(Dly, Sco[1]) + ], + Solver=RKCK54, + tolerance_lte_abs=1e-6, + log=False + ) + + Sim.run(duration=3.0, reset=True) + + time, [inp, delayed] = Sco.read() + + mask = time > tau + 0.5 + t_check = time[mask] + expected = np.sin(2 * np.pi * 2.0 * (t_check - tau)) + actual = delayed[mask] + + error = np.max(np.abs(expected - actual)) + self.assertLess(error, 0.1) + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/evals/test_steadystate_transient_system.py b/tests/evals/test_steadystate_transient_system.py new file mode 100644 index 00000000..fa61efeb --- /dev/null +++ b/tests/evals/test_steadystate_transient_system.py @@ -0,0 +1,308 @@ +######################################################################################## +## +## Testing steady-state + transient simulation +## +## Verifies the two-phase simulation workflow: find DC operating point +## (steady state), then run transient simulation from that point. +## Also tests linearization and simulation reset/continuation. +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim import Simulation, Connection +from pathsim.blocks import ( + Source, + Constant, + Integrator, + Amplifier, + Adder, + Scope, + DynamicalSystem + ) + +from pathsim.solvers import RKCK54, ESDIRK32 + + +# TESTCASE ============================================================================= + +class TestSteadyStateTransient(unittest.TestCase): + """ + Test steady state solve followed by transient simulation. + + System: dx/dt = -2*(x - u), y = x + With u=3 (constant input), steady state is x=3. + + After finding steady state, apply a step change in input + and verify the transient response. + """ + + def test_steady_state_then_step(self): + """Find DC operating point then apply step change""" + + #system: dx/dt = -x + u, u=2 -> steady state x=2 + Src = Constant(2.0) + Int = Integrator(0.0) + Amp = Amplifier(-1.0) + Add = Adder() + Sco = Scope(labels=["state"]) + + Sim = Simulation( + blocks=[Src, Int, Amp, Add, Sco], + connections=[ + Connection(Src, Add[0]), + Connection(Amp, Add[1]), + Connection(Add, Int), + Connection(Int, Amp, Sco) + ], + log=False + ) + + #find steady state + Sim.steadystate(reset=True) + + #state should be at 2.0 + self.assertAlmostEqual(Int.outputs[0], 2.0, 3, + f"Steady state should be 2.0, got {Int.outputs[0]:.4f}") + + + def test_steady_state_then_transient(self): + """Find steady state then run transient with perturbation""" + + Src = Constant(2.0) + + Int = Integrator(0.0) # start from zero + Amp = Amplifier(-1.0) + Add = Adder() + Sco = Scope(labels=["state"]) + + #system: dx/dt = -x + u, u=2 -> steady state x=2 + Sim = Simulation( + blocks=[Src, Int, Amp, Add, Sco], + connections=[ + Connection(Src, Add[0]), + Connection(Amp, Add[1]), + Connection(Add, Int), + Connection(Int, Amp, Sco) + ], + log=False + ) + + #find steady state from zero + Sim.steadystate(reset=True) + + #state should be at 2.0 + self.assertAlmostEqual(Int.outputs[0], 2.0, 3) + + #now run transient from steady state (should remain at 2.0) + Sim.run(duration=5, reset=False) + + time, [state] = Sco.read() + + #state should stay near 2.0 throughout + self.assertTrue(np.allclose(state, 2.0, atol=0.1), + f"State should stay near 2.0, range: [{np.min(state):.3f}, {np.max(state):.3f}]") + + +class TestLinearizeAndRun(unittest.TestCase): + """ + Test linearization followed by simulation. + + Linearize a nonlinear system around an operating point, then + run the linearized system and verify it matches the expected + linear behavior. + """ + + def test_linearize_nonlinear_system(self): + """Linearize a nonlinear plant and verify small-signal behavior""" + + #nonlinear system: dx/dt = -x^2 + u, y = x + #around x=1, u=1: linearized is dx/dt = -2*dx + du + DS = DynamicalSystem( + func_dyn=lambda x, u, t: -x**2 + u, + func_alg=lambda x, u, t: x, + initial_value=1.0, + jac_dyn=lambda x, u, t: -2*x + ) + + Src = Constant(1.0) # u=1 keeps x=1 at equilibrium + Sco = Scope(labels=["output"]) + + Sim = Simulation( + blocks=[Src, DS, Sco], + connections=[ + Connection(Src, DS), + Connection(DS, Sco) + ], + Solver=RKCK54, + tolerance_lte_abs=1e-8, + log=False + ) + + #first get to equilibrium + Sim.steadystate(reset=True) + self.assertAlmostEqual(DS.outputs[0], 1.0, 3) + + #linearize around equilibrium + Sim.linearize() + + #run the linearized system - it should stay at equilibrium + Sim.run(duration=3, reset=False) + + time, [res] = Sco.read() + + #linearized system should stay at x=1 (equilibrium) + self.assertTrue(np.allclose(res, 1.0, atol=0.01), + f"Linearized system should stay at 1.0, got range [{np.min(res):.4f}, {np.max(res):.4f}]") + + #delinearize + Sim.delinearize() + + +class TestResetAndContinue(unittest.TestCase): + """ + Test simulation reset and continuation behavior. + """ + + def test_run_reset_run(self): + """Run, reset, run again should produce same results""" + + Src = Source(lambda t: 1.0) + Int = Integrator(0.0) + Amp = Amplifier(-1.0) + Add = Adder() + Sco = Scope(labels=["state"]) + + Sim = Simulation( + blocks=[Src, Int, Amp, Add, Sco], + connections=[ + Connection(Src, Add[0]), + Connection(Amp, Add[1]), + Connection(Add, Int), + Connection(Int, Amp, Sco) + ], + dt=0.01, + log=False + ) + + #first run + Sim.run(duration=5, reset=True) + time1, [state1] = Sco.read() + + #reset and run again + Sim.run(duration=5, reset=True) + time2, [state2] = Sco.read() + + #results should be identical + self.assertTrue(np.allclose(time1, time2)) + self.assertTrue(np.allclose(state1, state2)) + + + def test_continuation_from_midpoint(self): + """Run 5s, continue for another 5s, should match a single 10s run""" + + Src = Source(lambda t: 1.0) + Int = Integrator(0.0) + Amp = Amplifier(-1.0) + Add = Adder() + Sco1 = Scope(labels=["state"]) + Sco2 = Scope(labels=["state"]) + + #system 1: run 10s in one go + Sim1 = Simulation( + blocks=[Src, Int, Amp, Add, Sco1], + connections=[ + Connection(Src, Add[0]), + Connection(Amp, Add[1]), + Connection(Add, Int), + Connection(Int, Amp, Sco1) + ], + dt=0.01, + log=False + ) + + Sim1.run(duration=10, reset=True) + time_full, [state_full] = Sco1.read() + + #system 2: identical but run 5+5 + Src2 = Source(lambda t: 1.0) + Int2 = Integrator(0.0) + Amp2 = Amplifier(-1.0) + Add2 = Adder() + + Sim2 = Simulation( + blocks=[Src2, Int2, Amp2, Add2, Sco2], + connections=[ + Connection(Src2, Add2[0]), + Connection(Amp2, Add2[1]), + Connection(Add2, Int2), + Connection(Int2, Amp2, Sco2) + ], + dt=0.01, + log=False + ) + + Sim2.run(duration=5, reset=True) + Sim2.run(duration=5, reset=False) # continue + time_split, [state_split] = Sco2.read() + + #the split run may have slight numerical differences at boundary + #compare final state values - should agree to reasonable tolerance + self.assertAlmostEqual(state_full[-1], state_split[-1], 5) + + #note: split run may overshoot by one extra dt at the boundary, + #so we only check that total simulated times are close (within ~dt) + self.assertAlmostEqual(Sim1.time, Sim2.time, 1) + + +class TestMultipleSolverSwitch(unittest.TestCase): + """ + Test switching solvers mid-simulation and verify results + are consistent. + """ + + def test_solver_switch_during_simulation(self): + """Switch from explicit to implicit solver and verify consistency""" + + Src = Source(lambda t: 1.0) + Int = Integrator(0.0) + Amp = Amplifier(-1.0) + Add = Adder() + Sco = Scope(labels=["state"]) + + Sim = Simulation( + blocks=[Src, Int, Amp, Add, Sco], + connections=[ + Connection(Src, Add[0]), + Connection(Amp, Add[1]), + Connection(Add, Int), + Connection(Int, Amp, Sco) + ], + Solver=RKCK54, + tolerance_lte_abs=1e-8, + log=False + ) + + #run with explicit solver + Sim.run(duration=3, reset=True) + state_at_3 = Int.outputs[0] + + #switch to implicit solver and continue + Sim._set_solver(ESDIRK32, tolerance_lte_abs=1e-8) + Sim.run(duration=3, reset=False) + + time, [state] = Sco.read() + + #analytical: x(t) = 1 - exp(-t) for unit step + ref_final = 1.0 - np.exp(-6.0) + self.assertAlmostEqual(state[-1], ref_final, 3, + f"Final state: {state[-1]:.6f}, expected: {ref_final:.6f}") + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/evals/test_switch_lti_system.py b/tests/evals/test_switch_lti_system.py new file mode 100644 index 00000000..3607bf5b --- /dev/null +++ b/tests/evals/test_switch_lti_system.py @@ -0,0 +1,257 @@ +######################################################################################## +## +## Testing switch routing and LTI systems +## +## Tests Switch block with scheduled switching, and StateSpace/TransferFunction +## blocks in feedback loops. Verifies correct signal routing and LTI behavior. +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim import Simulation, Connection +from pathsim.blocks import ( + Source, + Constant, + Switch, + Integrator, + Amplifier, + Adder, + Scope, + StateSpace, + TransferFunctionNumDen + ) + +from pathsim.events.schedule import Schedule + +from pathsim.solvers import RKCK54, ESDIRK32 + + +# TESTCASE ============================================================================= + +class TestSwitchRoutingSystem(unittest.TestCase): + """ + Test Switch block selecting between multiple input sources. + + System: two sources feed a switch, scheduled events toggle the switch. + Verify: output matches the selected source at each time segment. + """ + + def test_switch_between_sources(self): + """Switch between constant sources using scheduled events""" + + Src0 = Constant(1.0) + Src1 = Constant(5.0) + Sw = Switch(switch_state=0) # start with input 0 + Sco = Scope(labels=["output"]) + + #schedule to switch at t=2 and t=4 + def switch_to_1(t): + Sw.select(1) + def switch_to_0(t): + Sw.select(0) + + evt1 = Schedule(t_start=2.0, t_period=100, func_act=switch_to_1) + evt2 = Schedule(t_start=4.0, t_period=100, func_act=switch_to_0) + + Sim = Simulation( + blocks=[Src0, Src1, Sw, Sco], + connections=[ + Connection(Src0, Sw[0]), + Connection(Src1, Sw[1]), + Connection(Sw, Sco) + ], + events=[evt1, evt2], + dt=0.01, + log=False + ) + + Sim.run(duration=6.0, reset=True) + + time, [out] = Sco.read() + + #t < 2: output should be 1.0 (source 0) + mask_0 = (time > 0.1) & (time < 1.9) + self.assertTrue(np.allclose(out[mask_0], 1.0, atol=0.1), + "Before switch: output should be 1.0") + + #2 < t < 4: output should be 5.0 (source 1) + mask_1 = (time > 2.1) & (time < 3.9) + self.assertTrue(np.allclose(out[mask_1], 5.0, atol=0.1), + "After first switch: output should be 5.0") + + #t > 4: output should be 1.0 again (source 0) + mask_2 = (time > 4.1) & (time < 5.9) + self.assertTrue(np.allclose(out[mask_2], 1.0, atol=0.1), + "After second switch: output should be 1.0") + + + def test_switch_with_none_state(self): + """Switch with None state should output 0""" + + Src = Constant(10.0) + Sw = Switch(switch_state=None) + Sco = Scope(labels=["output"]) + + Sim = Simulation( + blocks=[Src, Sw, Sco], + connections=[ + Connection(Src, Sw[0]), + Connection(Sw, Sco) + ], + dt=0.01, + log=False + ) + + Sim.run(duration=1.0, reset=True) + + time, [out] = Sco.read() + + #with None state, output should be 0 + self.assertTrue(np.allclose(out, 0.0, atol=0.01)) + + +class TestStateSpaceSystem(unittest.TestCase): + """ + Test StateSpace block implementing a first-order system. + + System: dx/dt = A*x + B*u, y = C*x + D*u + With A=-1, B=1, C=1, D=0 -> first order low-pass + Step response: y(t) = 1 - exp(-t) + """ + + def setUp(self): + + Src = Source(lambda t: 1.0) # step input + + #first-order system: dx/dt = -x + u, y = x + self.SS = StateSpace( + A=[[-1.0]], + B=[[1.0]], + C=[[1.0]], + D=[[0.0]] + ) + + self.Sco = Scope(labels=["output"]) + + self.Sim = Simulation( + blocks=[Src, self.SS, self.Sco], + connections=[ + Connection(Src, self.SS), + Connection(self.SS, self.Sco) + ], + log=False + ) + + + def test_step_response_explicit(self): + """Verify step response with explicit adaptive solver""" + + self.Sim._set_solver(RKCK54, tolerance_lte_abs=1e-6, tolerance_lte_rel=0.0) + self.Sim.run(duration=8, reset=True) + + time, [res] = self.Sco.read() + ref = 1.0 - np.exp(-time) + + error = np.max(np.abs(ref - res)) + self.assertLess(error, 1e-4, + f"Step response error: {error:.2e}") + + + def test_step_response_implicit(self): + """Verify step response with implicit adaptive solver""" + + self.Sim._set_solver(ESDIRK32, tolerance_lte_abs=1e-6, tolerance_lte_rel=0.0) + self.Sim.run(duration=8, reset=True) + + time, [res] = self.Sco.read() + ref = 1.0 - np.exp(-time) + + error = np.max(np.abs(ref - res)) + self.assertLess(error, 1e-4, + f"Step response error: {error:.2e}") + + +class TestTransferFunctionSystem(unittest.TestCase): + """ + Test TransferFunction block in a feedback system. + + Transfer function: H(s) = 1/(s+1) which is equivalent to + the first-order system dx/dt = -x + u, y = x. + """ + + def test_tf_step_response(self): + """Verify transfer function step response""" + + Src = Source(lambda t: 1.0) + + #H(s) = 1/(s+1) + TF = TransferFunctionNumDen(Num=[1.0], Den=[1.0, 1.0]) + + Sco = Scope(labels=["output"]) + + Sim = Simulation( + blocks=[Src, TF, Sco], + connections=[ + Connection(Src, TF), + Connection(TF, Sco) + ], + Solver=RKCK54, + tolerance_lte_abs=1e-6, + log=False + ) + + Sim.run(duration=8, reset=True) + + time, [res] = Sco.read() + ref = 1.0 - np.exp(-time) + + error = np.max(np.abs(ref - res)) + self.assertLess(error, 1e-4, + f"TF step response error: {error:.2e}") + + + def test_tf_in_feedback(self): + """Test transfer function in a negative feedback loop""" + + #plant: H(s) = 1/(s+1) + #controller: proportional gain K=2 + #closed loop: y/r = K*H/(1+K*H) = 2/(s+3) + #step response: y(t) = 2/3 * (1 - exp(-3t)) + + Src = Source(lambda t: 1.0) + Err = Adder("+-") + K = Amplifier(2.0) + Plant = TransferFunctionNumDen(Num=[1.0], Den=[1.0, 1.0]) + Sco = Scope(labels=["output"]) + + Sim = Simulation( + blocks=[Src, Err, K, Plant, Sco], + connections=[ + Connection(Src, Err), + Connection(Plant, Err[1], Sco), + Connection(Err, K), + Connection(K, Plant) + ], + Solver=RKCK54, + tolerance_lte_abs=1e-8, + log=False + ) + + Sim.run(duration=5, reset=True) + + time, [res] = Sco.read() + ref = (2.0/3.0) * (1.0 - np.exp(-3.0 * time)) + + error = np.max(np.abs(ref - res)) + self.assertLess(error, 1e-4, + f"Feedback TF error: {error:.2e}") + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/pathsim/blocks/test_ctrl.py b/tests/pathsim/blocks/test_ctrl.py new file mode 100644 index 00000000..407f3f87 --- /dev/null +++ b/tests/pathsim/blocks/test_ctrl.py @@ -0,0 +1,498 @@ +######################################################################################## +## +## TESTS FOR +## 'blocks.ctrl.py' +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim.blocks.ctrl import ( + PT1, + PT2, + LeadLag, + PID, + AntiWindupPID, + RateLimiter, + Backlash, + Deadband + ) + +#base solver for testing +from pathsim.solvers._solver import Solver + +from tests.pathsim.blocks._embedding import Embedding + + +# TESTS ================================================================================ + +class TestPT1(unittest.TestCase): + """Test the implementation of the 'PT1' block class""" + + def test_init(self): + + pt1 = PT1(K=2.0, T=0.5) + + self.assertEqual(pt1.A.shape, (1, 1)) + self.assertEqual(pt1.B.shape, (1, 1)) + self.assertEqual(pt1.C.shape, (1, 1)) + self.assertEqual(pt1.D.shape, (1, 1)) + + #check statespace matrices + self.assertAlmostEqual(pt1.A[0, 0], -2.0) # -1/T + self.assertAlmostEqual(pt1.B[0, 0], 4.0) # K/T + self.assertAlmostEqual(pt1.C[0, 0], 1.0) + self.assertAlmostEqual(pt1.D[0, 0], 0.0) + + + def test_len(self): + + #PT1 has no direct passthrough (D=0) + pt1 = PT1(K=2.0, T=0.5) + self.assertEqual(len(pt1), 0) + + + def test_shape(self): + + pt1 = PT1() + self.assertEqual(pt1.shape, (1, 1)) + + + def test_set_solver(self): + + pt1 = PT1(K=1.0, T=1.0) + pt1.set_solver(Solver, None) + self.assertTrue(isinstance(pt1.engine, Solver)) + + + def test_embedding(self): + + #PT1 with D=0 -> output depends only on state, not input + pt1 = PT1(K=1.0, T=1.0) + pt1.set_solver(Solver, None) + + def src(t): return 1.0 + def ref(t): return 0.0 #initial state is zero, no passthrough + + E = Embedding(pt1, src, ref) + self.assertEqual(*E.check_SISO(0)) + + +class TestPT2(unittest.TestCase): + """Test the implementation of the 'PT2' block class""" + + def test_init(self): + + pt2 = PT2(K=1.0, T=0.5, d=0.7) + + self.assertEqual(pt2.A.shape, (2, 2)) + self.assertEqual(pt2.B.shape, (2, 1)) + self.assertEqual(pt2.C.shape, (1, 2)) + self.assertEqual(pt2.D.shape, (1, 1)) + + #check statespace matrices + T, d, K = 0.5, 0.7, 1.0 + self.assertAlmostEqual(pt2.A[0, 0], 0.0) + self.assertAlmostEqual(pt2.A[0, 1], 1.0) + self.assertAlmostEqual(pt2.A[1, 0], -1.0 / T**2) + self.assertAlmostEqual(pt2.A[1, 1], -2.0 * d / T) + self.assertAlmostEqual(pt2.C[0, 0], K / T**2) + + + def test_len(self): + + #PT2 has no direct passthrough (D=0) + pt2 = PT2(K=1.0, T=1.0, d=0.5) + self.assertEqual(len(pt2), 0) + + + def test_shape(self): + + pt2 = PT2() + self.assertEqual(pt2.shape, (1, 1)) + + + def test_damping_cases(self): + + #underdamped + pt2 = PT2(K=1.0, T=1.0, d=0.3) + self.assertEqual(pt2.A.shape, (2, 2)) + + #critically damped + pt2 = PT2(K=1.0, T=1.0, d=1.0) + self.assertEqual(pt2.A.shape, (2, 2)) + + #overdamped + pt2 = PT2(K=1.0, T=1.0, d=2.0) + self.assertEqual(pt2.A.shape, (2, 2)) + + +class TestLeadLag(unittest.TestCase): + """Test the implementation of the 'LeadLag' block class""" + + def test_init(self): + + ll = LeadLag(K=2.0, T1=0.5, T2=0.1) + + self.assertEqual(ll.A.shape, (1, 1)) + self.assertEqual(ll.B.shape, (1, 1)) + self.assertEqual(ll.C.shape, (1, 1)) + self.assertEqual(ll.D.shape, (1, 1)) + + #check statespace matrices + K, T1, T2 = 2.0, 0.5, 0.1 + self.assertAlmostEqual(ll.A[0, 0], -1.0 / T2) + self.assertAlmostEqual(ll.B[0, 0], 1.0 / T2) + self.assertAlmostEqual(ll.C[0, 0], K * (T2 - T1) / T2) + self.assertAlmostEqual(ll.D[0, 0], K * T1 / T2) + + + def test_len(self): + + #lead compensator: T1 > T2, has passthrough (D != 0) + ll = LeadLag(K=1.0, T1=1.0, T2=0.5) + self.assertEqual(len(ll), 1) + + #pure gain: T1 = T2, D = K + ll = LeadLag(K=1.0, T1=1.0, T2=1.0) + self.assertEqual(len(ll), 1) + + #T1 = 0: no passthrough (D = 0) + ll = LeadLag(K=1.0, T1=0.0, T2=1.0) + self.assertEqual(len(ll), 0) + + + def test_shape(self): + + ll = LeadLag() + self.assertEqual(ll.shape, (1, 1)) + + + def test_pure_gain(self): + + #when T1 = T2, should be pure gain K + ll = LeadLag(K=3.0, T1=1.0, T2=1.0) + self.assertAlmostEqual(ll.D[0, 0], 3.0) + self.assertAlmostEqual(ll.C[0, 0], 0.0) + + + def test_dc_gain(self): + + #DC gain should be K for any T1, T2 + #H(0) = K * (T1*0 + 1) / (T2*0 + 1) = K + #In state space: -C*A^{-1}*B + D + ll = LeadLag(K=2.5, T1=0.3, T2=0.8) + dc_gain = -ll.C @ np.linalg.inv(ll.A) @ ll.B + ll.D + self.assertAlmostEqual(dc_gain[0, 0], 2.5) + + +class TestPID(unittest.TestCase): + """Test the implementation of the 'PID' block class""" + + def test_init(self): + + pid = PID(Kp=2, Ki=0.5, Kd=0.1, f_max=100) + + self.assertEqual(pid.A.shape, (2, 2)) + self.assertEqual(pid.B.shape, (2, 1)) + self.assertEqual(pid.C.shape, (1, 2)) + self.assertEqual(pid.D.shape, (1, 1)) + + #check statespace matrices + Kp, Ki, Kd, f_max = 2, 0.5, 0.1, 100 + self.assertAlmostEqual(pid.A[0, 0], -f_max) + self.assertAlmostEqual(pid.A[0, 1], 0.0) + self.assertAlmostEqual(pid.A[1, 0], 0.0) + self.assertAlmostEqual(pid.A[1, 1], 0.0) + self.assertAlmostEqual(pid.B[0, 0], f_max) + self.assertAlmostEqual(pid.B[1, 0], 1.0) + self.assertAlmostEqual(pid.C[0, 0], -Kd * f_max) + self.assertAlmostEqual(pid.C[0, 1], Ki) + self.assertAlmostEqual(pid.D[0, 0], Kd * f_max + Kp) + + + def test_len(self): + + #has passthrough when Kp or Kd nonzero + pid = PID(Kp=1, Ki=0, Kd=0) + self.assertEqual(len(pid), 1) + + pid = PID(Kp=0, Ki=0, Kd=1) + self.assertEqual(len(pid), 1) + + #pure integrator: no passthrough + pid = PID(Kp=0, Ki=1, Kd=0) + self.assertEqual(len(pid), 0) + + + def test_shape(self): + + pid = PID() + self.assertEqual(pid.shape, (1, 1)) + + + def test_set_solver(self): + + pid = PID(Kp=1, Ki=1, Kd=0.1) + pid.set_solver(Solver, None) + self.assertTrue(isinstance(pid.engine, Solver)) + + + def test_embedding(self): + + #PID with Kp=2, Ki=0, Kd=0 -> pure proportional, D=2 + pid = PID(Kp=2, Ki=0, Kd=0) + pid.set_solver(Solver, None) + + def src(t): return 3.0 + def ref(t): return 6.0 #Kp * u = 2 * 3 + + E = Embedding(pid, src, ref) + self.assertAlmostEqual(*E.check_SISO(0), places=8) + + +class TestAntiWindupPID(unittest.TestCase): + """Test the implementation of the 'AntiWindupPID' block class""" + + def test_init(self): + + pid = AntiWindupPID(Kp=2, Ki=0.5, Kd=0.1, f_max=100, Ks=10, limits=[-5, 5]) + + self.assertEqual(pid.A.shape, (2, 2)) + self.assertEqual(pid.Ks, 10) + self.assertEqual(pid.limits, [-5, 5]) + + + def test_len(self): + + pid = AntiWindupPID(Kp=1, Ki=0.5, Kd=0) + self.assertEqual(len(pid), 1) + + + def test_shape(self): + + pid = AntiWindupPID() + self.assertEqual(pid.shape, (1, 1)) + + + def test_set_solver(self): + + pid = AntiWindupPID(Kp=1, Ki=1, Kd=0.1) + pid.set_solver(Solver, None) + self.assertTrue(isinstance(pid.engine, Solver)) + + + def test_dynamics_within_limits(self): + + #when output is within limits, anti-windup feedback w=0 + pid = AntiWindupPID(Kp=1, Ki=0.5, Kd=0, f_max=100, Ks=10, limits=[-10, 10]) + pid.set_solver(Solver, None) + + x = np.array([0.0, 0.0]) + u = np.array([1.0]) + + dx = pid.op_dyn(x, u, 0) + + #dx1 = f_max * (u0 - x1) = 100 * (1 - 0) = 100 + self.assertAlmostEqual(dx[0], 100.0) + #dx2 = u0 - w, with w=0 since y = Kp*u0 + Ki*x2 + Kd*f_max*(u0-x1) = 1 within limits + self.assertAlmostEqual(dx[1], 1.0) + + + def test_dynamics_outside_limits(self): + + #when output exceeds limits, anti-windup feedback kicks in + pid = AntiWindupPID(Kp=20, Ki=0.5, Kd=0, f_max=100, Ks=10, limits=[-5, 5]) + pid.set_solver(Solver, None) + + x = np.array([0.0, 0.0]) + u = np.array([1.0]) + + dx = pid.op_dyn(x, u, 0) + + #y = Kp*u0 + Ki*x2 + Kd*f_max*(u0-x1) = 20*1 + 0 + 0 = 20 (exceeds limit 5) + #w = Ks * (y - clip(y, -5, 5)) = 10 * (20 - 5) = 150 + #dx2 = u0 - w = 1 - 150 = -149 + self.assertAlmostEqual(dx[0], 100.0) + self.assertAlmostEqual(dx[1], -149.0) + + +class TestRateLimiter(unittest.TestCase): + """Test the implementation of the 'RateLimiter' block class""" + + def test_init(self): + + rl = RateLimiter(rate=10.0, f_max=1e3) + + self.assertEqual(rl.rate, 10.0) + self.assertEqual(rl.f_max, 1e3) + self.assertEqual(rl.initial_value, 0.0) + + + def test_len(self): + + #no direct passthrough + rl = RateLimiter() + self.assertEqual(len(rl), 0) + + + def test_shape(self): + + rl = RateLimiter() + self.assertEqual(rl.shape, (1, 1)) + + + def test_set_solver(self): + + rl = RateLimiter(rate=5.0) + rl.set_solver(Solver, None) + self.assertTrue(isinstance(rl.engine, Solver)) + + + def test_update(self): + + rl = RateLimiter(rate=1.0) + rl.set_solver(Solver, None) + + #output should be engine state (initially 0) + rl.update(0) + self.assertAlmostEqual(rl.outputs[0], 0.0) + + +class TestBacklash(unittest.TestCase): + """Test the implementation of the 'Backlash' block class""" + + def test_init(self): + + bl = Backlash(width=0.5, f_max=1e3) + + self.assertEqual(bl.width, 0.5) + self.assertEqual(bl.f_max, 1e3) + + + def test_len(self): + + #no direct passthrough + bl = Backlash() + self.assertEqual(len(bl), 0) + + + def test_shape(self): + + bl = Backlash() + self.assertEqual(bl.shape, (1, 1)) + + + def test_set_solver(self): + + bl = Backlash(width=1.0) + bl.set_solver(Solver, None) + self.assertTrue(isinstance(bl.engine, Solver)) + + + def test_update(self): + + bl = Backlash(width=1.0) + bl.set_solver(Solver, None) + + #output should be engine state (initially 0) + bl.update(0) + self.assertAlmostEqual(bl.outputs[0], 0.0) + + + def test_dynamics_inside_deadzone(self): + + #when gap < width/2, no movement + bl = Backlash(width=2.0, f_max=100) + bl.set_solver(Solver, None) + + #u=0.5, x=0 -> gap=0.5, hw=1.0 -> gap within [-1, 1] -> dx=0 + dx = bl.op_dyn(0.0, 0.5, 0) + self.assertAlmostEqual(float(dx), 0.0) + + + def test_dynamics_outside_deadzone(self): + + #when gap > width/2, output tracks + bl = Backlash(width=1.0, f_max=100) + bl.set_solver(Solver, None) + + #u=2.0, x=0 -> gap=2.0, hw=0.5 -> clip(2.0, -0.5, 0.5)=0.5 + #dx = f_max * (gap - clip(gap)) = 100 * (2.0 - 0.5) = 150 + dx = bl.op_dyn(0.0, 2.0, 0) + self.assertAlmostEqual(float(dx), 150.0) + + #negative direction: u=-3.0, x=0 -> gap=-3.0, clip(-3,-0.5,0.5)=-0.5 + #dx = 100 * (-3.0 - (-0.5)) = 100 * (-2.5) = -250 + dx = bl.op_dyn(0.0, -3.0, 0) + self.assertAlmostEqual(float(dx), -250.0) + + +class TestDeadband(unittest.TestCase): + """Test the implementation of the 'Deadband' block class""" + + def test_init(self): + + db = Deadband(lower=-0.5, upper=0.5) + + self.assertEqual(db.lower, -0.5) + self.assertEqual(db.upper, 0.5) + + + def test_len(self): + + #algebraic passthrough + db = Deadband() + self.assertEqual(len(db), 1) + + + def test_shape(self): + + db = Deadband() + self.assertEqual(db.shape, (1, 1)) + + + def test_embedding_inside(self): + + #input within dead zone -> output is 0 + db = Deadband(lower=-1.0, upper=1.0) + + def src(t): return 0.5 + def ref(t): return 0.0 + + E = Embedding(db, src, ref) + self.assertAlmostEqual(*E.check_SISO(0)) + + + def test_embedding_above(self): + + #input above dead zone -> output is u - upper + db = Deadband(lower=-1.0, upper=1.0) + + def src(t): return 3.0 + def ref(t): return 2.0 # 3.0 - 1.0 + + E = Embedding(db, src, ref) + self.assertAlmostEqual(*E.check_SISO(0)) + + + def test_embedding_below(self): + + #input below dead zone -> output is u - lower + db = Deadband(lower=-1.0, upper=1.0) + + def src(t): return -4.0 + def ref(t): return -3.0 # -4.0 - (-1.0) + + E = Embedding(db, src, ref) + self.assertAlmostEqual(*E.check_SISO(0)) + + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/pathsim/blocks/test_delay.py b/tests/pathsim/blocks/test_delay.py index 5a98236a..f4a86cec 100644 --- a/tests/pathsim/blocks/test_delay.py +++ b/tests/pathsim/blocks/test_delay.py @@ -105,6 +105,97 @@ def test_update(self): self.assertEqual(D.outputs[0], max(0, t-10.5)) +class TestDelayDiscrete(unittest.TestCase): + """ + Test the discrete-time (sampling_period) mode of the 'Delay' block class + """ + + def test_init_discrete(self): + + D = Delay(tau=0.01, sampling_period=0.001) + + self.assertEqual(D._n, 10) + self.assertEqual(len(D._ring), 10) + self.assertTrue(hasattr(D, 'events')) + self.assertEqual(len(D.events), 1) + + + def test_n_computation(self): + + #exact multiple + D = Delay(tau=0.05, sampling_period=0.01) + self.assertEqual(D._n, 5) + + #rounding + D = Delay(tau=0.015, sampling_period=0.01) + self.assertEqual(D._n, 2) + + #minimum of 1 + D = Delay(tau=0.001, sampling_period=0.01) + self.assertEqual(D._n, 1) + + + def test_len(self): + + D = Delay(tau=0.01, sampling_period=0.001) + + #no passthrough + self.assertEqual(len(D), 0) + + + def test_reset(self): + + D = Delay(tau=0.01, sampling_period=0.001) + + #push some values + D._sample_next_timestep = True + D.inputs[0] = 42.0 + D.sample(0, 0.001) + + D.reset() + + #ring buffer should be all zeros + self.assertTrue(all(v == 0.0 for v in D._ring)) + self.assertEqual(len(D._ring), D._n) + + + def test_discrete_delay(self): + + n = 3 + D = Delay(tau=0.003, sampling_period=0.001) + + self.assertEqual(D._n, n) + + #push values through the ring buffer + outputs = [] + for k in range(10): + D.inputs[0] = float(k) + D._sample_next_timestep = True + D.sample(k * 0.001, 0.001) + D.update(k * 0.001) + outputs.append(D.outputs[0]) + + #first n outputs should be 0 (initial buffer fill) + for k in range(n): + self.assertEqual(outputs[k], 0.0, f"output[{k}] should be 0.0") + + #after that, output should be delayed by n samples + for k in range(n, 10): + self.assertEqual(outputs[k], float(k - n), f"output[{k}] should be {k-n}") + + + def test_no_sample_without_flag(self): + + D = Delay(tau=0.003, sampling_period=0.001) + + #push a value without the flag set + D.inputs[0] = 42.0 + D.sample(0, 0.001) + + #ring buffer should be unchanged (all zeros) + self.assertTrue(all(v == 0.0 for v in D._ring)) + + # RUN TESTS LOCALLY ==================================================================== if __name__ == '__main__': diff --git a/tests/pathsim/blocks/test_divider.py b/tests/pathsim/blocks/test_divider.py new file mode 100644 index 00000000..a60b7edc --- /dev/null +++ b/tests/pathsim/blocks/test_divider.py @@ -0,0 +1,305 @@ +######################################################################################## +## +## TESTS FOR +## 'blocks.divider.py' +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim.blocks.divider import Divider + +from tests.pathsim.blocks._embedding import Embedding + + +# TESTS ================================================================================ + +class TestDivider(unittest.TestCase): + """ + Test the implementation of the 'Divider' block class + """ + + def test_init(self): + + # default initialization + D = Divider() + self.assertEqual(D.operations, "*/") + + # valid ops strings + for ops in ["*", "/", "*/", "/*", "**/", "/**"]: + D = Divider(ops) + self.assertEqual(D.operations, ops) + + # non-string types are rejected + for bad in [0.4, 3, [1, -1], True]: + with self.assertRaises(ValueError): + Divider(bad) + + # strings with invalid characters are rejected + for bad in ["+/", "*-", "a", "**0", "+-"]: + with self.assertRaises(ValueError): + Divider(bad) + + + def test_embedding(self): + """Test algebraic output against reference via Embedding.""" + + # default: '*/' — u0 * u2 * ... / u1 + D = Divider() + + def src(t): return t + 1, np.cos(t) + 2, 3.0 + def ref(t): return (t + 1) * 3.0 / (np.cos(t) + 2) + + E = Embedding(D, src, ref) + for t in range(10): self.assertEqual(*E.check_MIMO(t)) + + # '**/' : multiply first two, divide by third + D = Divider('**/') + + def src(t): return np.cos(t) + 2, np.sin(t) + 2, t + 1 + def ref(t): return (np.cos(t) + 2) * (np.sin(t) + 2) / (t + 1) + + E = Embedding(D, src, ref) + for t in range(10): self.assertEqual(*E.check_MIMO(t)) + + # '*/' : u0 / u1 + D = Divider('*/') + + def src(t): return t + 1, np.cos(t) + 2 + def ref(t): return (t + 1) / (np.cos(t) + 2) + + E = Embedding(D, src, ref) + for t in range(10): self.assertEqual(*E.check_MIMO(t)) + + # ops string shorter than number of inputs: extra inputs default to '*' + # '/' with 3 inputs → y = u1 * u2 / u0 + D = Divider('/') + + def src(t): return t + 1, np.cos(t) + 2, 3.0 + def ref(t): return (np.cos(t) + 2) * 3.0 / (t + 1) + + E = Embedding(D, src, ref) + for t in range(10): self.assertEqual(*E.check_MIMO(t)) + + # single input, default: passes through unchanged + D = Divider() + + def src(t): return np.cos(t) + def ref(t): return np.cos(t) + + E = Embedding(D, src, ref) + for t in range(10): self.assertEqual(*E.check_SISO(t)) + + + def test_linearization(self): + """Test linearize / delinearize round-trip.""" + + # default ('*/') — nonlinear, so only check at linearization point + D = Divider() + + def src(t): return np.cos(t) + 2, t + 1 + def ref(t): return (np.cos(t) + 2) / (t + 1) + + E = Embedding(D, src, ref) + + for t in range(10): self.assertEqual(*E.check_MIMO(t)) + + # linearize at the current operating point (inputs set to src(9) by the loop) + D.linearize(t) + a, b = E.check_MIMO(t) + self.assertAlmostEqual(np.linalg.norm(a - b), 0, 8) + + D.delinearize() + for t in range(10): self.assertEqual(*E.check_MIMO(t)) + + # with ops string + D = Divider('**/') + + def src(t): return np.cos(t) + 2, np.sin(t) + 2, t + 1 + def ref(t): return (np.cos(t) + 2) * (np.sin(t) + 2) / (t + 1) + + E = Embedding(D, src, ref) + + for t in range(10): self.assertEqual(*E.check_MIMO(t)) + + D.linearize(t) + a, b = E.check_MIMO(t) + self.assertAlmostEqual(np.linalg.norm(a - b), 0, 8) + + D.delinearize() + for t in range(10): self.assertEqual(*E.check_MIMO(t)) + + + def test_update_single(self): + + D = Divider() + + D.inputs[0] = 5.0 + D.update(None) + + self.assertEqual(D.outputs[0], 5.0) + + + def test_update_multi(self): + + # default '*/' with 3 inputs: ops=[*, /, *] → (u0 * u2) / u1 + D = Divider() + + D.inputs[0] = 6.0 + D.inputs[1] = 2.0 + D.inputs[2] = 3.0 + D.update(None) + + self.assertEqual(D.outputs[0], 9.0) + + + def test_update_ops(self): + + # '**/' : 2 * 3 / 4 = 1.5 + D = Divider('**/') + D.inputs[0] = 2.0 + D.inputs[1] = 3.0 + D.inputs[2] = 4.0 + D.update(None) + self.assertAlmostEqual(D.outputs[0], 1.5) + + # '*/' : 6 / 2 = 3 + D = Divider('*/') + D.inputs[0] = 6.0 + D.inputs[1] = 2.0 + D.update(None) + self.assertAlmostEqual(D.outputs[0], 3.0) + + # '/' with extra inputs: 2 * 3 / 4 = 1.5 (u0 divides, u1 u2 multiply) + D = Divider('/') + D.inputs[0] = 4.0 + D.inputs[1] = 2.0 + D.inputs[2] = 3.0 + D.update(None) + self.assertAlmostEqual(D.outputs[0], 1.5) + + # '/**' : u1 * u2 / u0 + D = Divider('/**') + D.inputs[0] = 4.0 + D.inputs[1] = 2.0 + D.inputs[2] = 3.0 + D.update(None) + self.assertAlmostEqual(D.outputs[0], 1.5) + + + def test_jacobian(self): + """Verify analytical Jacobian against central finite differences.""" + + eps = 1e-6 + + def numerical_jac(func, u): + n = len(u) + J = np.zeros((1, n)) + for k in range(n): + u_p = u.copy(); u_p[k] += eps + u_m = u.copy(); u_m[k] -= eps + J[0, k] = (func(u_p) - func(u_m)) / (2 * eps) + return J + + # default (all multiply) + D = Divider() + u = np.array([2.0, 3.0, 4.0]) + np.testing.assert_allclose( + D.op_alg.jac(u), + numerical_jac(D.op_alg._func, u), + rtol=1e-5, + ) + + # '**/' : u0 * u1 / u2 + D = Divider('**/') + u = np.array([2.0, 3.0, 4.0]) + np.testing.assert_allclose( + D.op_alg.jac(u), + numerical_jac(D.op_alg._func, u), + rtol=1e-5, + ) + + # '*/' : u0 / u1 + D = Divider('*/') + u = np.array([6.0, 2.0]) + np.testing.assert_allclose( + D.op_alg.jac(u), + numerical_jac(D.op_alg._func, u), + rtol=1e-5, + ) + + # '/**' : u1 * u2 / u0 + D = Divider('/**') + u = np.array([4.0, 2.0, 3.0]) + np.testing.assert_allclose( + D.op_alg.jac(u), + numerical_jac(D.op_alg._func, u), + rtol=1e-5, + ) + + # ops shorter than inputs: '/' with 3 inputs → u1 * u2 / u0 + D = Divider('/') + u = np.array([4.0, 2.0, 3.0]) + np.testing.assert_allclose( + D.op_alg.jac(u), + numerical_jac(D.op_alg._func, u), + rtol=1e-5, + ) + + + def test_zero_div(self): + + # 'warn' (default): produces inf, no exception + D = Divider('*/', zero_div='warn') + D.inputs[0] = 6.0 + D.inputs[1] = 0.0 + import warnings + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter('always') + D.update(None) + self.assertTrue(np.isinf(D.outputs[0])) + + # 'raise': ZeroDivisionError on zero denominator + D = Divider('*/', zero_div='raise') + D.inputs[0] = 6.0 + D.inputs[1] = 0.0 + with self.assertRaises(ZeroDivisionError): + D.update(None) + + # 'raise': no error when denominator is nonzero + D = Divider('*/', zero_div='raise') + D.inputs[0] = 6.0 + D.inputs[1] = 2.0 + D.update(None) + self.assertAlmostEqual(D.outputs[0], 3.0) + + # 'clamp': output is large-but-finite + D = Divider('*/', zero_div='clamp') + D.inputs[0] = 1.0 + D.inputs[1] = 0.0 + D.update(None) + self.assertTrue(np.isfinite(D.outputs[0])) + self.assertGreater(abs(D.outputs[0]), 1.0) + + # 'raise' invalid zero_div value + with self.assertRaises(ValueError): + Divider('*/', zero_div='ignore') + + # Jacobian: 'raise' on zero denominator input + D = Divider('*/', zero_div='raise') + with self.assertRaises(ZeroDivisionError): + D.op_alg.jac(np.array([6.0, 0.0])) + + # Jacobian: 'clamp' stays finite + D = Divider('*/', zero_div='clamp') + J = D.op_alg.jac(np.array([1.0, 0.0])) + self.assertTrue(np.all(np.isfinite(J))) + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/pathsim/blocks/test_dynsys.py b/tests/pathsim/blocks/test_dynsys.py index 2e6d5965..6320af4d 100644 --- a/tests/pathsim/blocks/test_dynsys.py +++ b/tests/pathsim/blocks/test_dynsys.py @@ -13,7 +13,8 @@ from pathsim.blocks.dynsys import DynamicalSystem #base solver for testing -from pathsim.solvers._solver import Solver +from pathsim.solvers._solver import Solver +from pathsim.solvers.euler import EUF, EUB from tests.pathsim.blocks._embedding import Embedding @@ -199,7 +200,7 @@ def test_time_varying_system(self): def test_nonlinear_dynamics(self): """Test nonlinear system (Van der Pol oscillator simplified)""" - + #dx/dt = x - x^3 D = DynamicalSystem( func_dyn=lambda x, u, t: x - x**3, @@ -213,6 +214,45 @@ def test_nonlinear_dynamics(self): self.assertAlmostEqual(result, 0.375, 8) + def test_solve(self): + """Test implicit solve path""" + + D = DynamicalSystem( + func_dyn=lambda x, u, t: -2*x + u, + func_alg=lambda x, u, t: x, + initial_value=1.0, + jac_dyn=lambda x, u, t: -2.0 + ) + D.set_solver(EUB, None) + + #buffer state before solve + D.engine.buffer(0.01) + + D.inputs[0] = 0.5 + err = D.solve(0, 0.01) + + self.assertIsInstance(err, (int, float, np.floating)) + + + def test_step(self): + """Test explicit step path""" + + D = DynamicalSystem( + func_dyn=lambda x, u, t: -x, + func_alg=lambda x, u, t: x, + initial_value=1.0 + ) + D.set_solver(EUF, None) + + #buffer state before step + D.engine.buffer(0.01) + + D.inputs[0] = 0.0 + success, error, scale = D.step(0, 0.01) + + self.assertTrue(success) + + # RUN TESTS LOCALLY ==================================================================== if __name__ == '__main__': diff --git a/tests/pathsim/blocks/test_logic.py b/tests/pathsim/blocks/test_logic.py new file mode 100644 index 00000000..e684205c --- /dev/null +++ b/tests/pathsim/blocks/test_logic.py @@ -0,0 +1,217 @@ +######################################################################################## +## +## TESTS FOR +## 'blocks.logic.py' +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim.blocks.logic import ( + GreaterThan, + LessThan, + Equal, + LogicAnd, + LogicOr, + LogicNot, +) + +from tests.pathsim.blocks._embedding import Embedding + + +# TESTS ================================================================================ + +class TestGreaterThan(unittest.TestCase): + """ + Test the implementation of the 'GreaterThan' block class + """ + + def test_embedding(self): + """test algebraic components via embedding""" + + B = GreaterThan() + + #test a > b + def src(t): return t, 5.0 + def ref(t): return float(t > 5.0) + E = Embedding(B, src, ref) + + for t in range(10): self.assertTrue(np.allclose(*E.check_MIMO(t))) + + def test_equal_values(self): + """test that equal values return 0""" + + B = GreaterThan() + B.inputs[0] = 5.0 + B.inputs[1] = 5.0 + B.update(0) + self.assertEqual(B.outputs[0], 0.0) + + def test_less_than(self): + """test that a < b returns 0""" + + B = GreaterThan() + B.inputs[0] = 3.0 + B.inputs[1] = 5.0 + B.update(0) + self.assertEqual(B.outputs[0], 0.0) + + +class TestLessThan(unittest.TestCase): + """ + Test the implementation of the 'LessThan' block class + """ + + def test_embedding(self): + """test algebraic components via embedding""" + + B = LessThan() + + #test a < b + def src(t): return t, 5.0 + def ref(t): return float(t < 5.0) + E = Embedding(B, src, ref) + + for t in range(10): self.assertTrue(np.allclose(*E.check_MIMO(t))) + + def test_equal_values(self): + """test that equal values return 0""" + + B = LessThan() + B.inputs[0] = 5.0 + B.inputs[1] = 5.0 + B.update(0) + self.assertEqual(B.outputs[0], 0.0) + + +class TestEqual(unittest.TestCase): + """ + Test the implementation of the 'Equal' block class + """ + + def test_equal(self): + """test that equal values return 1""" + + B = Equal() + B.inputs[0] = 5.0 + B.inputs[1] = 5.0 + B.update(0) + self.assertEqual(B.outputs[0], 1.0) + + def test_not_equal(self): + """test that different values return 0""" + + B = Equal() + B.inputs[0] = 5.0 + B.inputs[1] = 6.0 + B.update(0) + self.assertEqual(B.outputs[0], 0.0) + + def test_tolerance(self): + """test tolerance parameter""" + + B = Equal(tolerance=0.1) + B.inputs[0] = 5.0 + B.inputs[1] = 5.05 + B.update(0) + self.assertEqual(B.outputs[0], 1.0) + + B.inputs[1] = 5.2 + B.update(0) + self.assertEqual(B.outputs[0], 0.0) + + +class TestLogicAnd(unittest.TestCase): + """ + Test the implementation of the 'LogicAnd' block class + """ + + def test_truth_table(self): + """test all combinations of boolean inputs""" + + B = LogicAnd() + + cases = [ + (0.0, 0.0, 0.0), + (0.0, 1.0, 0.0), + (1.0, 0.0, 0.0), + (1.0, 1.0, 1.0), + ] + + for a, b, expected in cases: + B.inputs[0] = a + B.inputs[1] = b + B.update(0) + self.assertEqual(B.outputs[0], expected, f"AND({a}, {b}) should be {expected}") + + def test_nonzero_is_true(self): + """test that nonzero values are treated as true""" + + B = LogicAnd() + B.inputs[0] = 5.0 + B.inputs[1] = -3.0 + B.update(0) + self.assertEqual(B.outputs[0], 1.0) + + +class TestLogicOr(unittest.TestCase): + """ + Test the implementation of the 'LogicOr' block class + """ + + def test_truth_table(self): + """test all combinations of boolean inputs""" + + B = LogicOr() + + cases = [ + (0.0, 0.0, 0.0), + (0.0, 1.0, 1.0), + (1.0, 0.0, 1.0), + (1.0, 1.0, 1.0), + ] + + for a, b, expected in cases: + B.inputs[0] = a + B.inputs[1] = b + B.update(0) + self.assertEqual(B.outputs[0], expected, f"OR({a}, {b}) should be {expected}") + + +class TestLogicNot(unittest.TestCase): + """ + Test the implementation of the 'LogicNot' block class + """ + + def test_true_to_false(self): + """test that nonzero input gives 0""" + + B = LogicNot() + B.inputs[0] = 1.0 + B.update(0) + self.assertEqual(B.outputs[0], 0.0) + + def test_false_to_true(self): + """test that zero input gives 1""" + + B = LogicNot() + B.inputs[0] = 0.0 + B.update(0) + self.assertEqual(B.outputs[0], 1.0) + + def test_nonzero_is_true(self): + """test that arbitrary nonzero values are treated as true""" + + B = LogicNot() + B.inputs[0] = 42.0 + B.update(0) + self.assertEqual(B.outputs[0], 0.0) + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/pathsim/blocks/test_math.py b/tests/pathsim/blocks/test_math.py index a9c950b6..075e831f 100644 --- a/tests/pathsim/blocks/test_math.py +++ b/tests/pathsim/blocks/test_math.py @@ -544,6 +544,132 @@ def ref(t): +class TestAtan2(unittest.TestCase): + """ + Test the implementation of the 'Atan2' block class + """ + + def test_embedding(self): + """test algebraic components via embedding""" + + B = Atan2() + + def src(t): return np.sin(t + 0.1), np.cos(t + 0.1) + def ref(t): return np.arctan2(np.sin(t + 0.1), np.cos(t + 0.1)) + E = Embedding(B, src, ref) + + for t in range(10): self.assertTrue(np.allclose(*E.check_MIMO(t))) + + def test_quadrants(self): + """test all four quadrants""" + + B = Atan2() + + cases = [ + ( 1.0, 1.0, np.arctan2(1.0, 1.0)), + ( 1.0, -1.0, np.arctan2(1.0, -1.0)), + (-1.0, -1.0, np.arctan2(-1.0, -1.0)), + (-1.0, 1.0, np.arctan2(-1.0, 1.0)), + ] + + for a, b, expected in cases: + B.inputs[0] = a + B.inputs[1] = b + B.update(0) + self.assertAlmostEqual(B.outputs[0], expected) + + +class TestRescale(unittest.TestCase): + """ + Test the implementation of the 'Rescale' block class + """ + + def test_default_identity(self): + """test default mapping [0,1] -> [0,1] is identity""" + + B = Rescale() + + def src(t): return t * 0.1 + def ref(t): return t * 0.1 + E = Embedding(B, src, ref) + + for t in range(10): self.assertEqual(*E.check_SISO(t)) + + def test_custom_mapping(self): + """test custom linear mapping""" + + B = Rescale(i0=0.0, i1=10.0, o0=0.0, o1=100.0) + + def src(t): return float(t) + def ref(t): return float(t) * 10.0 + E = Embedding(B, src, ref) + + for t in range(10): self.assertAlmostEqual(*E.check_SISO(t)) + + def test_saturate(self): + """test saturation clamping""" + + B = Rescale(i0=0.0, i1=1.0, o0=0.0, o1=10.0, saturate=True) + + #input beyond range + B.inputs[0] = 2.0 + B.update(0) + self.assertEqual(B.outputs[0], 10.0) + + #input below range + B.inputs[0] = -1.0 + B.update(0) + self.assertEqual(B.outputs[0], 0.0) + + def test_no_saturate(self): + """test that without saturation, output can exceed range""" + + B = Rescale(i0=0.0, i1=1.0, o0=0.0, o1=10.0, saturate=False) + + B.inputs[0] = 2.0 + B.update(0) + self.assertEqual(B.outputs[0], 20.0) + + def test_vector_input(self): + """test with vector inputs""" + + B = Rescale(i0=0.0, i1=10.0, o0=-1.0, o1=1.0) + + def src(t): return float(t), float(t) * 2 + def ref(t): return -1.0 + float(t) * 0.2, -1.0 + float(t) * 2 * 0.2 + E = Embedding(B, src, ref) + + for t in range(5): self.assertTrue(np.allclose(*E.check_MIMO(t))) + + +class TestAlias(unittest.TestCase): + """ + Test the implementation of the 'Alias' block class + """ + + def test_passthrough_siso(self): + """test that input passes through unchanged""" + + B = Alias() + + def src(t): return float(t) + def ref(t): return float(t) + E = Embedding(B, src, ref) + + for t in range(10): self.assertEqual(*E.check_SISO(t)) + + def test_passthrough_mimo(self): + """test that vector input passes through unchanged""" + + B = Alias() + + def src(t): return float(t), float(t) * 2 + def ref(t): return float(t), float(t) * 2 + E = Embedding(B, src, ref) + + for t in range(10): self.assertTrue(np.allclose(*E.check_MIMO(t))) + + # RUN TESTS LOCALLY ==================================================================== if __name__ == '__main__': unittest.main(verbosity=2) \ No newline at end of file diff --git a/tests/pathsim/blocks/test_ode.py b/tests/pathsim/blocks/test_ode.py index b39f7844..992bfed9 100644 --- a/tests/pathsim/blocks/test_ode.py +++ b/tests/pathsim/blocks/test_ode.py @@ -15,7 +15,8 @@ from pathsim.blocks.ode import ODE #base solver for testing -from pathsim.solvers._solver import Solver +from pathsim.solvers._solver import Solver +from pathsim.solvers.euler import EUF, EUB # TESTS ================================================================================ @@ -122,6 +123,46 @@ def test_update(self): self.assertEqual(D.outputs[0], 0.0) + def test_solve(self): + + def f(x, u, t): + return -x + u + def J(x, u, t): + return -1.0 + + D = ODE(func=f, initial_value=1.0, jac=J) + D.set_solver(EUB, None) + + #buffer state before solve + D.engine.buffer(0.01) + + #call solve to exercise the implicit update path + D.inputs[0] = 2.0 + err = D.solve(0, 0.01) + + #error should be a numeric value + self.assertIsInstance(err, (int, float, np.floating)) + + + def test_step(self): + + def f(x, u, t): + return -x + + D = ODE(func=f, initial_value=1.0) + D.set_solver(EUF, None) + + #buffer state before step + D.engine.buffer(0.01) + + #call step to exercise the explicit update path + D.inputs[0] = 0.0 + success, error, scale = D.step(0, 0.01) + + #step should succeed + self.assertTrue(success) + + # RUN TESTS LOCALLY ==================================================================== if __name__ == '__main__': diff --git a/tests/pathsim/blocks/test_relay.py b/tests/pathsim/blocks/test_relay.py new file mode 100644 index 00000000..07d4cd93 --- /dev/null +++ b/tests/pathsim/blocks/test_relay.py @@ -0,0 +1,141 @@ +######################################################################################## +## +## TESTS FOR +## 'blocks.relay.py' +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim.blocks.relay import Relay +from pathsim.events.zerocrossing import ZeroCrossingUp, ZeroCrossingDown + + +# TESTS ================================================================================ + +class TestRelay(unittest.TestCase): + """ + Test the implementation of the 'Relay' block class + """ + + def test_init_default(self): + + R = Relay() + + self.assertEqual(R.threshold_up, 1.0) + self.assertEqual(R.threshold_down, 0.0) + self.assertEqual(R.value_up, 1.0) + self.assertEqual(R.value_down, 0.0) + + #check events are created + self.assertEqual(len(R.events), 2) + self.assertIsInstance(R.events[0], ZeroCrossingUp) + self.assertIsInstance(R.events[1], ZeroCrossingDown) + + + def test_init_custom(self): + + R = Relay( + threshold_up=21.0, + threshold_down=19.0, + value_up=0.0, + value_down=1.0 + ) + + self.assertEqual(R.threshold_up, 21.0) + self.assertEqual(R.threshold_down, 19.0) + self.assertEqual(R.value_up, 0.0) + self.assertEqual(R.value_down, 1.0) + + + def test_len(self): + + R = Relay() + self.assertEqual(len(R), 0) + + + def test_update(self): + + #update is a no-op for relay (events handle switching) + R = Relay() + R.inputs[0] = 5.0 + R.update(0) + + #output unchanged since update is a no-op + self.assertEqual(R.outputs[0], 0.0) + + + def test_event_functions(self): + + R = Relay(threshold_up=2.0, threshold_down=-1.0) + + #check up-crossing event function: input - threshold_up + R.inputs[0] = 5.0 + evt_up = R.events[0].func_evt(0) + self.assertEqual(evt_up, 3.0) # 5 - 2 + + R.inputs[0] = 0.0 + evt_up = R.events[0].func_evt(0) + self.assertEqual(evt_up, -2.0) # 0 - 2 + + #check down-crossing event function: input - threshold_down + R.inputs[0] = 0.0 + evt_down = R.events[1].func_evt(0) + self.assertEqual(evt_down, 1.0) # 0 - (-1) + + R.inputs[0] = -5.0 + evt_down = R.events[1].func_evt(0) + self.assertEqual(evt_down, -4.0) # -5 - (-1) + + + def test_event_actions(self): + + R = Relay( + threshold_up=1.0, + threshold_down=-1.0, + value_up=10.0, + value_down=-10.0 + ) + + #trigger up action + R.events[0].func_act(0) + self.assertEqual(R.outputs[0], 10.0) + + #trigger down action + R.events[1].func_act(0) + self.assertEqual(R.outputs[0], -10.0) + + + def test_thermostat_scenario(self): + + #thermostat: heater on below 19, off above 21 + R = Relay( + threshold_up=21.0, + threshold_down=19.0, + value_up=0.0, + value_down=1.0 + ) + + #temperature rises above 21 -> heater off + R.events[0].func_act(0) + self.assertEqual(R.outputs[0], 0.0) + + #temperature drops below 19 -> heater on + R.events[1].func_act(0) + self.assertEqual(R.outputs[0], 1.0) + + + def test_port_labels(self): + + R = Relay() + self.assertEqual(R.input_port_labels, {"in": 0}) + self.assertEqual(R.output_port_labels, {"out": 0}) + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/tests/pathsim/blocks/test_sources.py b/tests/pathsim/blocks/test_sources.py index 989b004c..1c54729c 100644 --- a/tests/pathsim/blocks/test_sources.py +++ b/tests/pathsim/blocks/test_sources.py @@ -18,6 +18,7 @@ ) from pathsim.events.schedule import Schedule, ScheduleList from pathsim.solvers import EUF +from pathsim.solvers.euler import EUB # TESTS ================================================================================ @@ -258,6 +259,40 @@ def test_len(self): S = SinusoidalPhaseNoiseSource() self.assertEqual(len(S), 0) + def test_continuous_sampling(self): + #sampling_period=None means continuous sampling + S = SinusoidalPhaseNoiseSource(sampling_period=None) + S.set_solver(EUF, None) + + n1_before = S.noise_1 + n2_before = S.noise_2 + + S.sample(0, 0.01) + + #noise should be re-sampled + #(statistically will almost never be the same) + self.assertTrue( + S.noise_1 != n1_before or S.noise_2 != n2_before + ) + + def test_solve(self): + S = SinusoidalPhaseNoiseSource() + S.set_solver(EUB, None) + + S.engine.buffer(0.01) + err = S.solve(0, 0.01) + self.assertEqual(err, 0.0) + + def test_step(self): + S = SinusoidalPhaseNoiseSource() + S.set_solver(EUF, None) + + S.engine.buffer(0.01) + success, error, scale = S.step(0, 0.01) + self.assertTrue(success) + self.assertEqual(error, 0.0) + self.assertIsNone(scale) + class TestChirpPhaseNoiseSource(unittest.TestCase): """Test the implementation of the 'ChirpPhaseNoiseSource' block class""" @@ -310,6 +345,37 @@ def test_len(self): C = ChirpPhaseNoiseSource() self.assertEqual(len(C), 0) + def test_reset(self): + C = ChirpPhaseNoiseSource() + n1 = C.noise_1 + C.reset() + #noise should be re-sampled on reset + #cannot assert inequality (random), just ensure it runs + self.assertIsNotNone(C.noise_1) + + def test_continuous_sampling(self): + C = ChirpPhaseNoiseSource(sampling_period=None) + C.set_solver(EUF, None) + C.sample(0, 0.01) + #just ensure it runs without error + self.assertIsNotNone(C.noise_1) + + def test_solve(self): + C = ChirpPhaseNoiseSource() + C.set_solver(EUB, None) + C.engine.buffer(0.01) + err = C.solve(0, 0.01) + self.assertEqual(err, 0.0) + + def test_step(self): + C = ChirpPhaseNoiseSource() + C.set_solver(EUF, None) + C.engine.buffer(0.01) + success, error, scale = C.step(0, 0.01) + self.assertTrue(success) + self.assertEqual(error, 0.0) + self.assertIsNone(scale) + class TestChirpSource(unittest.TestCase): """Test the deprecated ChirpSource alias""" diff --git a/tests/pathsim/blocks/test_switch.py b/tests/pathsim/blocks/test_switch.py index 8c75a1ef..8dd0defe 100644 --- a/tests/pathsim/blocks/test_switch.py +++ b/tests/pathsim/blocks/test_switch.py @@ -26,14 +26,14 @@ def test_init(self): #test default initialization S = Switch() - self.assertEqual(S.state, None) + self.assertEqual(S.switch_state, None) #test special initialization S = Switch(1) - self.assertEqual(S.state, 1) + self.assertEqual(S.switch_state, 1) - S = Switch(state=0) - self.assertEqual(S.state, 0) + S = Switch(switch_state=0) + self.assertEqual(S.switch_state, 0) def test_len(self): @@ -51,23 +51,23 @@ def test_select(self): #test the switch state selector S = Switch() - self.assertEqual(S.state, None) + self.assertEqual(S.switch_state, None) S.select(0) - self.assertEqual(S.state, 0) + self.assertEqual(S.switch_state, 0) S.select(1) - self.assertEqual(S.state, 1) - + self.assertEqual(S.switch_state, 1) + S.select(3) - self.assertEqual(S.state, 3) + self.assertEqual(S.switch_state, 3) def test_update(self): #test default initialization S = Switch() - self.assertEqual(S.state, None) + self.assertEqual(S.switch_state, None) S.inputs[0] = 3 S.update(0) @@ -77,7 +77,7 @@ def test_update(self): #test switch setting S = Switch(3) - self.assertEqual(S.state, 3) + self.assertEqual(S.switch_state, 3) S.inputs[0] = 3 S.inputs[1] = 4 diff --git a/tests/pathsim/test_checkpoint.py b/tests/pathsim/test_checkpoint.py new file mode 100644 index 00000000..94fed3d6 --- /dev/null +++ b/tests/pathsim/test_checkpoint.py @@ -0,0 +1,769 @@ +"""Tests for checkpoint save/load functionality.""" + +import os +import json +import tempfile + +import numpy as np +import pytest + +from pathsim import Simulation, Connection, Subsystem, Interface +from pathsim.blocks import ( + Source, Integrator, Amplifier, Scope, Constant, Function +) +from pathsim.blocks.delay import Delay +from pathsim.blocks.switch import Switch +from pathsim.blocks.fir import FIR +from pathsim.blocks.kalman import KalmanFilter +from pathsim.blocks.noise import WhiteNoise, PinkNoise +from pathsim.blocks.rng import RandomNumberGenerator + + +class TestBlockCheckpoint: + """Test block-level checkpoint methods.""" + + def test_basic_block_to_checkpoint(self): + """Block produces valid checkpoint data.""" + b = Integrator(1.0) + b.inputs[0] = 3.14 + prefix = "Integrator_0" + json_data, npz_data = b.to_checkpoint(prefix) + + assert json_data["type"] == "Integrator" + assert json_data["active"] is True + assert f"{prefix}/inputs" in npz_data + assert f"{prefix}/outputs" in npz_data + + def test_block_checkpoint_roundtrip(self): + """Block state survives save/load cycle.""" + b = Integrator(2.5) + b.inputs[0] = 1.0 + b.outputs[0] = 2.5 + prefix = "Integrator_0" + + json_data, npz_data = b.to_checkpoint(prefix) + + #reset block + b.reset() + assert b.inputs[0] == 0.0 + + #restore + b.load_checkpoint(prefix, json_data, npz_data) + assert np.isclose(b.inputs[0], 1.0) + assert np.isclose(b.outputs[0], 2.5) + + def test_block_type_mismatch_raises(self): + """Loading checkpoint with wrong type raises ValueError.""" + b = Integrator() + prefix = "Integrator_0" + json_data, npz_data = b.to_checkpoint(prefix) + + b2 = Amplifier(1.0) + with pytest.raises(ValueError, match="type mismatch"): + b2.load_checkpoint(prefix, json_data, npz_data) + + +class TestEventCheckpoint: + """Test event-level checkpoint methods.""" + + def test_event_checkpoint_roundtrip(self): + from pathsim.events import ZeroCrossing + e = ZeroCrossing(func_evt=lambda t: t - 1.0) + e._history = (0.5, 0.99) + e._times = [1.0, 2.0, 3.0] + e._active = False + prefix = "ZeroCrossing_0" + + json_data, npz_data = e.to_checkpoint(prefix) + + e.reset() + assert e._active is True + assert len(e._times) == 0 + + e.load_checkpoint(prefix, json_data, npz_data) + assert e._active is False + assert e._times == [1.0, 2.0, 3.0] + assert e._history == (0.5, 0.99) + + +class TestSwitchCheckpoint: + """Test Switch block checkpoint.""" + + def test_switch_state_preserved(self): + s = Switch(switch_state=2) + prefix = "Switch_0" + json_data, npz_data = s.to_checkpoint(prefix) + + s.select(None) + assert s.switch_state is None + + s.load_checkpoint(prefix, json_data, npz_data) + assert s.switch_state == 2 + + +class TestSimulationCheckpoint: + """Test simulation-level checkpoint save/load.""" + + def test_save_load_simple(self): + """Simple simulation checkpoint round-trip.""" + src = Source(lambda t: np.sin(2 * np.pi * t)) + integ = Integrator() + scope = Scope() + + sim = Simulation( + blocks=[src, integ, scope], + connections=[ + Connection(src, integ, scope), + ], + dt=0.01 + ) + + #run for 1 second + sim.run(1.0) + time_after_run = sim.time + state_after_run = integ.state.copy() + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "checkpoint") + sim.save_checkpoint(path) + + #verify files exist + assert os.path.exists(f"{path}.json") + assert os.path.exists(f"{path}.npz") + + #verify JSON structure + with open(f"{path}.json") as f: + data = json.load(f) + assert data["version"] == "1.0.0" + assert data["simulation"]["time"] == time_after_run + assert any(b["_key"] == "Integrator_0" for b in data["blocks"]) + + #reset and reload + sim.time = 0.0 + integ.state = np.array([0.0]) + + sim.load_checkpoint(path) + assert sim.time == time_after_run + assert np.allclose(integ.state, state_after_run) + + def test_continue_after_load(self): + """Simulation continues correctly after checkpoint load.""" + #run continuously for 2 seconds + src1 = Source(lambda t: 1.0) + integ1 = Integrator() + sim1 = Simulation( + blocks=[src1, integ1], + connections=[Connection(src1, integ1)], + dt=0.01 + ) + sim1.run(2.0) + reference_state = integ1.state.copy() + + #run for 1 second, save, load, run 1 more second + src2 = Source(lambda t: 1.0) + integ2 = Integrator() + sim2 = Simulation( + blocks=[src2, integ2], + connections=[Connection(src2, integ2)], + dt=0.01 + ) + sim2.run(1.0) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim2.save_checkpoint(path) + sim2.load_checkpoint(path) + sim2.run(1.0) # run 1 more second (t=1 -> t=2) + + #compare results + assert np.allclose(integ2.state, reference_state, rtol=1e-6) + + def test_scope_recordings(self): + """Scope recordings are saved when recordings=True.""" + src = Source(lambda t: t) + scope = Scope() + sim = Simulation( + blocks=[src, scope], + connections=[Connection(src, scope)], + dt=0.1 + ) + sim.run(1.0) + + with tempfile.TemporaryDirectory() as tmpdir: + #without recordings + path1 = os.path.join(tmpdir, "no_rec") + sim.save_checkpoint(path1, recordings=False) + npz1 = np.load(f"{path1}.npz") + assert "Scope_0/recording_time" not in npz1 + npz1.close() + + #with recordings + path2 = os.path.join(tmpdir, "with_rec") + sim.save_checkpoint(path2, recordings=True) + npz2 = np.load(f"{path2}.npz") + assert "Scope_0/recording_time" in npz2 + npz2.close() + + def test_delay_continuous_checkpoint(self): + """Continuous delay block preserves buffer.""" + src = Source(lambda t: np.sin(t)) + delay = Delay(tau=0.1) + scope = Scope() + sim = Simulation( + blocks=[src, delay, scope], + connections=[ + Connection(src, delay, scope), + ], + dt=0.01 + ) + sim.run(0.5) + + #capture delay output + delay_output = delay.outputs[0] + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim.save_checkpoint(path) + + #reset delay buffer + delay._buffer.clear() + + sim.load_checkpoint(path) + assert np.isclose(delay.outputs[0], delay_output) + + def test_delay_discrete_checkpoint(self): + """Discrete delay block preserves ring buffer.""" + src = Source(lambda t: float(t > 0)) + delay = Delay(tau=0.05, sampling_period=0.01) + sim = Simulation( + blocks=[src, delay], + connections=[Connection(src, delay)], + dt=0.01 + ) + sim.run(0.1) + + ring_before = list(delay._ring) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim.save_checkpoint(path) + delay._ring.clear() + sim.load_checkpoint(path) + assert list(delay._ring) == ring_before + + def test_cross_instance_load(self): + """Checkpoint loads into a freshly constructed simulation (different UUIDs).""" + src1 = Source(lambda t: 1.0) + integ1 = Integrator() + sim1 = Simulation( + blocks=[src1, integ1], + connections=[Connection(src1, integ1)], + dt=0.01 + ) + sim1.run(1.0) + saved_time = sim1.time + saved_state = integ1.state.copy() + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim1.save_checkpoint(path) + + #create entirely new simulation (new block objects, new UUIDs) + src2 = Source(lambda t: 1.0) + integ2 = Integrator() + sim2 = Simulation( + blocks=[src2, integ2], + connections=[Connection(src2, integ2)], + dt=0.01 + ) + + sim2.load_checkpoint(path) + assert sim2.time == saved_time + assert np.allclose(integ2.state, saved_state) + + def test_scope_recordings_preserved_without_flag(self): + """Loading without recordings flag does not erase existing recordings.""" + src = Source(lambda t: t) + scope = Scope() + sim = Simulation( + blocks=[src, scope], + connections=[Connection(src, scope)], + dt=0.1 + ) + sim.run(1.0) + + #scope has recordings + assert len(scope.recording_time) > 0 + rec_len = len(scope.recording_time) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim.save_checkpoint(path, recordings=False) + sim.load_checkpoint(path) + + #recordings should still be intact + assert len(scope.recording_time) == rec_len + + def test_multiple_same_type_blocks(self): + """Multiple blocks of the same type are matched by insertion order.""" + src = Source(lambda t: 1.0) + i1 = Integrator(1.0) + i2 = Integrator(2.0) + sim = Simulation( + blocks=[src, i1, i2], + connections=[Connection(src, i1), Connection(src, i2)], + dt=0.01 + ) + sim.run(0.5) + + state1 = i1.state.copy() + state2 = i2.state.copy() + assert not np.allclose(state1, state2) # different initial conditions + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim.save_checkpoint(path) + + i1.state = np.array([0.0]) + i2.state = np.array([0.0]) + + sim.load_checkpoint(path) + assert np.allclose(i1.state, state1) + assert np.allclose(i2.state, state2) + + +class TestFIRCheckpoint: + """Test FIR block checkpoint.""" + + def test_fir_buffer_preserved(self): + """FIR filter buffer survives checkpoint round-trip.""" + fir = FIR(coeffs=[0.25, 0.5, 0.25], T=0.01) + prefix = "FIR_0" + + #simulate some input to fill the buffer + fir._buffer.appendleft(1.0) + fir._buffer.appendleft(2.0) + buffer_before = list(fir._buffer) + + json_data, npz_data = fir.to_checkpoint(prefix) + + fir._buffer.clear() + fir._buffer.extend([0.0] * 3) + + fir.load_checkpoint(prefix, json_data, npz_data) + assert list(fir._buffer) == buffer_before + + +class TestKalmanFilterCheckpoint: + """Test KalmanFilter block checkpoint.""" + + def test_kalman_state_preserved(self): + """Kalman filter state and covariance survive checkpoint.""" + F = np.array([[1.0, 0.1], [0.0, 1.0]]) + H = np.array([[1.0, 0.0]]) + Q = np.eye(2) * 0.01 + R = np.array([[0.1]]) + + kf = KalmanFilter(F, H, Q, R) + prefix = "KalmanFilter_0" + + #set some state + kf.x = np.array([3.14, -1.0]) + kf.P = np.array([[0.5, 0.1], [0.1, 0.3]]) + + json_data, npz_data = kf.to_checkpoint(prefix) + + kf.x = np.zeros(2) + kf.P = np.eye(2) + + kf.load_checkpoint(prefix, json_data, npz_data) + assert np.allclose(kf.x, [3.14, -1.0]) + assert np.allclose(kf.P, [[0.5, 0.1], [0.1, 0.3]]) + + +class TestNoiseCheckpoint: + """Test noise block checkpoints.""" + + def test_white_noise_sample_preserved(self): + """WhiteNoise current sample survives checkpoint.""" + wn = WhiteNoise(standard_deviation=2.0) + wn._current_sample = 1.234 + prefix = "WhiteNoise_0" + + json_data, npz_data = wn.to_checkpoint(prefix) + wn._current_sample = 0.0 + + wn.load_checkpoint(prefix, json_data, npz_data) + assert wn._current_sample == pytest.approx(1.234) + + def test_pink_noise_state_preserved(self): + """PinkNoise algorithm state survives checkpoint.""" + pn = PinkNoise(num_octaves=8, seed=42) + prefix = "PinkNoise_0" + + #advance the noise state + for _ in range(10): + pn._generate_sample(0.01) + + n_samples_before = pn.n_samples + octaves_before = pn.octave_values.copy() + sample_before = pn._current_sample + + json_data, npz_data = pn.to_checkpoint(prefix) + + pn.reset() + assert pn.n_samples == 0 + + pn.load_checkpoint(prefix, json_data, npz_data) + assert pn.n_samples == n_samples_before + assert np.allclose(pn.octave_values, octaves_before) + + +class TestRNGCheckpoint: + """Test RandomNumberGenerator checkpoint.""" + + def test_rng_sample_preserved(self): + """RNG current sample survives checkpoint (continuous mode).""" + rng = RandomNumberGenerator(sampling_period=None) + prefix = "RandomNumberGenerator_0" + sample_before = rng._sample + + json_data, npz_data = rng.to_checkpoint(prefix) + rng._sample = 0.0 + + rng.load_checkpoint(prefix, json_data, npz_data) + assert rng._sample == pytest.approx(sample_before) + + +class TestSubsystemCheckpoint: + """Test Subsystem checkpoint.""" + + def test_subsystem_roundtrip(self): + """Subsystem with internal blocks survives checkpoint round-trip.""" + #build a simple subsystem: two integrators in series + If = Interface() + I1 = Integrator(1.0) + I2 = Integrator(0.0) + + sub = Subsystem( + blocks=[If, I1, I2], + connections=[ + Connection(If, I1), + Connection(I1, I2), + Connection(I2, If), + ] + ) + + #embed in a simulation + src = Source(lambda t: 1.0) + scope = Scope() + sim = Simulation( + blocks=[src, sub, scope], + connections=[ + Connection(src, sub), + Connection(sub, scope), + ], + dt=0.01 + ) + + sim.run(0.5) + state_I1 = I1.state.copy() + state_I2 = I2.state.copy() + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim.save_checkpoint(path) + + #zero out states + I1.state = np.array([0.0]) + I2.state = np.array([0.0]) + + sim.load_checkpoint(path) + assert np.allclose(I1.state, state_I1) + assert np.allclose(I2.state, state_I2) + + def test_subsystem_cross_instance(self): + """Subsystem checkpoint loads into a fresh simulation instance.""" + If1 = Interface() + I1 = Integrator(1.0) + sub1 = Subsystem( + blocks=[If1, I1], + connections=[Connection(If1, I1), Connection(I1, If1)] + ) + src1 = Source(lambda t: 1.0) + sim1 = Simulation( + blocks=[src1, sub1], + connections=[Connection(src1, sub1)], + dt=0.01 + ) + sim1.run(0.5) + state_before = I1.state.copy() + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim1.save_checkpoint(path) + + #new instance + If2 = Interface() + I2 = Integrator(1.0) + sub2 = Subsystem( + blocks=[If2, I2], + connections=[Connection(If2, I2), Connection(I2, If2)] + ) + src2 = Source(lambda t: 1.0) + sim2 = Simulation( + blocks=[src2, sub2], + connections=[Connection(src2, sub2)], + dt=0.01 + ) + sim2.load_checkpoint(path) + assert np.allclose(I2.state, state_before) + + +class TestGEARCheckpoint: + """Test GEAR solver checkpoint round-trip.""" + + def test_gear_solver_roundtrip(self): + """GEAR solver state survives checkpoint including BDF coefficients.""" + from pathsim.solvers import GEAR32 + + src = Source(lambda t: np.sin(2 * np.pi * t)) + integ = Integrator() + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + dt=0.01, + Solver=GEAR32 + ) + + #run long enough for GEAR to exit startup phase + sim.run(0.5) + state_after = integ.state.copy() + time_after = sim.time + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim.save_checkpoint(path) + + #reset state + integ.state = np.array([0.0]) + sim.time = 0.0 + + sim.load_checkpoint(path) + assert sim.time == time_after + assert np.allclose(integ.state, state_after) + + def test_gear_continue_after_load(self): + """GEAR simulation continues correctly after checkpoint load.""" + from pathsim.solvers import GEAR32 + + #reference: run 2s continuously + src1 = Source(lambda t: 1.0) + integ1 = Integrator() + sim1 = Simulation( + blocks=[src1, integ1], + connections=[Connection(src1, integ1)], + dt=0.01, + Solver=GEAR32 + ) + sim1.run(2.0) + reference = integ1.state.copy() + + #split: run 1s, save, load, run 1s more + src2 = Source(lambda t: 1.0) + integ2 = Integrator() + sim2 = Simulation( + blocks=[src2, integ2], + connections=[Connection(src2, integ2)], + dt=0.01, + Solver=GEAR32 + ) + sim2.run(1.0) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim2.save_checkpoint(path) + sim2.load_checkpoint(path) + sim2.run(1.0) + + assert np.allclose(integ2.state, reference, rtol=1e-6) + + +class TestSpectrumCheckpoint: + """Test Spectrum block checkpoint.""" + + def test_spectrum_roundtrip(self): + """Spectrum block state survives checkpoint round-trip.""" + from pathsim.blocks.spectrum import Spectrum + + src = Source(lambda t: np.sin(2 * np.pi * 10 * t)) + spec = Spectrum(freq=[5, 10, 15], t_wait=0.0) + sim = Simulation( + blocks=[src, spec], + connections=[Connection(src, spec)], + dt=0.001 + ) + sim.run(0.1) + + time_before = spec.time + t_sample_before = spec.t_sample + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim.save_checkpoint(path) + + spec.time = 0.0 + spec.t_sample = 0.0 + + sim.load_checkpoint(path) + assert spec.time == pytest.approx(time_before) + assert spec.t_sample == pytest.approx(t_sample_before) + + +class TestScopeCheckpointExtended: + """Extended Scope checkpoint tests for coverage.""" + + def test_scope_with_sampling_period(self): + """Scope with sampling_period preserves _sample_next_timestep.""" + src = Source(lambda t: t) + scope = Scope(sampling_period=0.1) + sim = Simulation( + blocks=[src, scope], + connections=[Connection(src, scope)], + dt=0.01 + ) + sim.run(0.5) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim.save_checkpoint(path) + sim.load_checkpoint(path) + + #verify scope still works after load + sim.run(0.1) + assert len(scope.recording_time) > 0 + + def test_scope_recordings_roundtrip(self): + """Scope recording data round-trips with recordings=True.""" + src = Source(lambda t: t) + scope = Scope() + sim = Simulation( + blocks=[src, scope], + connections=[Connection(src, scope)], + dt=0.1 + ) + sim.run(1.0) + + rec_time = scope.recording_time.copy() + rec_data = [row.copy() for row in scope.recording_data] + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim.save_checkpoint(path, recordings=True) + + #clear recordings + scope.recording_time = [] + scope.recording_data = [] + + sim.load_checkpoint(path) + assert len(scope.recording_time) == len(rec_time) + assert np.allclose(scope.recording_time, rec_time) + + def test_scope_recordings_included_by_default(self): + """Default save_checkpoint includes recordings.""" + src = Source(lambda t: t) + scope = Scope() + sim = Simulation( + blocks=[src, scope], + connections=[Connection(src, scope)], + dt=0.1 + ) + sim.run(1.0) + + rec_time = scope.recording_time.copy() + assert len(rec_time) > 0 + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim.save_checkpoint(path) # no recordings kwarg — default + + #clear recordings + scope.recording_time = [] + scope.recording_data = [] + + sim.load_checkpoint(path) + assert len(scope.recording_time) == len(rec_time) + assert np.allclose(scope.recording_time, rec_time) + + +class TestSimulationCheckpointExtended: + """Extended simulation checkpoint tests for coverage.""" + + def test_save_load_with_extension(self): + """Path with .json extension is handled correctly.""" + src = Source(lambda t: 1.0) + integ = Integrator() + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + dt=0.01 + ) + sim.run(0.1) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp.json") + sim.save_checkpoint(path) + + assert os.path.exists(os.path.join(tmpdir, "cp.json")) + assert os.path.exists(os.path.join(tmpdir, "cp.npz")) + + sim.load_checkpoint(path) + assert sim.time == pytest.approx(0.1, abs=0.01) + + def test_checkpoint_with_events(self): + """Simulation with external events checkpoints correctly.""" + from pathsim.events import Schedule + + src = Source(lambda t: 1.0) + integ = Integrator() + + event_fired = [False] + def on_event(t): + event_fired[0] = True + + evt = Schedule(t_start=0.5, t_period=1.0, func_act=on_event) + + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + events=[evt], + dt=0.01 + ) + sim.run(1.0) + + with tempfile.TemporaryDirectory() as tmpdir: + path = os.path.join(tmpdir, "cp") + sim.save_checkpoint(path) + + #verify events in JSON + with open(f"{path}.json") as f: + data = json.load(f) + assert len(data["events"]) == 1 + assert data["events"][0]["type"] == "Schedule" + + sim.load_checkpoint(path) + + def test_event_numpy_history(self): + """Event with numpy scalar in history serializes correctly.""" + from pathsim.events import ZeroCrossing + + e = ZeroCrossing(func_evt=lambda t: t - 1.0) + e._history = (np.float64(0.5), 0.99) + prefix = "ZeroCrossing_0" + + json_data, npz_data = e.to_checkpoint(prefix) + assert isinstance(json_data["history_eval"], float) + + e.reset() + e.load_checkpoint(prefix, json_data, npz_data) + assert e._history[0] == pytest.approx(0.5) diff --git a/tests/pathsim/test_diagnostics.py b/tests/pathsim/test_diagnostics.py new file mode 100644 index 00000000..9f9514d2 --- /dev/null +++ b/tests/pathsim/test_diagnostics.py @@ -0,0 +1,353 @@ +"""Tests for simulation diagnostics.""" + +import unittest +import numpy as np + +from pathsim import Simulation, Connection +from pathsim.blocks import Source, Integrator, Amplifier, Adder, Scope +from pathsim.utils.diagnostics import Diagnostics, ConvergenceTracker, StepTracker + + +class TestDiagnosticsOff(unittest.TestCase): + """Verify diagnostics=False (default) has no side effects.""" + + def test_diagnostics_none_by_default(self): + src = Source(lambda t: 1.0) + integ = Integrator() + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + dt=0.01 + ) + self.assertIsNone(sim.diagnostics) + sim.run(0.1) + self.assertIsNone(sim.diagnostics) + + +class TestDiagnosticsExplicitSolver(unittest.TestCase): + """Diagnostics with an explicit solver (step errors only).""" + + def test_snapshot_after_run(self): + src = Source(lambda t: np.sin(t)) + integ = Integrator() + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + dt=0.01, + diagnostics=True + ) + sim.run(0.1) + + diag = sim.diagnostics + self.assertIsInstance(diag, Diagnostics) + self.assertAlmostEqual(diag.time, sim.time, places=6) + + #explicit solver: step errors should be populated + self.assertGreater(len(diag.step_errors), 0) + first_key = list(diag.step_errors.keys())[0] + self.assertEqual(first_key.__class__.__name__, "Integrator") + + #no implicit solver or algebraic loops + self.assertEqual(len(diag.solve_residuals), 0) + self.assertEqual(len(diag.loop_residuals), 0) + + def test_worst_block(self): + src = Source(lambda t: 1.0) + i1 = Integrator() + i2 = Integrator() + sim = Simulation( + blocks=[src, i1, i2], + connections=[Connection(src, i1), Connection(i1, i2)], + dt=0.01, + diagnostics=True + ) + sim.run(0.1) + + result = sim.diagnostics.worst_block() + self.assertIsNotNone(result) + label, err = result + self.assertIn("Integrator", label) + + def test_summary_string(self): + src = Source(lambda t: 1.0) + integ = Integrator() + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + dt=0.01, + diagnostics=True + ) + sim.run(0.1) + + summary = sim.diagnostics.summary() + self.assertIn("Diagnostics at t", summary) + self.assertIn("Integrator", summary) + + def test_reset_clears_diagnostics(self): + src = Source(lambda t: 1.0) + integ = Integrator() + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + dt=0.01, + diagnostics=True + ) + sim.run(0.1) + self.assertGreater(sim.diagnostics.time, 0) + + sim.reset() + self.assertEqual(sim.diagnostics.time, 0.0) + + +class TestDiagnosticsAdaptiveSolver(unittest.TestCase): + """Diagnostics with an adaptive solver.""" + + def test_adaptive_step_errors(self): + from pathsim.solvers import RKCK54 + + src = Source(lambda t: np.sin(10 * t)) + integ = Integrator() + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + dt=0.1, + Solver=RKCK54, + tolerance_lte_abs=1e-6, + tolerance_lte_rel=1e-4, + diagnostics=True + ) + sim.run(1.0) + + diag = sim.diagnostics + self.assertIsInstance(diag, Diagnostics) + self.assertGreater(len(diag.step_errors), 0) + + +class TestDiagnosticsImplicitSolver(unittest.TestCase): + """Diagnostics with an implicit solver (solve residuals).""" + + def test_implicit_solve_residuals(self): + from pathsim.solvers import ESDIRK32 + + src = Source(lambda t: np.sin(t)) + integ = Integrator() + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + dt=0.01, + Solver=ESDIRK32, + diagnostics=True + ) + sim.run(0.1) + + diag = sim.diagnostics + self.assertIsInstance(diag, Diagnostics) + self.assertGreater(len(diag.solve_residuals), 0) + self.assertGreater(diag.solve_iterations, 0) + + #worst_block should find the block from solve residuals + result = diag.worst_block() + self.assertIsNotNone(result) + + #summary should include implicit solver section + summary = diag.summary() + self.assertIn("Implicit solver residuals", summary) + + +class TestDiagnosticsAlgebraicLoop(unittest.TestCase): + """Diagnostics with algebraic loops (loop residuals).""" + + def test_algebraic_loop_residuals(self): + src = Source(lambda t: 1.0) + P1 = Adder() + A1 = Amplifier(0.5) + sco = Scope() + + sim = Simulation( + blocks=[src, P1, A1, sco], + connections=[ + Connection(src, P1), + Connection(P1, A1, sco), + Connection(A1, P1[1]), + ], + dt=0.01, + diagnostics=True + ) + + self.assertTrue(sim.graph.has_loops) + sim.run(0.05) + + diag = sim.diagnostics + self.assertGreater(len(diag.loop_residuals), 0) + + result = diag.worst_booster() + self.assertIsNotNone(result) + + #summary should include algebraic loop section + summary = diag.summary() + self.assertIn("Algebraic loop residuals", summary) + + +class TestDiagnosticsHistory(unittest.TestCase): + """Diagnostics history recording.""" + + def test_no_history_by_default(self): + src = Source(lambda t: 1.0) + integ = Integrator() + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + dt=0.01, + diagnostics=True + ) + sim.run(0.1) + + self.assertIsNone(sim._diagnostics_history) + self.assertIsInstance(sim.diagnostics, Diagnostics) + + def test_history_enabled(self): + src = Source(lambda t: 1.0) + integ = Integrator() + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + dt=0.01, + diagnostics="history" + ) + sim.run(0.1) + + #should have ~10 snapshots (0.1s / 0.01 dt) + self.assertGreater(len(sim._diagnostics_history), 5) + + #each snapshot should have a time + times = [s.time for s in sim._diagnostics_history] + self.assertEqual(times, sorted(times)) + + def test_history_reset(self): + src = Source(lambda t: 1.0) + integ = Integrator() + sim = Simulation( + blocks=[src, integ], + connections=[Connection(src, integ)], + dt=0.01, + diagnostics="history" + ) + sim.run(0.05) + self.assertGreater(len(sim._diagnostics_history), 0) + + sim.reset() + self.assertEqual(len(sim._diagnostics_history), 0) + + +class TestDiagnosticsUnit(unittest.TestCase): + """Unit tests for the Diagnostics dataclass.""" + + def test_defaults(self): + d = Diagnostics() + self.assertEqual(d.time, 0.0) + self.assertIsNone(d.worst_block()) + self.assertIsNone(d.worst_booster()) + + def test_worst_block_from_step_errors(self): + + class FakeBlock: + pass + + b1, b2 = FakeBlock(), FakeBlock() + d = Diagnostics(step_errors={b1: (True, 1e-3, 0.9), b2: (True, 5e-3, 0.7)}) + + label, err = d.worst_block() + self.assertAlmostEqual(err, 5e-3) + + def test_worst_block_from_solve_residuals(self): + + class FakeBlock: + pass + + b1, b2 = FakeBlock(), FakeBlock() + d = Diagnostics(solve_residuals={b1: 1e-4, b2: 3e-3}) + + label, err = d.worst_block() + self.assertAlmostEqual(err, 3e-3) + + def test_summary_with_all_data(self): + + class FakeBlock: + pass + + class FakeBooster: + class connection: + def __str__(self): + return "A -> B" + connection = connection() + + b = FakeBlock() + bst = FakeBooster() + d = Diagnostics( + time=1.0, + step_errors={b: (True, 1e-4, 0.9)}, + solve_residuals={b: 1e-8}, + solve_iterations=3, + loop_residuals={bst: 1e-12}, + loop_iterations=2, + ) + + summary = d.summary() + self.assertIn("Diagnostics at t", summary) + self.assertIn("Adaptive step errors", summary) + self.assertIn("Implicit solver residuals", summary) + self.assertIn("Algebraic loop residuals", summary) + + +class TestConvergenceTrackerUnit(unittest.TestCase): + """Unit tests for ConvergenceTracker.""" + + def test_record_and_converge(self): + t = ConvergenceTracker() + t.record("a", 1e-5) + t.record("b", 1e-8) + self.assertAlmostEqual(t.max_error, 1e-5) + self.assertTrue(t.converged(1e-4)) + self.assertFalse(t.converged(1e-6)) + + def test_begin_iteration_clears(self): + t = ConvergenceTracker() + t.record("a", 1.0) + t.begin_iteration() + self.assertEqual(len(t.errors), 0) + self.assertEqual(t.max_error, 0.0) + + def test_details(self): + t = ConvergenceTracker() + t.record("block_a", 1e-3) + t.record("block_b", 2e-4) + lines = t.details(lambda obj: f"name:{obj}") + self.assertEqual(len(lines), 2) + self.assertIn("name:block_a", lines[0]) + + +class TestStepTrackerUnit(unittest.TestCase): + """Unit tests for StepTracker.""" + + def test_record_aggregation(self): + t = StepTracker() + t.record("a", True, 1e-4, 0.9) + t.record("b", False, 2e-3, 0.5) + t.record("c", True, 1e-5, None) + + self.assertFalse(t.success) + self.assertAlmostEqual(t.max_error, 2e-3) + self.assertAlmostEqual(t.scale, 0.5) + + def test_scale_default(self): + t = StepTracker() + t.record("a", True, 0.0, None) + self.assertEqual(t.scale, 1.0) + + def test_reset(self): + t = StepTracker() + t.record("a", False, 1.0, 0.1) + t.reset() + self.assertTrue(t.success) + self.assertEqual(t.max_error, 0.0) + self.assertEqual(len(t.errors), 0) diff --git a/tests/pathsim/test_simulation.py b/tests/pathsim/test_simulation.py index c1752f3e..d370b473 100644 --- a/tests/pathsim/test_simulation.py +++ b/tests/pathsim/test_simulation.py @@ -34,6 +34,10 @@ SIM_ITERATIONS_MAX ) +from pathsim.blocks import Source, Relay +from pathsim.events.schedule import Schedule +from pathsim.events._event import Event + # TESTS ================================================================================ @@ -48,9 +52,9 @@ def test_init_default(self): #test default initialization Sim = Simulation(log=False) - self.assertEqual(Sim.blocks, set()) - self.assertEqual(Sim.connections, set()) - self.assertEqual(Sim.events, set()) + self.assertEqual(Sim.blocks, []) + self.assertEqual(Sim.connections, []) + self.assertEqual(Sim.events, []) self.assertEqual(Sim.dt, SIM_TIMESTEP) self.assertEqual(Sim.dt_min, SIM_TIMESTEP_MIN) self.assertEqual(Sim.dt_max, SIM_TIMESTEP_MAX) @@ -126,12 +130,12 @@ def test_add_block(self): Sim = Simulation(log=False) - self.assertEqual(Sim.blocks, set()) + self.assertEqual(Sim.blocks, []) #test adding a block B1 = Block() Sim.add_block(B1) - self.assertEqual(Sim.blocks, {B1}) + self.assertEqual(Sim.blocks, [B1]) #test adding the same block again with self.assertRaises(ValueError): @@ -149,17 +153,17 @@ def test_add_connection(self): log=False ) - self.assertEqual(Sim.connections, {C1}) + self.assertEqual(Sim.connections, [C1]) #test adding a connection C2 = Connection(B2, B3) Sim.add_connection(C2) - self.assertEqual(Sim.connections, {C1, C2}) + self.assertEqual(Sim.connections, [C1, C2]) #test adding the same connection again with self.assertRaises(ValueError): Sim.add_connection(C2) - self.assertEqual(Sim.connections, {C1, C2}) + self.assertEqual(Sim.connections, [C1, C2]) def test_set_solver(self): @@ -473,6 +477,573 @@ def test_run(self): +class TestSimulationEvents(unittest.TestCase): + """Test event management and event handling during simulation""" + + def test_add_event(self): + """Test adding events to simulation""" + + Sim = Simulation(log=False) + + evt = Event(func_evt=lambda t: t - 1.0, func_act=lambda t: None) + Sim.add_event(evt) + self.assertEqual(len(Sim.events), 1) + self.assertIn(evt, Sim.events) + + #adding same event again raises ValueError + with self.assertRaises(ValueError): + Sim.add_event(evt) + + + def test_contains_event(self): + """Test __contains__ for events""" + + evt = Event(func_evt=lambda t: t - 1.0) + evt2 = Event(func_evt=lambda t: t - 2.0) + + B1, B2 = Block(), Block() + C1 = Connection(B1, B2) + Sim = Simulation( + blocks=[B1, B2], + connections=[C1], + events=[evt], + log=False + ) + + self.assertIn(evt, Sim) + self.assertNotIn(evt2, Sim) + + + def test_run_with_schedule_event(self): + """Test simulation with schedule event covering event system paths""" + + #simple system: integrator with source + Src = Source(lambda t: 1.0) + Int = Integrator(0.0) + Sco = Scope(labels=["output"]) + + #schedule event that fires periodically + counter = [0] + def count_action(t): + counter[0] += 1 + + evt = Schedule(t_start=0.0, t_period=0.5, func_act=count_action) + + Sim = Simulation( + blocks=[Src, Int, Sco], + connections=[ + Connection(Src, Int), + Connection(Int, Sco) + ], + events=[evt], + dt=0.01, + log=False + ) + + Sim.run(duration=2.0, reset=True) + + #schedule should have fired multiple times + self.assertGreater(counter[0], 0) + self.assertAlmostEqual(Sim.time, 2.0, 2) + + + def test_run_with_relay_block(self): + """Test simulation with Relay block that has internal events""" + + #ramp source crosses relay thresholds + Src = Source(lambda t: t - 1.0) # crosses 0 at t=1 + Rly = Relay(threshold_up=0.5, threshold_down=-0.5, value_up=10.0, value_down=-10.0) + Sco = Scope(labels=["relay"]) + + Sim = Simulation( + blocks=[Src, Rly, Sco], + connections=[ + Connection(Src, Rly), + Connection(Rly, Sco) + ], + dt=0.01, + log=False + ) + + Sim.run(duration=3.0, reset=True) + self.assertAlmostEqual(Sim.time, 3.0, 1) + + + def test_reset_with_events(self): + """Test that reset clears event state""" + + counter = [0] + evt = Schedule(t_start=0, t_period=1.0, func_act=lambda t: None) + + B1 = Block() + Sim = Simulation(blocks=[B1], events=[evt], log=False) + + Sim.run(duration=3.0) + events_before = len(evt) + + #reset should clear event times + Sim.reset() + self.assertEqual(len(evt), 0) + self.assertEqual(Sim.time, 0.0) + + +class TestSimulationAdvanced(unittest.TestCase): + """Test advanced simulation features""" + + def setUp(self): + """Set up a simple feedback system for reuse""" + self.Int = Integrator(1.0) + self.Amp = Amplifier(-1) + self.Add = Adder() + self.Sco = Scope(labels=["response"]) + + blocks = [self.Int, self.Amp, self.Add, self.Sco] + connections = [ + Connection(self.Amp, self.Add[1]), + Connection(self.Add, self.Int), + Connection(self.Int, self.Amp, self.Sco) + ] + + self.Sim = Simulation(blocks, connections, dt=0.02, log=False) + + + def test_linearize_delinearize(self): + """Test linearize and delinearize methods""" + + self.Sim.reset() + + #linearize should not raise + self.Sim.linearize() + + #run a few steps with linearized system + self.Sim.timestep() + self.Sim.timestep() + + #delinearize should not raise + self.Sim.delinearize() + + #system should still work after delinearization + self.Sim.timestep() + + + def test_steadystate(self): + """Test steady state solving""" + + self.Sim.reset() + + #for dx/dt = -x, steady state is x=0 + self.Sim.steadystate(reset=True) + + #state should be close to zero (steady state of dx/dt = -x) + self.assertAlmostEqual(self.Int.outputs[0], 0.0, 4) + + + def test_run_adaptive(self): + """Test run with adaptive explicit solver""" + + from pathsim.solvers import RKCK54 + + self.Sim._set_solver(RKCK54) + self.Sim.reset() + + #run with adaptive stepping + stats = self.Sim.run(duration=2.0, reset=True, adaptive=True) + + self.assertAlmostEqual(self.Sim.time, 2.0, 2) + + #solution should still decay + time, data = self.Sco.read() + self.assertTrue(np.all(np.diff(data) < 0.0)) + + + def test_run_adaptive_with_events(self): + """Test adaptive solver with scheduled events to cover event estimation""" + + from pathsim.solvers import RKCK54 + + self.Sim._set_solver(RKCK54) + + #add schedule event + counter = [0] + evt = Schedule(t_start=0.5, t_period=0.5, func_act=lambda t: counter[0].__add__(1) or None) + self.Sim.add_event(evt) + + self.Sim.run(duration=2.0, reset=True, adaptive=True) + self.assertAlmostEqual(self.Sim.time, 2.0, 2) + + + def test_run_adaptive_implicit(self): + """Test run with adaptive implicit solver""" + + from pathsim.solvers import ESDIRK32 + + self.Sim._set_solver(ESDIRK32) + self.Sim.reset() + + stats = self.Sim.run(duration=2.0, reset=True, adaptive=True) + self.assertAlmostEqual(self.Sim.time, 2.0, 2) + + + def test_run_streaming(self): + """Test run_streaming generator""" + + self.Sim.reset() + + results = [] + for result in self.Sim.run_streaming( + duration=1.0, + reset=True, + tickrate=100, + func_callback=lambda: self.Sim.time + ): + results.append(result) + + #should have yielded at least one result + self.assertGreater(len(results), 0) + + #last result should be close to end time + self.assertAlmostEqual(self.Sim.time, 1.0, 2) + + + def test_run_streaming_no_callback(self): + """Test run_streaming without callback returns None""" + + self.Sim.reset() + + results = [] + for result in self.Sim.run_streaming( + duration=0.5, + reset=True, + tickrate=100 + ): + results.append(result) + + #without callback, results should be None + for r in results: + self.assertIsNone(r) + + + def test_collect(self): + """Test deprecated collect method""" + + self.Sim.run(duration=1.0, reset=True) + + import warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + result = self.Sim.collect() + + #should have scopes key with data + self.assertIn("scopes", result) + self.assertIn("spectra", result) + self.assertGreater(len(result["scopes"]), 0) + + + def test_stop_interrupts_run(self): + """Test that stop() interrupts an active run""" + + #use schedule event to stop simulation at t=0.5 + def stop_action(t): + self.Sim.stop() + + evt = Schedule(t_start=0.5, t_period=100, func_act=stop_action) + self.Sim.add_event(evt) + self.Sim.run(duration=5.0, reset=True) + + #simulation should have stopped early + self.assertLess(self.Sim.time, 5.0) + + + def test_deprecated_timestep_methods(self): + """Test deprecated timestep methods still work""" + + self.Sim.reset() + + import warnings + with warnings.catch_warnings(): + warnings.simplefilter("ignore", DeprecationWarning) + + result = self.Sim.timestep_fixed_explicit(dt=0.01) + self.assertEqual(len(result), 5) + + result = self.Sim.timestep_fixed_implicit(dt=0.01) + self.assertEqual(len(result), 5) + + result = self.Sim.timestep_adaptive_explicit(dt=0.01) + self.assertEqual(len(result), 5) + + result = self.Sim.timestep_adaptive_implicit(dt=0.01) + self.assertEqual(len(result), 5) + + + def test_run_realtime(self): + """Test run_realtime generator""" + + self.Sim.reset() + + results = [] + for result in self.Sim.run_realtime( + duration=0.2, + reset=True, + tickrate=50, + speed=100.0, #run 100x faster than real time + func_callback=lambda: self.Sim.time + ): + results.append(result) + + #should have yielded results + self.assertGreater(len(results), 0) + self.assertAlmostEqual(self.Sim.time, 0.2, 1) + + +class TestSimulationRuntimeMutation(unittest.TestCase): + """Test runtime mutation of simulation components (add/remove blocks, + connections, events) and lazy graph rebuild via _graph_dirty flag.""" + + def setUp(self): + """Set up a simple system: Src -> Int -> Sco""" + self.Src = Source(lambda t: 1.0) + self.Int = Integrator(0.0) + self.Sco = Scope(labels=["output"]) + + self.C1 = Connection(self.Src, self.Int) + self.C2 = Connection(self.Int, self.Sco) + + self.Sim = Simulation( + blocks=[self.Src, self.Int, self.Sco], + connections=[self.C1, self.C2], + dt=0.01, + log=False + ) + + def test_graph_dirty_after_add_block(self): + """Adding a block after init marks graph dirty""" + self.assertFalse(self.Sim._graph_dirty) + + B = Block() + self.Sim.add_block(B) + + self.assertTrue(self.Sim._graph_dirty) + self.assertIn(B, self.Sim.blocks) + + def test_graph_dirty_after_remove_block(self): + """Removing a block marks graph dirty""" + B = Block() + self.Sim.add_block(B) + + # Clear dirty flag by stepping + self.Sim.step(0.01) + self.assertFalse(self.Sim._graph_dirty) + + self.Sim.remove_block(B) + self.assertTrue(self.Sim._graph_dirty) + self.assertNotIn(B, self.Sim.blocks) + + def test_graph_dirty_after_add_connection(self): + """Adding a connection marks graph dirty""" + B = Block() + self.Sim.add_block(B) + + # Clear dirty flag + self.Sim.step(0.01) + self.assertFalse(self.Sim._graph_dirty) + + C = Connection(self.Sco, B) + self.Sim.add_connection(C) + self.assertTrue(self.Sim._graph_dirty) + + def test_graph_dirty_after_remove_connection(self): + """Removing a connection marks graph dirty""" + self.Sim.step(0.01) + self.assertFalse(self.Sim._graph_dirty) + + self.Sim.remove_connection(self.C2) + self.assertTrue(self.Sim._graph_dirty) + self.assertNotIn(self.C2, self.Sim.connections) + + def test_lazy_rebuild_on_update(self): + """Graph is rebuilt lazily when _update is called""" + B = Block() + self.Sim.add_block(B) + self.assertTrue(self.Sim._graph_dirty) + + # step triggers _update which should rebuild graph + self.Sim.step(0.01) + self.assertFalse(self.Sim._graph_dirty) + + def test_remove_block_error(self): + """Removing a block not in simulation raises ValueError""" + B = Block() + with self.assertRaises(ValueError): + self.Sim.remove_block(B) + + def test_remove_connection_error(self): + """Removing a connection not in simulation raises ValueError""" + B1, B2 = Block(), Block() + C = Connection(B1, B2) + with self.assertRaises(ValueError): + self.Sim.remove_connection(C) + + def test_remove_connection_zeroes_inputs(self): + """Removing a connection zeroes target inputs on next graph rebuild""" + # Run a step so the connection pushes data + self.Src.function = lambda t: 5.0 + self.Sim.step(0.01) + + # Int should have received input from Src + self.assertNotEqual(self.Int.inputs[0], 0.0) + + # Remove the connection — graph is dirty but not rebuilt yet + self.Sim.remove_connection(self.C1) + self.assertTrue(self.Sim._graph_dirty) + + # Next step triggers graph rebuild which resets inputs + self.Sim.step(0.01) + self.assertEqual(self.Int.inputs[0], 0.0) + + def test_remove_event(self): + """Adding and removing events works""" + evt = Event(func_evt=lambda t: t - 1.0, func_act=lambda t: None) + self.Sim.add_event(evt) + self.assertIn(evt, self.Sim.events) + + self.Sim.remove_event(evt) + self.assertNotIn(evt, self.Sim.events) + + def test_remove_event_error(self): + """Removing an event not in simulation raises ValueError""" + evt = Event(func_evt=lambda t: t - 1.0, func_act=lambda t: None) + with self.assertRaises(ValueError): + self.Sim.remove_event(evt) + + def test_add_block_during_run(self): + """Add a block mid-simulation and continue running""" + # Run for a bit + self.Sim.run(duration=0.5, reset=True) + t_before = self.Sim.time + + # Add a new block + Amp = Amplifier(2) + self.Sim.add_block(Amp) + self.assertTrue(self.Sim._graph_dirty) + + # Continue running - graph should rebuild and work + self.Sim.run(duration=0.5, reset=False) + self.assertGreater(self.Sim.time, t_before) + + def test_remove_block_during_run(self): + """Remove a block mid-simulation and continue running""" + # Run for a bit + self.Sim.run(duration=0.5, reset=True) + + # Remove scope (leaf node, safe to remove) + self.Sim.remove_connection(self.C2) + self.Sim.remove_block(self.Sco) + + # Continue running + self.Sim.run(duration=0.5, reset=False) + self.assertAlmostEqual(self.Sim.time, 1.0, 1) + + def test_add_connection_during_run(self): + """Add a connection mid-simulation and continue""" + Sco2 = Scope(labels=["extra"]) + self.Sim.add_block(Sco2) + + self.Sim.run(duration=0.5, reset=True) + + C_new = Connection(self.Int, Sco2) + self.Sim.add_connection(C_new) + + self.Sim.run(duration=0.5, reset=False) + self.assertAlmostEqual(self.Sim.time, 1.0, 1) + + # Sco2 should have recorded data from the second half + time, data = Sco2.read() + self.assertGreater(len(time), 0) + + def test_remove_connection_during_run(self): + """Remove a connection mid-simulation and continue""" + self.Sim.run(duration=0.5, reset=True) + + self.Sim.remove_connection(self.C2) + + self.Sim.run(duration=0.5, reset=False) + self.assertAlmostEqual(self.Sim.time, 1.0, 1) + + def test_multiple_mutations_single_rebuild(self): + """Multiple mutations only trigger one rebuild""" + B1, B2 = Block(), Block() + C = Connection(B1, B2) + + self.Sim.add_block(B1) + self.Sim.add_block(B2) + self.Sim.add_connection(C) + + # Should still be dirty (not rebuilt yet) + self.assertTrue(self.Sim._graph_dirty) + + # Single step triggers single rebuild + self.Sim.step(0.01) + self.assertFalse(self.Sim._graph_dirty) + + def test_dynamic_block_tracking(self): + """Dynamically added integrators get solver and are tracked""" + new_int = Integrator(0.0) + self.Sim.add_block(new_int) + + # Should have been given a solver + self.assertIsNotNone(new_int.engine) + self.assertIn(new_int, self.Sim._blocks_dyn) + + def test_remove_dynamic_block_tracking(self): + """Removing a dynamic block removes it from _blocks_dyn""" + new_int = Integrator(0.0) + self.Sim.add_block(new_int) + self.assertIn(new_int, self.Sim._blocks_dyn) + + self.Sim.remove_block(new_int) + self.assertNotIn(new_int, self.Sim._blocks_dyn) + + def test_streaming_with_mutations(self): + """Add blocks between streaming generator steps using run_streaming""" + # Use a longer duration + high tickrate to get many yields + gen = self.Sim.run_streaming( + duration=10.0, reset=True, tickrate=1000 + ) + + # Consume a few ticks + first_result = next(gen) + + # Mutate mid-stream: add a new scope + Sco2 = Scope(labels=["live"]) + self.Sim.add_block(Sco2) + C_new = Connection(self.Int, Sco2) + self.Sim.add_connection(C_new) + + # Continue streaming — should rebuild graph and keep going + results = list(gen) + self.assertGreater(len(results), 0) + + # New scope should have data from after connection was added + time_data, data = Sco2.read() + self.assertGreater(len(time_data), 0) + + def test_parameter_mutation_during_run(self): + """Change block parameters mid-simulation""" + self.Sim.run(duration=0.5, reset=True) + + # Change amplifier gain-equivalent by modifying source + # Integrator initial value is immutable at runtime, but we can + # change the source function + self.Src.function = lambda t: 2.0 # double the input + + self.Sim.run(duration=0.5, reset=False) + self.assertAlmostEqual(self.Sim.time, 1.0, 1) + + # Integration result should reflect the change + time, data = self.Sco.read() + self.assertGreater(len(time), 0) + # RUN TESTS LOCALLY ==================================================================== diff --git a/tests/pathsim/test_subsystem.py b/tests/pathsim/test_subsystem.py index db6bf1fd..64b89f35 100644 --- a/tests/pathsim/test_subsystem.py +++ b/tests/pathsim/test_subsystem.py @@ -411,6 +411,188 @@ def test_graph(self): pass def test_nesting(self): pass +class TestSubsystemRuntimeMutation(unittest.TestCase): + """Test runtime mutation of subsystem components (add/remove blocks, + connections, events) and lazy graph rebuild via _graph_dirty flag.""" + + def setUp(self): + """Set up a subsystem: I1 -> B1 -> B2 -> I1""" + self.I1 = Interface() + self.B1 = Block() + self.B2 = Block() + self.C1 = Connection(self.I1, self.B1) + self.C2 = Connection(self.B1, self.B2) + self.C3 = Connection(self.B2, self.I1) + + self.S = Subsystem( + blocks=[self.I1, self.B1, self.B2], + connections=[self.C1, self.C2, self.C3] + ) + + def test_graph_dirty_after_add_block(self): + """Adding a block marks graph dirty""" + self.assertFalse(self.S._graph_dirty) + + B = Block() + self.S.add_block(B) + + self.assertTrue(self.S._graph_dirty) + self.assertIn(B, self.S.blocks) + + def test_graph_dirty_after_remove_block(self): + """Removing a block marks graph dirty""" + # Clear dirty flag by calling update + self.S.update(0.0) + self.assertFalse(self.S._graph_dirty) + + self.S.remove_block(self.B2) + self.assertTrue(self.S._graph_dirty) + self.assertNotIn(self.B2, self.S.blocks) + + def test_graph_dirty_after_add_connection(self): + """Adding a connection marks graph dirty""" + B3 = Block() + self.S.add_block(B3) + self.S.update(0.0) + self.assertFalse(self.S._graph_dirty) + + C = Connection(self.B2, B3) + self.S.add_connection(C) + self.assertTrue(self.S._graph_dirty) + + def test_graph_dirty_after_remove_connection(self): + """Removing a connection marks graph dirty""" + self.S.update(0.0) + self.assertFalse(self.S._graph_dirty) + + self.S.remove_connection(self.C3) + self.assertTrue(self.S._graph_dirty) + self.assertNotIn(self.C3, self.S.connections) + + def test_lazy_rebuild_on_update(self): + """Graph is rebuilt lazily when update is called""" + B = Block() + self.S.add_block(B) + self.assertTrue(self.S._graph_dirty) + + self.S.update(0.0) + self.assertFalse(self.S._graph_dirty) + + def test_remove_block_error(self): + """Removing a block not in subsystem raises ValueError""" + B = Block() + with self.assertRaises(ValueError): + self.S.remove_block(B) + + def test_remove_connection_error(self): + """Removing a connection not in subsystem raises ValueError""" + B1, B2 = Block(), Block() + C = Connection(B1, B2) + with self.assertRaises(ValueError): + self.S.remove_connection(C) + + def test_add_remove_event(self): + """Adding and removing events works""" + from pathsim.events._event import Event + + evt = Event(func_evt=lambda t: t - 1.0, func_act=lambda t: None) + self.S.add_event(evt) + self.assertIn(evt, self.S._events) + + self.S.remove_event(evt) + self.assertNotIn(evt, self.S._events) + + def test_add_event_duplicate_error(self): + """Adding duplicate event raises ValueError""" + from pathsim.events._event import Event + + evt = Event(func_evt=lambda t: t - 1.0) + self.S.add_event(evt) + + with self.assertRaises(ValueError): + self.S.add_event(evt) + + def test_remove_event_error(self): + """Removing event not in subsystem raises ValueError""" + from pathsim.events._event import Event + + evt = Event(func_evt=lambda t: t - 1.0) + with self.assertRaises(ValueError): + self.S.remove_event(evt) + + def test_multiple_mutations_single_rebuild(self): + """Multiple mutations only trigger one rebuild""" + B3, B4 = Block(), Block() + C = Connection(B3, B4) + + self.S.add_block(B3) + self.S.add_block(B4) + self.S.add_connection(C) + + # Still dirty — no rebuild yet + self.assertTrue(self.S._graph_dirty) + + # Single update clears it + self.S.update(0.0) + self.assertFalse(self.S._graph_dirty) + + def test_dynamic_block_with_solver(self): + """Dynamically added blocks get solver if subsystem has one""" + from pathsim.blocks import Integrator + from pathsim.solvers import EUF + + self.S.set_solver(EUF, None) + + new_int = Integrator(0.0) + self.S.add_block(new_int) + + # Should have been given a solver + self.assertIsNotNone(new_int.engine) + self.assertIn(new_int, self.S._blocks_dyn) + + def test_remove_dynamic_block_tracking(self): + """Removing a dynamic block removes it from _blocks_dyn""" + from pathsim.blocks import Integrator + from pathsim.solvers import EUF + + self.S.set_solver(EUF, None) + + new_int = Integrator(0.0) + self.S.add_block(new_int) + self.assertIn(new_int, self.S._blocks_dyn) + + self.S.remove_block(new_int) + self.assertNotIn(new_int, self.S._blocks_dyn) + + def test_mutation_in_simulation_context(self): + """Subsystem with mutations works inside a Simulation""" + from pathsim.simulation import Simulation + from pathsim.blocks import Source, Scope + + Src = Source(lambda t: 1.0) + Sco = Scope() + + C_in = Connection(Src, self.S) + C_out = Connection(self.S, Sco) + + Sim = Simulation( + blocks=[Src, self.S, Sco], + connections=[C_in, C_out], + dt=0.01, + log=False + ) + + # Run a bit + Sim.run(duration=0.2, reset=True) + + # Add a block to the subsystem + B_new = Block() + self.S.add_block(B_new) + self.assertTrue(self.S._graph_dirty) + + # Continue running — subsystem graph rebuilds internally + Sim.run(duration=0.2, reset=False) + self.assertAlmostEqual(Sim.time, 0.4, 1) # RUN TESTS LOCALLY ==================================================================== diff --git a/tests/pathsim/utils/test_mutable.py b/tests/pathsim/utils/test_mutable.py new file mode 100644 index 00000000..e43d36fd --- /dev/null +++ b/tests/pathsim/utils/test_mutable.py @@ -0,0 +1,267 @@ +######################################################################################## +## +## TESTS FOR +## 'utils.mutable.py' +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim.blocks._block import Block +from pathsim.blocks.lti import StateSpace +from pathsim.blocks.ctrl import PT1, PT2, LeadLag, PID, AntiWindupPID +from pathsim.blocks.lti import TransferFunctionNumDen, TransferFunctionZPG +from pathsim.blocks.filters import ButterworthLowpassFilter +from pathsim.blocks.sources import SinusoidalSource, ClockSource +from pathsim.blocks.delay import Delay +from pathsim.blocks.fir import FIR +from pathsim.blocks.samplehold import SampleHold + +from pathsim.utils.mutable import mutable + + +# TESTS FOR DECORATOR ================================================================== + +class TestMutableDecorator(unittest.TestCase): + """Test the @mutable decorator mechanics.""" + + def test_basic_construction(self): + """Block construction should work as before.""" + pt1 = PT1(K=2.0, T=0.5) + self.assertEqual(pt1.K, 2.0) + self.assertEqual(pt1.T, 0.5) + np.testing.assert_array_almost_equal(pt1.A, [[-2.0]]) + np.testing.assert_array_almost_equal(pt1.B, [[4.0]]) + + def test_param_mutation_triggers_reinit(self): + """Changing a mutable param should update derived state.""" + pt1 = PT1(K=1.0, T=1.0) + np.testing.assert_array_almost_equal(pt1.A, [[-1.0]]) + np.testing.assert_array_almost_equal(pt1.B, [[1.0]]) + + pt1.K = 5.0 + np.testing.assert_array_almost_equal(pt1.A, [[-1.0]]) + np.testing.assert_array_almost_equal(pt1.B, [[5.0]]) + + pt1.T = 0.5 + np.testing.assert_array_almost_equal(pt1.A, [[-2.0]]) + np.testing.assert_array_almost_equal(pt1.B, [[10.0]]) + + def test_batched_set(self): + """set() should update multiple params with a single reinit.""" + pt1 = PT1(K=1.0, T=1.0) + pt1.set(K=3.0, T=0.2) + self.assertEqual(pt1.K, 3.0) + self.assertEqual(pt1.T, 0.2) + np.testing.assert_array_almost_equal(pt1.A, [[-5.0]]) + np.testing.assert_array_almost_equal(pt1.B, [[15.0]]) + + def test_mutable_params_introspection(self): + """_mutable_params should list all init params.""" + self.assertEqual(PT1._mutable_params, ("K", "T")) + self.assertEqual(PT2._mutable_params, ("K", "T", "d")) + self.assertEqual(PID._mutable_params, ("Kp", "Ki", "Kd", "f_max")) + + def test_mutable_params_inherited(self): + """AntiWindupPID should accumulate parent and own params.""" + self.assertIn("Kp", AntiWindupPID._mutable_params) + self.assertIn("Ks", AntiWindupPID._mutable_params) + self.assertIn("limits", AntiWindupPID._mutable_params) + # no duplicates + self.assertEqual( + len(AntiWindupPID._mutable_params), + len(set(AntiWindupPID._mutable_params)) + ) + + def test_no_reinit_during_construction(self): + """Properties should not trigger reinit during __init__.""" + # If this doesn't hang or error, the init guard works + pt1 = PT1(K=2.0, T=0.5) + self.assertTrue(pt1._param_locked) + + +# TESTS FOR ENGINE PRESERVATION ========================================================= + +class TestEnginePreservation(unittest.TestCase): + """Test that engine state is preserved across reinit.""" + + def test_engine_preserved_same_dimension(self): + """Engine should be preserved when state dimension doesn't change.""" + from pathsim.solvers.euler import EUF + + pt1 = PT1(K=1.0, T=1.0) + pt1.set_solver(EUF, None) + pt1.engine.state = np.array([42.0]) + + # Mutate parameter + pt1.K = 5.0 + + # Engine should be preserved with same state + self.assertIsNotNone(pt1.engine) + np.testing.assert_array_equal(pt1.engine.state, [42.0]) + + def test_engine_recreated_on_dimension_change(self): + """Engine should be recreated when state dimension changes.""" + from pathsim.solvers.euler import EUF + + filt = ButterworthLowpassFilter(Fc=100, n=2) + filt.set_solver(EUF, None) + + old_state_dim = len(filt.engine) + self.assertEqual(old_state_dim, 2) + + # Change filter order -> dimension change + filt.n = 4 + + # Engine should exist but with new dimension + self.assertIsNotNone(filt.engine) + self.assertEqual(len(filt.engine), 4) + + +# TESTS FOR INHERITANCE ================================================================= + +class TestInheritance(unittest.TestCase): + """Test that @mutable works with class hierarchies.""" + + def test_antiwinduppid_construction(self): + """AntiWindupPID should construct correctly with both decorators.""" + awpid = AntiWindupPID(Kp=2, Ki=0.5, Kd=0.1, f_max=1e3, Ks=10, limits=[-5, 5]) + self.assertEqual(awpid.Kp, 2) + self.assertEqual(awpid.Ks, 10) + + def test_antiwinduppid_parent_param_mutation(self): + """Mutating inherited param should reinit from most derived class.""" + awpid = AntiWindupPID(Kp=2, Ki=0.5, Kd=0.1, f_max=1e3, Ks=10, limits=[-5, 5]) + + # Mutate inherited param + awpid.Kp = 5.0 + + # op_dyn should still be the antiwindup version (not plain PID) + x = np.array([0.0, 0.0]) + u = np.array([1.0]) + result = awpid.op_dyn(x, u, 0) + # For AntiWindupPID with these params, dx1 = f_max*(u-x1), dx2 = u - w + self.assertEqual(len(result), 2) + + def test_antiwinduppid_own_param_mutation(self): + """Mutating AntiWindupPID's own param should work.""" + awpid = AntiWindupPID(Kp=2, Ki=0.5, Kd=0.1, f_max=1e3, Ks=10, limits=[-5, 5]) + awpid.Ks = 20 + self.assertEqual(awpid.Ks, 20) + + +# TESTS FOR SPECIFIC BLOCKS ============================================================= + +class TestSpecificBlocks(unittest.TestCase): + """Test @mutable on various block types.""" + + def test_pt2(self): + pt2 = PT2(K=1.0, T=1.0, d=0.5) + A_before = pt2.A.copy() + pt2.d = 0.7 + # A matrix should have changed + self.assertFalse(np.allclose(pt2.A, A_before)) + + def test_leadlag(self): + ll = LeadLag(K=1.0, T1=0.5, T2=0.1) + ll.K = 2.0 + self.assertEqual(ll.K, 2.0) + # C and D should reflect new K + np.testing.assert_array_almost_equal(ll.D, [[2.0 * 0.5 / 0.1]]) + + def test_transfer_function_numden(self): + tf = TransferFunctionNumDen(Num=[1], Den=[1, 1]) + np.testing.assert_array_almost_equal(tf.A, [[-1.0]]) + tf.Den = [1, 2] + np.testing.assert_array_almost_equal(tf.A, [[-2.0]]) + + def test_transfer_function_dimension_change(self): + """Changing denominator order should change state dimension.""" + tf = TransferFunctionNumDen(Num=[1], Den=[1, 1]) + self.assertEqual(tf.A.shape, (1, 1)) + tf.Den = [1, 3, 2] # second order + self.assertEqual(tf.A.shape, (2, 2)) + + def test_sinusoidal_source(self): + s = SinusoidalSource(frequency=10, amplitude=2, phase=0.5) + self.assertAlmostEqual(s._omega, 2*np.pi*10) + s.frequency = 20 + self.assertAlmostEqual(s._omega, 2*np.pi*20) + + def test_delay(self): + d = Delay(tau=0.01) + self.assertEqual(d._buffer.delay, 0.01) + d.tau = 0.05 + self.assertEqual(d._buffer.delay, 0.05) + + def test_clock_source(self): + c = ClockSource(T=1.0, tau=0.0) + self.assertEqual(c.events[0].t_period, 1.0) + c.T = 2.0 + self.assertEqual(c.events[0].t_period, 2.0) + + def test_fir(self): + f = FIR(coeffs=[0.5, 0.5], T=0.1) + self.assertEqual(f.T, 0.1) + f.T = 0.2 + self.assertEqual(f.T, 0.2) + self.assertEqual(f.events[0].t_period, 0.2) + + def test_samplehold(self): + sh = SampleHold(T=0.5, tau=0.0) + sh.T = 1.0 + self.assertEqual(sh.T, 1.0) + + def test_butterworth_filter_mutation(self): + filt = ButterworthLowpassFilter(Fc=100, n=2) + A_before = filt.A.copy() + filt.Fc = 200 + # Matrices should change + self.assertFalse(np.allclose(filt.A, A_before)) + + def test_butterworth_filter_order_change(self): + filt = ButterworthLowpassFilter(Fc=100, n=2) + self.assertEqual(filt.A.shape, (2, 2)) + filt.n = 4 + self.assertEqual(filt.A.shape, (4, 4)) + + +# INTEGRATION TEST ====================================================================== + +class TestMutableInSimulation(unittest.TestCase): + """Test parameter mutation in an actual simulation context.""" + + def test_pt1_mutation_mid_simulation(self): + """Mutating PT1 gain mid-simulation should affect output.""" + from pathsim import Simulation, Connection + from pathsim.blocks.sources import Constant + + src = Constant(value=1.0) + pt1 = PT1(K=1.0, T=0.1) + + sim = Simulation( + blocks=[src, pt1], + connections=[Connection(src, pt1)], + dt=0.01 + ) + + # Run for a bit + sim.run(duration=1.0) + output_before = pt1.outputs[0] + + # Change gain + pt1.K = 5.0 + + # Run more + sim.run(duration=1.0) + output_after = pt1.outputs[0] + + # With K=5 and enough settling time, output should approach 5.0 + self.assertGreater(output_after, output_before) + + +if __name__ == "__main__": + unittest.main()