From 1844799875c586d782f696de27ff1238569512df Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 22 Jan 2026 08:55:14 +0100 Subject: [PATCH 01/60] cleanup docs and docstrings, fix typos, standardize notebook intros and math formatting --- docs/source/examples/algebraic_loop.ipynb | 8 +--- docs/source/examples/billards.ipynb | 10 +---- docs/source/examples/bouncing_ball.ipynb | 10 +---- docs/source/examples/cascade_controller.ipynb | 10 +---- docs/source/examples/chemical_reactor.ipynb | 41 +------------------ .../source/examples/coupled_oscillators.ipynb | 10 +---- docs/source/examples/delta_sigma_adc.ipynb | 9 +--- docs/source/examples/diode_circuit.ipynb | 27 +----------- docs/source/examples/elastic_pendulum.ipynb | 8 +--- docs/source/examples/fmu_cosimulation.ipynb | 26 +----------- .../source/examples/harmonic_oscillator.ipynb | 8 +--- docs/source/examples/linear_feedback.ipynb | 10 +---- docs/source/examples/lorenz_attractor.ipynb | 29 +------------ docs/source/examples/nested_subsystems.ipynb | 28 +------------ docs/source/examples/noisy_amplifier.ipynb | 6 +-- docs/source/examples/pendulum.ipynb | 10 +---- docs/source/examples/pid_controller.ipynb | 10 +---- docs/source/examples/poincare_maps.ipynb | 31 +------------- docs/source/examples/rf_network_oneport.ipynb | 6 +-- docs/source/examples/sar_adc.ipynb | 9 +--- docs/source/examples/stick_slip.ipynb | 8 +--- docs/source/examples/thermostat.ipynb | 17 +------- docs/source/examples/vanderpol.ipynb | 23 ++--------- src/pathsim/blocks/_block.py | 10 ++--- src/pathsim/blocks/ctrl.py | 8 ++-- src/pathsim/blocks/dynsys.py | 8 ++-- src/pathsim/blocks/function.py | 2 +- src/pathsim/blocks/integrator.py | 6 +-- src/pathsim/blocks/lti.py | 24 +++++------ src/pathsim/blocks/ode.py | 8 ++-- 30 files changed, 76 insertions(+), 344 deletions(-) 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/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/src/pathsim/blocks/_block.py b/src/pathsim/blocks/_block.py index 3716d895..bcdb8346 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 @@ -108,7 +108,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. diff --git a/src/pathsim/blocks/ctrl.py b/src/pathsim/blocks/ctrl.py index b27edba7..4e24cefc 100644 --- a/src/pathsim/blocks/ctrl.py +++ b/src/pathsim/blocks/ctrl.py @@ -195,10 +195,10 @@ class AntiWindupPID(PID): .. 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) 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/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/lti.py b/src/pathsim/blocks/lti.py index 25d64c1d..de85e99c 100644 --- a/src/pathsim/blocks/lti.py +++ b/src/pathsim/blocks/lti.py @@ -31,10 +31,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. @@ -190,10 +190,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 @@ -247,10 +247,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 @@ -300,10 +300,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/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 From e655db5e075a3dc03d10892f9f6f37e6fb9012c0 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 22 Jan 2026 09:05:12 +0100 Subject: [PATCH 02/60] fix typos and math errors in docstrings --- src/pathsim/_constants.py | 2 +- src/pathsim/blocks/_block.py | 4 ++-- src/pathsim/blocks/counter.py | 2 +- src/pathsim/connection.py | 2 +- src/pathsim/events/_event.py | 2 +- src/pathsim/subsystem.py | 4 ++-- src/pathsim/utils/adaptivebuffer.py | 6 +++--- src/pathsim/utils/gilbert.py | 4 ++-- src/pathsim/utils/portreference.py | 4 ++-- 9 files changed, 15 insertions(+), 15 deletions(-) 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/_block.py b/src/pathsim/blocks/_block.py index bcdb8346..fa559d0d 100644 --- a/src/pathsim/blocks/_block.py +++ b/src/pathsim/blocks/_block.py @@ -323,7 +323,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 +418,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. 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/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..fd911a5b 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. diff --git a/src/pathsim/subsystem.py b/src/pathsim/subsystem.py index 3ec211dc..00a7047e 100644 --- a/src/pathsim/subsystem.py +++ b/src/pathsim/subsystem.py @@ -354,8 +354,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. diff --git a/src/pathsim/utils/adaptivebuffer.py b/src/pathsim/utils/adaptivebuffer.py index fe6c5755..b24e2e5a 100644 --- a/src/pathsim/utils/adaptivebuffer.py +++ b/src/pathsim/utils/adaptivebuffer.py @@ -34,7 +34,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 +44,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 +67,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() 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/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) From 13919b61c5a10dba71c5a7bcc80325aa4e29b11e Mon Sep 17 00:00:00 2001 From: Milan Rother <105657697+milanofthe@users.noreply.github.com> Date: Mon, 26 Jan 2026 07:39:38 +0100 Subject: [PATCH 03/60] Add reset for internal events in block Reset internal events in the block during reset. --- src/pathsim/blocks/_block.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pathsim/blocks/_block.py b/src/pathsim/blocks/_block.py index fa559d0d..c291cae8 100644 --- a/src/pathsim/blocks/_block.py +++ b/src/pathsim/blocks/_block.py @@ -316,6 +316,10 @@ def reset(self): 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. @@ -578,4 +582,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 From b0a42e0624cce2a05f445d79cc6a317d08dc47a0 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 27 Jan 2026 12:44:30 +0100 Subject: [PATCH 04/60] overhaul ODE solver docstrings with verified references and usage guidance --- src/pathsim/solvers/bdf.py | 178 ++++++++++++-------------- src/pathsim/solvers/dirk2.py | 44 +++---- src/pathsim/solvers/dirk3.py | 51 ++++---- src/pathsim/solvers/esdirk32.py | 45 +++---- src/pathsim/solvers/esdirk4.py | 40 +++--- src/pathsim/solvers/esdirk43.py | 50 ++++---- src/pathsim/solvers/esdirk54.py | 46 +++---- src/pathsim/solvers/esdirk85.py | 53 ++++---- src/pathsim/solvers/euler.py | 95 ++++++-------- src/pathsim/solvers/gear.py | 198 ++++++++++++----------------- src/pathsim/solvers/rk4.py | 45 +++---- src/pathsim/solvers/rkbs32.py | 48 +++---- src/pathsim/solvers/rkck54.py | 47 +++---- src/pathsim/solvers/rkdp54.py | 51 ++++---- src/pathsim/solvers/rkdp87.py | 53 ++++---- src/pathsim/solvers/rkf21.py | 43 +++---- src/pathsim/solvers/rkf45.py | 46 +++---- src/pathsim/solvers/rkf78.py | 43 +++---- src/pathsim/solvers/rkv65.py | 43 +++---- src/pathsim/solvers/ssprk22.py | 47 +++---- src/pathsim/solvers/ssprk33.py | 62 ++++----- src/pathsim/solvers/ssprk34.py | 43 +++---- src/pathsim/solvers/steadystate.py | 21 ++- 23 files changed, 610 insertions(+), 782 deletions(-) diff --git a/src/pathsim/solvers/bdf.py b/src/pathsim/solvers/bdf.py index fafa382b..aa4b8522 100644 --- a/src/pathsim/solvers/bdf.py +++ b/src/pathsim/solvers/bdf.py @@ -274,35 +274,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. - 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. + .. math:: + + 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 +319,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 +359,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 +400,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 +441,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..53ebfe86 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. A-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 + * A-stable, SSP-optimal, symplectic + + Note + ---- + The simplest multi-stage implicit Runge-Kutta method. A-stability makes + it safe for moderately stiff block diagrams, 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..9b9305aa 100644 --- a/src/pathsim/solvers/esdirk32.py +++ b/src/pathsim/solvers/esdirk32.py @@ -15,41 +15,34 @@ # 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. A-stable. 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. + Note + ---- + The cheapest adaptive implicit solver in this library. Three implicit + stages per step keeps the cost low while A-stability handles moderately + stiff block diagrams. Also used internally as the startup method for + ``GEAR`` solvers. When higher accuracy or L-stability is needed, step up + to ``ESDIRK43``. 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..71546cb6 100644 --- a/src/pathsim/solvers/esdirk4.py +++ b/src/pathsim/solvers/esdirk4.py @@ -15,37 +15,35 @@ # SOLVERS ============================================================================== class ESDIRK4(DiagonallyImplicitRungeKutta): - """Six-stage, 4th order Singly Diagonally Implicit Runge-Kutta (ESDIRK) method. + """Six-stage, 4th order ESDIRK method. A-stable with explicit first stage. - 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 + * Stages: 6 (1 explicit, 5 implicit) + * Fixed timestep * 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. + 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. 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..c3e92f16 100644 --- a/src/pathsim/solvers/esdirk43.py +++ b/src/pathsim/solvers/esdirk43.py @@ -17,39 +17,37 @@ # 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 + + 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..a5b929a1 100644 --- a/src/pathsim/solvers/esdirk54.py +++ b/src/pathsim/solvers/esdirk54.py @@ -15,39 +15,35 @@ # 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 + * L-stable, stiffly accurate - **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. + 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..2f5ecfdc 100644 --- a/src/pathsim/solvers/esdirk85.py +++ b/src/pathsim/solvers/esdirk85.py @@ -15,43 +15,36 @@ # 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 + + 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..0cfdcacd 100644 --- a/src/pathsim/solvers/gear.py +++ b/src/pathsim/solvers/gear.py @@ -424,40 +424,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 +468,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 +511,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 +554,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 +597,40 @@ def __init__(self, *solver_args, **solver_kwargs): class GEAR52A(GEAR): - """Adaptive-order, adaptive-stepsize GEAR integrator (Variable-Step Variable-Order BDF). + """Variable-step, variable-order BDF (orders 2--5). Adapts both timestep + and order automatically. - 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. - - 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..aab16ae7 100644 --- a/src/pathsim/solvers/rk4.py +++ b/src/pathsim/solvers/rk4.py @@ -17,41 +17,38 @@ 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:: + + 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) 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): From b92c89acb5017c1d197563fb1ff95f9e35bcc690 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 27 Jan 2026 12:52:55 +0100 Subject: [PATCH 05/60] overhaul Anderson and NewtonAnderson class docstrings with verified references --- src/pathsim/optim/anderson.py | 72 ++++++++++++++++++++++++++++------- 1 file changed, 58 insertions(+), 14 deletions(-) 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` """ From 15e96d6157325b48652cba395693e6eff60ca451 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 27 Jan 2026 13:00:22 +0100 Subject: [PATCH 06/60] fix ESDIRK32 note: clarify GEAR methods are cheaper per step --- src/pathsim/solvers/esdirk32.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pathsim/solvers/esdirk32.py b/src/pathsim/solvers/esdirk32.py index 9b9305aa..74ec18d4 100644 --- a/src/pathsim/solvers/esdirk32.py +++ b/src/pathsim/solvers/esdirk32.py @@ -27,11 +27,13 @@ class ESDIRK32(DiagonallyImplicitRungeKutta): Note ---- - The cheapest adaptive implicit solver in this library. Three implicit - stages per step keeps the cost low while A-stability handles moderately - stiff block diagrams. Also used internally as the startup method for - ``GEAR`` solvers. When higher accuracy or L-stability is needed, step up - to ``ESDIRK43``. + The cheapest adaptive implicit Runge-Kutta solver in this library. + Three implicit stages per step keeps the cost low while A-stability + handles moderately stiff block diagrams. For 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. When higher accuracy or L-stability is needed, step up to + ``ESDIRK43``. References ---------- From 727989e98dfc38243f9a68057558d191c1a3dbec Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 27 Jan 2026 13:05:16 +0100 Subject: [PATCH 07/60] fix ESDIRK32 properties: L-stable, stiffly accurate, stage order 2 --- src/pathsim/solvers/esdirk32.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pathsim/solvers/esdirk32.py b/src/pathsim/solvers/esdirk32.py index 74ec18d4..9c190b41 100644 --- a/src/pathsim/solvers/esdirk32.py +++ b/src/pathsim/solvers/esdirk32.py @@ -16,24 +16,27 @@ class ESDIRK32(DiagonallyImplicitRungeKutta): """Four-stage, 3rd order ESDIRK method with embedded 2nd order error - estimate. A-stable. + estimate. L-stable and stiffly accurate. Characteristics --------------- * Order: 3 (propagating) / 2 (embedded) * Stages: 4 (1 explicit, 3 implicit) * Adaptive timestep - * A-stable + * L-stable, stiffly accurate + * Stage order 2 (:math:`\\gamma = 1/2`) Note ---- - The cheapest adaptive implicit Runge-Kutta solver in this library. - Three implicit stages per step keeps the cost low while A-stability - handles moderately stiff block diagrams. For 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. When higher accuracy or L-stability is needed, step up to - ``ESDIRK43``. + 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 ---------- From 9c3a133ba7442c524d09f2c392872ba0aafe6831 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 27 Jan 2026 13:14:00 +0100 Subject: [PATCH 08/60] fix implicit RK stability properties after computational verification --- src/pathsim/solvers/dirk2.py | 8 ++++---- src/pathsim/solvers/esdirk4.py | 10 ++++++---- src/pathsim/solvers/esdirk43.py | 1 + src/pathsim/solvers/esdirk54.py | 1 + src/pathsim/solvers/esdirk85.py | 1 + 5 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/pathsim/solvers/dirk2.py b/src/pathsim/solvers/dirk2.py index 53ebfe86..bbceb2b9 100644 --- a/src/pathsim/solvers/dirk2.py +++ b/src/pathsim/solvers/dirk2.py @@ -15,19 +15,19 @@ # SOLVERS ============================================================================== class DIRK2(DiagonallyImplicitRungeKutta): - """Two-stage, 2nd order DIRK method. A-stable, SSP-optimal, symplectic. + """Two-stage, 2nd order DIRK method. L-stable, SSP-optimal, symplectic. Characteristics --------------- * Order: 2 * Stages: 2 (implicit) * Fixed timestep - * A-stable, SSP-optimal, symplectic + * L-stable, SSP-optimal, symplectic Note ---- - The simplest multi-stage implicit Runge-Kutta method. A-stability makes - it safe for moderately stiff block diagrams, and the symplectic property + 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``. diff --git a/src/pathsim/solvers/esdirk4.py b/src/pathsim/solvers/esdirk4.py index 71546cb6..b52dd152 100644 --- a/src/pathsim/solvers/esdirk4.py +++ b/src/pathsim/solvers/esdirk4.py @@ -15,7 +15,7 @@ # SOLVERS ============================================================================== class ESDIRK4(DiagonallyImplicitRungeKutta): - """Six-stage, 4th order ESDIRK method. A-stable with explicit first stage. + """Six-stage, 4th order ESDIRK method. L-stable and stiffly accurate. No embedded error estimator; fixed timestep only. @@ -24,7 +24,8 @@ class ESDIRK4(DiagonallyImplicitRungeKutta): * Order: 4 * Stages: 6 (1 explicit, 5 implicit) * Fixed timestep - * A-stable + * L-stable, stiffly accurate + * Stage order 2 Note ---- @@ -32,8 +33,9 @@ class ESDIRK4(DiagonallyImplicitRungeKutta): 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. For adaptive stepping, use ``ESDIRK43`` which adds an - embedded error estimator at the same stage count. + 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 ---------- diff --git a/src/pathsim/solvers/esdirk43.py b/src/pathsim/solvers/esdirk43.py index c3e92f16..38db4c09 100644 --- a/src/pathsim/solvers/esdirk43.py +++ b/src/pathsim/solvers/esdirk43.py @@ -26,6 +26,7 @@ class ESDIRK43(DiagonallyImplicitRungeKutta): * Stages: 6 (1 explicit, 5 implicit) * Adaptive timestep * L-stable, stiffly accurate + * Stage order 2 Note ---- diff --git a/src/pathsim/solvers/esdirk54.py b/src/pathsim/solvers/esdirk54.py index a5b929a1..860be7a5 100644 --- a/src/pathsim/solvers/esdirk54.py +++ b/src/pathsim/solvers/esdirk54.py @@ -24,6 +24,7 @@ class ESDIRK54(DiagonallyImplicitRungeKutta): * Stages: 7 (1 explicit, 6 implicit) * Adaptive timestep * L-stable, stiffly accurate + * Stage order 2 Note ---- diff --git a/src/pathsim/solvers/esdirk85.py b/src/pathsim/solvers/esdirk85.py index 2f5ecfdc..8225fe04 100644 --- a/src/pathsim/solvers/esdirk85.py +++ b/src/pathsim/solvers/esdirk85.py @@ -24,6 +24,7 @@ class ESDIRK85(DiagonallyImplicitRungeKutta): * Stages: 16 (1 explicit, 15 implicit) * Adaptive timestep * L-stable, stiffly accurate + * Stage order 2 Note ---- From 5de0b917fe336aa156f6d59c9e1ccbf3618320b0 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 27 Jan 2026 13:21:16 +0100 Subject: [PATCH 09/60] wrap RK4 multi-line math in aligned environment --- src/pathsim/solvers/rk4.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pathsim/solvers/rk4.py b/src/pathsim/solvers/rk4.py index aab16ae7..0ae9f882 100644 --- a/src/pathsim/solvers/rk4.py +++ b/src/pathsim/solvers/rk4.py @@ -19,11 +19,13 @@ class RK4(ExplicitRungeKutta): .. 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 --------------- From e69e3b0d87803e4ff6b992dc640823e2b20c56cc Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 28 Jan 2026 15:54:52 +0100 Subject: [PATCH 10/60] update logos with new brand colors --- docs/source/logos/pathsim_icon.png | Bin 14730 -> 18548 bytes docs/source/logos/pathsim_logo.png | Bin 19684 -> 19877 bytes 2 files changed, 0 insertions(+), 0 deletions(-) diff --git a/docs/source/logos/pathsim_icon.png b/docs/source/logos/pathsim_icon.png index 2c39d27945470607caf31b2db24c81c415ad248a..c786aa0805466071025133c8ddd3ca633146f27c 100644 GIT binary patch literal 18548 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 diff --git a/docs/source/logos/pathsim_logo.png b/docs/source/logos/pathsim_logo.png index 8ee02f135c97731a89eef55e3f1f04425152b143..64528d72c40fec18310da2af3e6c962fe98a9a1a 100644 GIT binary patch literal 19877 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 Date: Thu, 29 Jan 2026 11:24:11 +0100 Subject: [PATCH 11/60] Add migration banner to legacy RTD docs --- docs/source/index.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/source/index.rst b/docs/source/index.rst index 993ec87b..129aa25c 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -6,6 +6,16 @@ PathSim .. raw:: html + +

A scalable block-based time-domain system simulation framework in Python with hierarchical modeling and event handling! From 5d249cb9f70cfe2c9f2989affe89e298991a7b62 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 29 Jan 2026 11:38:59 +0100 Subject: [PATCH 12/60] Redesign README for new docs and homepage --- README.md | 353 ++++++++++++------------------------------------------ 1 file changed, 76 insertions(+), 277 deletions(-) diff --git a/README.md b/README.md index a67764b9..0c25ee5f 100644 --- a/README.md +++ b/README.md @@ -1,313 +1,112 @@ - -

PathSim Logo

------------- - - -# PathSim - A System Simulation Framework -[![DOI](https://joss.theoj.org/papers/10.21105/joss.08158/status.svg)](https://doi.org/10.21105/joss.08158) -[![PySimHub](https://pysimhub.io/badge.svg)](https://pysimhub.io/projects/pathsim) -![GitHub License](https://img.shields.io/github/license/pathsim/pathsim) -![GitHub Release](https://img.shields.io/github/v/release/pathsim/pathsim) -[![Documentation Status](https://readthedocs.org/projects/pathsim/badge/?version=latest)](https://pathsim.readthedocs.io/en/latest/?badge=latest) -![PyPI - Downloads](https://img.shields.io/pypi/dw/pathsim) -[![codecov](https://codecov.io/gh/pathsim/pathsim/branch/master/graph/badge.svg)](https://codecov.io/gh/pathsim/pathsim) - - -## Overview - -**PathSim** is a flexible block-based time-domain system simulation framework in Python! It provides a variety of classes that enable modeling and simulation of complex interconnected dynamical systems through Python scripting in the block diagram paradigm. - -All of that with minimal dependencies, only `numpy`, `scipy` and `matplotlib`! - -Key Features: - -- **Dynamic system modification** at simulation runtime, i.e. triggered through events -- Automatic block- and system-level **linearization** at runtime -- Wide range of **numerical integrators** (implicit, explicit, high order, adaptive), able to handle [stiff systems](#stiff-systems) -- **Modular and hierarchical** modeling with (nested) subsystems -- [Event handling](#event-detection) system to detect and resolve **discrete events** (zero-crossing detection) -- **Extensibility** by subclassing the base `Block` class and implementing just a handful of methods - - -For the full **documentation**, tutorials and API-reference visit [Read the Docs](https://pathsim.readthedocs.io/en/latest/)! - -The source code can be found in the [GitHub repository](https://github.com/pathsim/pathsim) and is fully open source under **MIT license**. Consider starring PathSim to support its development. - +

+ A block-based time-domain system simulation framework in Python +

-## Contributing and Future +

+ DOI + PySimHub + PyPI + Conda + License + Release + Downloads + Coverage +

-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: - - -![png](https://raw.githubusercontent.com/pathsim/pathsim/master/docs/source/examples/figures/figures_g/harmonic_oscillator_g.png) - - -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() ``` -![png](https://raw.githubusercontent.com/pathsim/pathsim/master/docs/source/examples/figures/figures_g/harmonic_oscillator_result_g.png) - - -## 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. - - -![png](https://raw.githubusercontent.com/pathsim/pathsim/master/docs/source/examples/figures/figures_g/vanderpol_blockdiagram_g.png) - +## 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(".-") -``` - -![png](https://raw.githubusercontent.com/pathsim/pathsim/master/docs/source/examples/figures/figures_g/vanderpol_result_g.png) - - -## 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. - - -![png](https://raw.githubusercontent.com/pathsim/pathsim/master/docs/source/examples/figures/figures_g/bouncing_ball_g.png) - - -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: - - -![png](https://raw.githubusercontent.com/pathsim/pathsim/master/docs/source/examples/figures/figures_g/bouncing_ball_blockdiagram_g.png) - - -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() -``` - -![png](https://raw.githubusercontent.com/pathsim/pathsim/master/docs/source/examples/figures/figures_g/bouncing_ball_result_g.png) - -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 -![png](https://raw.githubusercontent.com/pathsim/pathsim/master/docs/source/examples/figures/figures_g/bouncing_ball_result_timesteps_g.png) +MIT From 5d2c5e24c31e6e225e59062e6ddad3f7628e7428 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 10 Feb 2026 13:49:54 +0100 Subject: [PATCH 13/60] Add get_params() method to Block base class --- src/pathsim/blocks/_block.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/pathsim/blocks/_block.py b/src/pathsim/blocks/_block.py index c291cae8..947d113a 100644 --- a/src/pathsim/blocks/_block.py +++ b/src/pathsim/blocks/_block.py @@ -261,6 +261,26 @@ def info(cls): } + def get_params(self): + """Return constructor parameters as a dict with actual instance values. + + Inspects the ``__init__`` signature and retrieves the corresponding + attributes from the instance, skipping ``self``, ``name``, ``*args`` + and ``**kwargs``. + + Returns + ------- + params : dict + mapping of parameter names to their current values + """ + sig = inspect.signature(self.__init__) + return { + name: getattr(self, name) + for name, param in sig.parameters.items() + if name not in ("self", "kwargs", "args", "name") and hasattr(self, name) + } + + # methods for visualization --------------------------------------------------------- def plot(self, *args, **kwargs): From 2541faec0b4652332a03d6572195c0bee23413f0 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Mon, 16 Feb 2026 13:34:30 +0100 Subject: [PATCH 14/60] Refactor ctrl blocks: PID as StateSpace subclass, add PT1, PT2, LeadLag, RateLimiter --- src/pathsim/blocks/ctrl.py | 517 ++++++++++++++++++++---------- tests/pathsim/blocks/test_ctrl.py | 332 +++++++++++++++++++ 2 files changed, 677 insertions(+), 172 deletions(-) create mode 100644 tests/pathsim/blocks/test_ctrl.py diff --git a/src/pathsim/blocks/ctrl.py b/src/pathsim/blocks/ctrl.py index 4e24cefc..c088e487 100644 --- a/src/pathsim/blocks/ctrl.py +++ b/src/pathsim/blocks/ctrl.py @@ -9,48 +9,207 @@ import numpy as np +from .lti import StateSpace from ._block import Block -from ..utils.register import Register from ..optim.operator import DynamicOperator -# SISO BLOCKS =========================================================================== +# LTI CONTROL BLOCKS (StateSpace subclasses) ============================================ -class PID(Block): - """Proportional-Integral-Differntiation (PID) controller. +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]]) + ) + + +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]]) + ) + + +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]]) + ) + + +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 +217,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 +239,67 @@ 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, + #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]]) ) - self.op_alg = DynamicOperator( - func=_g_pid, - jac_x=_jac_x_g_pid, - jac_u=_jac_u_g_pid, - ) - - - 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) 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{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 +307,7 @@ class AntiWindupPID(PID): Parameters ---------- Kp : float - poroportional controller coefficient + proportional controller coefficient Ki : float integral controller coefficient Kd : float @@ -246,15 +318,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 +327,148 @@ 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) + + +# NONLINEAR CONTROL BLOCKS ============================================================== + +class RateLimiter(Block): + """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 + + Attributes + ---------- + op_dyn : DynamicOperator + internal dynamic operator for rate-limited ODE + """ + + input_port_labels = {"in": 0} + output_port_labels = {"out": 0} + + def __init__(self, rate=1.0, f_max=100): + super().__init__() + + #rate limiter parameters + self.rate = rate + self.f_max = f_max + + #initial state for integration engine + self.initial_value = 0.0 + + #dynamic operator with clipped rate 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, + func=lambda x, u, t: np.clip(self.f_max * (u - x), -self.rate, self.rate) ) + + + def __len__(self): + return 0 + + + def update(self, t): + """update system equation for fixed point loop + + Note + ---- + Rate limiter does not have passthrough, therefore this + method is performance optimized for this case. + + Parameters + ---------- + t : float + evaluation time + """ + self.outputs.update_from_array(self.engine.state) + + + 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.to_array() + 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.to_array() + f = self.op_dyn(x, u, t) + return self.engine.step(f, dt) diff --git a/tests/pathsim/blocks/test_ctrl.py b/tests/pathsim/blocks/test_ctrl.py new file mode 100644 index 00000000..f7055cf1 --- /dev/null +++ b/tests/pathsim/blocks/test_ctrl.py @@ -0,0 +1,332 @@ +######################################################################################## +## +## TESTS FOR +## 'blocks.ctrl.py' +## +######################################################################################## + +# IMPORTS ============================================================================== + +import unittest +import numpy as np + +from pathsim.blocks.ctrl import ( + PT1, + PT2, + LeadLag, + PID, + AntiWindupPID, + RateLimiter + ) + +#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)) + + +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) + + + +# RUN TESTS LOCALLY ==================================================================== + +if __name__ == '__main__': + unittest.main(verbosity=2) From c3f62d0ff969adb1bd099c0c4ff1164dfdd4232e Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Mon, 16 Feb 2026 13:52:50 +0100 Subject: [PATCH 15/60] Simplify RateLimiter by inheriting from DynamicalSystem --- src/pathsim/blocks/ctrl.py | 80 +++----------------------------------- 1 file changed, 6 insertions(+), 74 deletions(-) diff --git a/src/pathsim/blocks/ctrl.py b/src/pathsim/blocks/ctrl.py index c088e487..df5347d1 100644 --- a/src/pathsim/blocks/ctrl.py +++ b/src/pathsim/blocks/ctrl.py @@ -10,7 +10,7 @@ import numpy as np from .lti import StateSpace -from ._block import Block +from .dynsys import DynamicalSystem from ..optim.operator import DynamicOperator @@ -347,7 +347,7 @@ def _f_pid(x, u, t): # NONLINEAR CONTROL BLOCKS ============================================================== -class RateLimiter(Block): +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 @@ -384,91 +384,23 @@ class RateLimiter(Block): maximum rate of change (positive value) f_max : float tracking bandwidth parameter - - Attributes - ---------- - op_dyn : DynamicOperator - internal dynamic operator for rate-limited ODE """ input_port_labels = {"in": 0} output_port_labels = {"out": 0} def __init__(self, rate=1.0, f_max=100): - super().__init__() #rate limiter parameters self.rate = rate self.f_max = f_max - #initial state for integration engine - self.initial_value = 0.0 - - #dynamic operator with clipped rate - self.op_dyn = DynamicOperator( - func=lambda x, u, t: np.clip(self.f_max * (u - x), -self.rate, self.rate) + 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 ) def __len__(self): return 0 - - - def update(self, t): - """update system equation for fixed point loop - - Note - ---- - Rate limiter does not have passthrough, therefore this - method is performance optimized for this case. - - Parameters - ---------- - t : float - evaluation time - """ - self.outputs.update_from_array(self.engine.state) - - - 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.to_array() - 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.to_array() - f = self.op_dyn(x, u, t) - return self.engine.step(f, dt) From 990955aaa7e7cd6d691b3a1780c8223f8b25839d Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Mon, 16 Feb 2026 14:09:38 +0100 Subject: [PATCH 16/60] Add Deadband and Backlash control blocks --- src/pathsim/blocks/ctrl.py | 116 +++++++++++++++++++++++++++++- tests/pathsim/blocks/test_ctrl.py | 105 ++++++++++++++++++++++++++- 2 files changed, 219 insertions(+), 2 deletions(-) diff --git a/src/pathsim/blocks/ctrl.py b/src/pathsim/blocks/ctrl.py index df5347d1..5f65cdc6 100644 --- a/src/pathsim/blocks/ctrl.py +++ b/src/pathsim/blocks/ctrl.py @@ -9,10 +9,11 @@ import numpy as np +from ._block import Block from .lti import StateSpace from .dynsys import DynamicalSystem -from ..optim.operator import DynamicOperator +from ..optim.operator import Operator, DynamicOperator # LTI CONTROL BLOCKS (StateSpace subclasses) ============================================ @@ -404,3 +405,116 @@ def __init__(self, rate=1.0, f_max=100): 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/tests/pathsim/blocks/test_ctrl.py b/tests/pathsim/blocks/test_ctrl.py index f7055cf1..fee96cfc 100644 --- a/tests/pathsim/blocks/test_ctrl.py +++ b/tests/pathsim/blocks/test_ctrl.py @@ -16,7 +16,9 @@ LeadLag, PID, AntiWindupPID, - RateLimiter + RateLimiter, + Backlash, + Deadband ) #base solver for testing @@ -325,6 +327,107 @@ def test_update(self): 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) + + +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 ==================================================================== From cfac8be4abaaef7b2121b45923818ecf3a32413d Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 10:40:27 +0100 Subject: [PATCH 17/60] Solver.reset optionally updates the initial condition --- src/pathsim/blocks/_block.py | 20 -------------------- src/pathsim/solvers/_solver.py | 29 ++++++++++++++++++++--------- 2 files changed, 20 insertions(+), 29 deletions(-) diff --git a/src/pathsim/blocks/_block.py b/src/pathsim/blocks/_block.py index 947d113a..c291cae8 100644 --- a/src/pathsim/blocks/_block.py +++ b/src/pathsim/blocks/_block.py @@ -261,26 +261,6 @@ def info(cls): } - def get_params(self): - """Return constructor parameters as a dict with actual instance values. - - Inspects the ``__init__`` signature and retrieves the corresponding - attributes from the instance, skipping ``self``, ``name``, ``*args`` - and ``**kwargs``. - - Returns - ------- - params : dict - mapping of parameter names to their current values - """ - sig = inspect.signature(self.__init__) - return { - name: getattr(self, name) - for name, param in sig.parameters.items() - if name not in ("self", "kwargs", "args", "name") and hasattr(self, name) - } - - # methods for visualization --------------------------------------------------------- def plot(self, *args, **kwargs): diff --git a/src/pathsim/solvers/_solver.py b/src/pathsim/solvers/_solver.py index 2df7e6f6..9cf00de9 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() From 26a509a6444fd7ff3331106aa8356fe7ed6331cb Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 10:42:37 +0100 Subject: [PATCH 18/60] Block.reset now updates the initial value of the solver instance --- src/pathsim/blocks/_block.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pathsim/blocks/_block.py b/src/pathsim/blocks/_block.py index c291cae8..3204caf1 100644 --- a/src/pathsim/blocks/_block.py +++ b/src/pathsim/blocks/_block.py @@ -309,8 +309,9 @@ 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() From fe3cc0f724851f04e7ac0bc3f515f67a4eb9378e Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 10:53:20 +0100 Subject: [PATCH 19/60] added Block.state and its setter method via property that exposes the internal solver state --- src/pathsim/blocks/_block.py | 39 +++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/src/pathsim/blocks/_block.py b/src/pathsim/blocks/_block.py index 3204caf1..4b4275fb 100644 --- a/src/pathsim/blocks/_block.py +++ b/src/pathsim/blocks/_block.py @@ -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 @@ -487,6 +489,41 @@ 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 + + # methods for block output and state updates ---------------------------------------- def update(self, t): From 34e28cdb91727d947b3ce606514557fa99443e1f Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 11:06:31 +0100 Subject: [PATCH 20/60] fix naming conflict with state of Switch block, rename Switch.state -> Switch.switch_state --- src/pathsim/blocks/switch.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/pathsim/blocks/switch.py b/src/pathsim/blocks/switch.py index e21a8ca5..dc60d887 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,10 +76,10 @@ 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 update(self, t): @@ -92,5 +92,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] From 8c454bf48d5dadbf48bf19263e2c53ab0cdd2e11 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 11:10:50 +0100 Subject: [PATCH 21/60] Solvers with custom reset method now have the initial_value update --- src/pathsim/solvers/bdf.py | 17 ++++++++++++++--- src/pathsim/solvers/gear.py | 17 ++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/pathsim/solvers/bdf.py b/src/pathsim/solvers/bdf.py index aa4b8522..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): diff --git a/src/pathsim/solvers/gear.py b/src/pathsim/solvers/gear.py index 0cfdcacd..e8cf269f 100644 --- a/src/pathsim/solvers/gear.py +++ b/src/pathsim/solvers/gear.py @@ -231,8 +231,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 +253,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): From 77050d3111fb80ef8b9ff5b5ef240b348eca727c Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 11:22:02 +0100 Subject: [PATCH 22/60] Fix Switch tests to use switch_state instead of shadowed state property --- tests/pathsim/blocks/test_switch.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) 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 From 3df897c6b3266e2088b42735f9044f456d0827ec Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 11:22:42 +0100 Subject: [PATCH 23/60] Improve CI: fix codecov upload, update action versions, clean up deps --- .github/workflows/pypi_deployment.yml | 2 +- .github/workflows/tests_codecov.yml | 6 ++++-- .gitignore | 1 + coverage.json | 1 - pyproject.toml | 2 +- 5 files changed, 7 insertions(+), 5 deletions(-) delete mode 100644 coverage.json 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/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/pyproject.toml b/pyproject.toml index 4542bfa7..65b25e9c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,7 @@ 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", "FMPy", "scikit-rf"] fmi = ["FMPy"] [project.urls] From 483db948cc9a63aa49c26c17c2112a440f1bf8ae Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 11:55:39 +0100 Subject: [PATCH 24/60] Add tests for Relay block --- tests/pathsim/blocks/test_relay.py | 141 +++++++++++++++++++++++++++++ 1 file changed, 141 insertions(+) create mode 100644 tests/pathsim/blocks/test_relay.py 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) From d50e04bf84e82c4ae6bac593f5cd3bfff3afc27a Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 11:56:23 +0100 Subject: [PATCH 25/60] Add tests for AntiWindupPID dynamics and Backlash dead zone --- tests/pathsim/blocks/test_ctrl.py | 63 +++++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/tests/pathsim/blocks/test_ctrl.py b/tests/pathsim/blocks/test_ctrl.py index fee96cfc..407f3f87 100644 --- a/tests/pathsim/blocks/test_ctrl.py +++ b/tests/pathsim/blocks/test_ctrl.py @@ -285,6 +285,41 @@ def test_set_solver(self): 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""" @@ -368,6 +403,34 @@ def test_update(self): 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""" From 4517d46aa10ed756615d17e8bed5dcb21eb7ac65 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 11:58:54 +0100 Subject: [PATCH 26/60] Add solve/step tests for ODE, DynamicalSystem, and phase noise sources --- tests/pathsim/blocks/test_dynsys.py | 44 ++++++++++++++++++- tests/pathsim/blocks/test_ode.py | 43 +++++++++++++++++- tests/pathsim/blocks/test_sources.py | 66 ++++++++++++++++++++++++++++ 3 files changed, 150 insertions(+), 3 deletions(-) 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_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_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""" From 53d3414bdb85e4b379887084ccd5d358a9acd1ca Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 12:12:14 +0100 Subject: [PATCH 27/60] Improve simulation.py test coverage from 65% to 94% --- tests/pathsim/test_simulation.py | 322 +++++++++++++++++++++++++++++++ 1 file changed, 322 insertions(+) diff --git a/tests/pathsim/test_simulation.py b/tests/pathsim/test_simulation.py index c1752f3e..394c1a29 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 ================================================================================ @@ -473,6 +477,324 @@ 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) + # RUN TESTS LOCALLY ==================================================================== From 176e781f9e6a10f0a698ca8134d31114ae3f747f Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 13:06:20 +0100 Subject: [PATCH 28/60] Add 6 functional eval tests for counters, dynamical systems, relay, signal processing, LTI, and steady-state --- tests/evals/test_counter_comparator_system.py | 246 ++++++++++++++ tests/evals/test_dynamical_system_ivp.py | 247 ++++++++++++++ tests/evals/test_relay_thermostat_system.py | 151 +++++++++ tests/evals/test_signal_processing_system.py | 192 +++++++++++ .../test_steadystate_transient_system.py | 308 ++++++++++++++++++ tests/evals/test_switch_lti_system.py | 257 +++++++++++++++ 6 files changed, 1401 insertions(+) create mode 100644 tests/evals/test_counter_comparator_system.py create mode 100644 tests/evals/test_dynamical_system_ivp.py create mode 100644 tests/evals/test_relay_thermostat_system.py create mode 100644 tests/evals/test_signal_processing_system.py create mode 100644 tests/evals/test_steadystate_transient_system.py create mode 100644 tests/evals/test_switch_lti_system.py 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_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_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) From f81924d9e00354973a8ce6e2b071703f3b1c3502 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 18 Feb 2026 13:15:42 +0100 Subject: [PATCH 29/60] Add pytest markers and xdist for faster test runs --- conftest.py | 13 +++++++++++++ pyproject.toml | 8 +++++++- tests/evals/conftest.py | 7 +++++++ 3 files changed, 27 insertions(+), 1 deletion(-) create mode 100644 conftest.py create mode 100644 tests/evals/conftest.py 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/pyproject.toml b/pyproject.toml index 65b25e9c..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", "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/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) From 2e44ad6d872f6d35184b78729a25d4b7bfacd1b8 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 19 Feb 2026 11:16:39 +0100 Subject: [PATCH 30/60] Add remove_block/connection/event and lazy graph rebuild --- src/pathsim/simulation.py | 98 +++++++++++++++++++++++++-- src/pathsim/subsystem.py | 139 ++++++++++++++++++++++++++++++++++++-- 2 files changed, 227 insertions(+), 10 deletions(-) diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index ba38e450..75062429 100644 --- a/src/pathsim/simulation.py +++ b/src/pathsim/simulation.py @@ -194,6 +194,7 @@ def __init__( #internal system graph -> initialized later self.graph = None + self._graph_dirty = False #internal algebraic loop solvers -> initialized later self.boosters = None @@ -366,13 +367,45 @@ def add_block(self, block, _defer_graph=False): #add block to global blocklist self.blocks.add(block) - #if graph already exists, it needs to be rebuilt + #if graph already exists, mark it for rebuild if not _defer_graph and self.graph: - self._assemble_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.discard(block) + + #remove from dynamic list + self._blocks_dyn.discard(block) + + #remove from eventful list + self._blocks_evt.discard(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 + """Adds a new connection to the simulaiton and checks if the new connection overwrites any existing connections. This works dynamically for running simulations. @@ -394,9 +427,35 @@ def add_connection(self, connection, _defer_graph=False): #add connection to global connection list self.connections.add(connection) - #if graph already exists, it needs to be rebuilt + #if graph already exists, mark it for rebuild if not _defer_graph and self.graph: - self._assemble_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.discard(connection) + + #mark graph for rebuild + if self.graph: + self._graph_dirty = True def add_event(self, event): @@ -420,6 +479,27 @@ def add_event(self, event): self.events.add(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.discard(event) + + # system assembly ------------------------------------------------------------- def _assemble_graph(self): @@ -430,6 +510,7 @@ def _assemble_graph(self): #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: @@ -711,11 +792,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) diff --git a/src/pathsim/subsystem.py b/src/pathsim/subsystem.py index 00a7047e..8f8177e1 100644 --- a/src/pathsim/subsystem.py +++ b/src/pathsim/subsystem.py @@ -176,6 +176,7 @@ def __init__(self, #internal graph representation -> initialized later self.graph = None + self._graph_dirty = False #internal algebraic loop solvers -> initialized later self.boosters = None @@ -254,13 +255,134 @@ 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.add(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.discard(block) + + #remove from dynamic list + if 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.add(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.discard(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) + self._graph_dirty = False #create boosters for loop closing connections if self.graph.has_loops: @@ -422,20 +544,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 +733,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: From ca34c5b627cf8d1e7c1a1b799c4ecf5e1464286d Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 19 Feb 2026 14:22:26 +0100 Subject: [PATCH 31/60] Remove _defer_graph, add runtime mutation tests --- src/pathsim/simulation.py | 28 ++-- src/pathsim/subsystem.py | 2 +- tests/pathsim/test_simulation.py | 232 +++++++++++++++++++++++++++++++ tests/pathsim/test_subsystem.py | 182 ++++++++++++++++++++++++ 4 files changed, 427 insertions(+), 17 deletions(-) diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index 75062429..f2814cc8 100644 --- a/src/pathsim/simulation.py +++ b/src/pathsim/simulation.py @@ -236,12 +236,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: @@ -333,18 +333,16 @@ def plot(self, *args, **kwargs): # 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 @@ -367,8 +365,8 @@ def add_block(self, block, _defer_graph=False): #add block to global blocklist self.blocks.add(block) - #if graph already exists, mark it for rebuild - if not _defer_graph and self.graph: + #mark graph for rebuild + if self.graph: self._graph_dirty = True @@ -404,8 +402,8 @@ def remove_block(self, block): 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. @@ -414,8 +412,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 @@ -427,8 +423,8 @@ def add_connection(self, connection, _defer_graph=False): #add connection to global connection list self.connections.add(connection) - #if graph already exists, mark it for rebuild - if not _defer_graph and self.graph: + #mark graph for rebuild + if self.graph: self._graph_dirty = True diff --git a/src/pathsim/subsystem.py b/src/pathsim/subsystem.py index 8f8177e1..3f0cabc4 100644 --- a/src/pathsim/subsystem.py +++ b/src/pathsim/subsystem.py @@ -298,7 +298,7 @@ def remove_block(self, block): self.blocks.discard(block) #remove from dynamic list - if block in self._blocks_dyn: + if hasattr(self, '_blocks_dyn') and block in self._blocks_dyn: self._blocks_dyn.remove(block) if self.graph: diff --git a/tests/pathsim/test_simulation.py b/tests/pathsim/test_simulation.py index 394c1a29..c1ae6b8a 100644 --- a/tests/pathsim/test_simulation.py +++ b/tests/pathsim/test_simulation.py @@ -796,6 +796,238 @@ def test_run_realtime(self): 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_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 ==================================================================== if __name__ == '__main__': 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 ==================================================================== From 0e2f038fe6b7196833c74ccb43fa453bc9afa915 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 19 Feb 2026 15:42:22 +0100 Subject: [PATCH 32/60] Zero target inputs on remove_connection to avoid stale values --- src/pathsim/simulation.py | 5 +++++ src/pathsim/subsystem.py | 5 +++++ tests/pathsim/test_simulation.py | 15 +++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index f2814cc8..7f9307db 100644 --- a/src/pathsim/simulation.py +++ b/src/pathsim/simulation.py @@ -446,6 +446,11 @@ def remove_connection(self, connection): self.logger.error(_msg) raise ValueError(_msg) + #zero out target input ports to avoid stale values + for target in connection.targets: + for port in target.ports: + target.block.inputs[port] = 0.0 + #remove from global connection list self.connections.discard(connection) diff --git a/src/pathsim/subsystem.py b/src/pathsim/subsystem.py index 3f0cabc4..a46415ff 100644 --- a/src/pathsim/subsystem.py +++ b/src/pathsim/subsystem.py @@ -337,6 +337,11 @@ def remove_connection(self, connection): if connection not in self.connections: raise ValueError(f"{connection} not part of subsystem") + #zero out target input ports to avoid stale values + for target in connection.targets: + for port in target.ports: + target.block.inputs[port] = 0.0 + self.connections.discard(connection) if self.graph: diff --git a/tests/pathsim/test_simulation.py b/tests/pathsim/test_simulation.py index c1ae6b8a..e17e3fb9 100644 --- a/tests/pathsim/test_simulation.py +++ b/tests/pathsim/test_simulation.py @@ -884,6 +884,21 @@ def test_remove_connection_error(self): with self.assertRaises(ValueError): self.Sim.remove_connection(C) + def test_remove_connection_zeroes_inputs(self): + """Removing a connection zeroes the target block's input ports""" + # 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 + self.Sim.remove_connection(self.C1) + + # Target input should now be zero + 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) From 655ed3a36066adc5ca1ba033cbc581404e7aaad3 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 19 Feb 2026 15:53:09 +0100 Subject: [PATCH 33/60] Reset block inputs in _assemble_graph instead of remove_connection --- src/pathsim/simulation.py | 11 +++++------ src/pathsim/subsystem.py | 10 +++++----- tests/pathsim/test_simulation.py | 8 +++++--- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index 7f9307db..306c7e4c 100644 --- a/src/pathsim/simulation.py +++ b/src/pathsim/simulation.py @@ -446,11 +446,6 @@ def remove_connection(self, connection): self.logger.error(_msg) raise ValueError(_msg) - #zero out target input ports to avoid stale values - for target in connection.targets: - for port in target.ports: - target.block.inputs[port] = 0.0 - #remove from global connection list self.connections.discard(connection) @@ -504,10 +499,14 @@ def remove_event(self, 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) diff --git a/src/pathsim/subsystem.py b/src/pathsim/subsystem.py index a46415ff..2dd3640c 100644 --- a/src/pathsim/subsystem.py +++ b/src/pathsim/subsystem.py @@ -337,11 +337,6 @@ def remove_connection(self, connection): if connection not in self.connections: raise ValueError(f"{connection} not part of subsystem") - #zero out target input ports to avoid stale values - for target in connection.targets: - for port in target.ports: - target.block.inputs[port] = 0.0 - self.connections.discard(connection) if self.graph: @@ -386,6 +381,11 @@ def _assemble_graph(self): """Assemble internal graph of subsystem for fast algebraic evaluation during simulation. """ + + #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 diff --git a/tests/pathsim/test_simulation.py b/tests/pathsim/test_simulation.py index e17e3fb9..beda6aae 100644 --- a/tests/pathsim/test_simulation.py +++ b/tests/pathsim/test_simulation.py @@ -885,7 +885,7 @@ def test_remove_connection_error(self): self.Sim.remove_connection(C) def test_remove_connection_zeroes_inputs(self): - """Removing a connection zeroes the target block's input ports""" + """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) @@ -893,10 +893,12 @@ def test_remove_connection_zeroes_inputs(self): # Int should have received input from Src self.assertNotEqual(self.Int.inputs[0], 0.0) - # Remove the connection + # Remove the connection — graph is dirty but not rebuilt yet self.Sim.remove_connection(self.C1) + self.assertTrue(self.Sim._graph_dirty) - # Target input should now be zero + # 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): From 80a3810651f2ae927a37d9df8dfb2b44e0c392b5 Mon Sep 17 00:00:00 2001 From: kwmcbride Date: Tue, 24 Feb 2026 06:55:41 -0800 Subject: [PATCH 34/60] adding divider block --- src/pathsim/blocks/__init__.py | 1 + src/pathsim/blocks/divider.py | 215 +++++++++++++++++++ tests/pathsim/blocks/test_divider.py | 304 +++++++++++++++++++++++++++ 3 files changed, 520 insertions(+) create mode 100644 src/pathsim/blocks/divider.py create mode 100644 tests/pathsim/blocks/test_divider.py diff --git a/src/pathsim/blocks/__init__.py b/src/pathsim/blocks/__init__.py index c8580e25..a196856e 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 * diff --git a/src/pathsim/blocks/divider.py b/src/pathsim/blocks/divider.py new file mode 100644 index 00000000..ae6329a0 --- /dev/null +++ b/src/pathsim/blocks/divider.py @@ -0,0 +1,215 @@ +######################################################################################### +## +## 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 + + +# MISO BLOCKS =========================================================================== + +_ZERO_DIV_OPTIONS = ("warn", "raise", "clamp") + + +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 all inputs (same as :class:`Multiplier`): + + .. 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 ``*``. ``None`` multiplies all inputs. + 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=None, 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/tests/pathsim/blocks/test_divider.py b/tests/pathsim/blocks/test_divider.py new file mode 100644 index 00000000..f304dbb3 --- /dev/null +++ b/tests/pathsim/blocks/test_divider.py @@ -0,0 +1,304 @@ +######################################################################################## +## +## 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.assertIsNone(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: multiply all (identical to Multiplier) + D = Divider() + + def src(t): return np.cos(t), np.sin(t) + 2, 3.0, t + 1 + def ref(t): return np.cos(t) * (np.sin(t) + 2) * 3.0 * (t + 1) + + 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 (multiply all) — nonlinear, so only check at linearization point + D = Divider() + + def src(t): return np.cos(t) + 2, t + 1, 3.0 + def ref(t): return (np.cos(t) + 2) * (t + 1) * 3.0 + + 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): + + D = Divider() + + D.inputs[0] = 2.0 + D.inputs[1] = 3.0 + D.inputs[2] = 4.0 + D.update(None) + + self.assertEqual(D.outputs[0], 24.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) From 7d152136071f6751568b7b21c255b693e8c18ab4 Mon Sep 17 00:00:00 2001 From: kwmcbride Date: Tue, 24 Feb 2026 07:11:04 -0800 Subject: [PATCH 35/60] I had this default to multiplier - changed it to */ and updated tests --- src/pathsim/blocks/divider.py | 7 ++++--- tests/pathsim/blocks/test_divider.py | 23 ++++++++++++----------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/pathsim/blocks/divider.py b/src/pathsim/blocks/divider.py index ae6329a0..a0d684cc 100644 --- a/src/pathsim/blocks/divider.py +++ b/src/pathsim/blocks/divider.py @@ -42,7 +42,7 @@ class Divider(Block): Example ------- - Default initialization multiplies all inputs (same as :class:`Multiplier`): + Default initialization multiplies the first input and divides by the second: .. code-block:: python @@ -79,7 +79,8 @@ class Divider(Block): operations : str, optional String of ``*`` and ``/`` characters indicating which inputs are multiplied (``*``) or divided (``/``). Inputs beyond the length of - the string default to ``*``. ``None`` multiplies all inputs. + the string default to ``*``. Defaults to ``'*/'`` (divide second + input by first). zero_div : str, optional Behaviour when a denominator input is zero. One of: @@ -107,7 +108,7 @@ class Divider(Block): input_port_labels = None output_port_labels = {"out": 0} - def __init__(self, operations=None, zero_div="warn"): + def __init__(self, operations="*/", zero_div="warn"): super().__init__() # validate zero_div diff --git a/tests/pathsim/blocks/test_divider.py b/tests/pathsim/blocks/test_divider.py index f304dbb3..a60b7edc 100644 --- a/tests/pathsim/blocks/test_divider.py +++ b/tests/pathsim/blocks/test_divider.py @@ -26,7 +26,7 @@ def test_init(self): # default initialization D = Divider() - self.assertIsNone(D.operations) + self.assertEqual(D.operations, "*/") # valid ops strings for ops in ["*", "/", "*/", "/*", "**/", "/**"]: @@ -47,11 +47,11 @@ def test_init(self): def test_embedding(self): """Test algebraic output against reference via Embedding.""" - # default: multiply all (identical to Multiplier) + # default: '*/' — u0 * u2 * ... / u1 D = Divider() - def src(t): return np.cos(t), np.sin(t) + 2, 3.0, t + 1 - def ref(t): return np.cos(t) * (np.sin(t) + 2) * 3.0 * (t + 1) + 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)) @@ -97,11 +97,11 @@ def ref(t): return np.cos(t) def test_linearization(self): """Test linearize / delinearize round-trip.""" - # default (multiply all) — nonlinear, so only check at linearization point + # default ('*/') — nonlinear, so only check at linearization point D = Divider() - def src(t): return np.cos(t) + 2, t + 1, 3.0 - def ref(t): return (np.cos(t) + 2) * (t + 1) * 3.0 + 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) @@ -145,14 +145,15 @@ def test_update_single(self): def test_update_multi(self): + # default '*/' with 3 inputs: ops=[*, /, *] → (u0 * u2) / u1 D = Divider() - D.inputs[0] = 2.0 - D.inputs[1] = 3.0 - D.inputs[2] = 4.0 + D.inputs[0] = 6.0 + D.inputs[1] = 2.0 + D.inputs[2] = 3.0 D.update(None) - self.assertEqual(D.outputs[0], 24.0) + self.assertEqual(D.outputs[0], 9.0) def test_update_ops(self): From 7bc8876c10f39229860bdf1937c64af84f1544f8 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 19 Feb 2026 21:47:52 +0100 Subject: [PATCH 36/60] Add mutable class decorator for runtime parameter reinitialization --- src/pathsim/utils/__init__.py | 1 + src/pathsim/utils/mutable.py | 171 ++++++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 src/pathsim/utils/mutable.py 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/mutable.py b/src/pathsim/utils/mutable.py new file mode 100644 index 00000000..686ec00c --- /dev/null +++ b/src/pathsim/utils/mutable.py @@ -0,0 +1,171 @@ +######################################################################################### +## +## 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 + + +# DECORATOR ============================================================================= + +def mutable(*params): + """Class decorator that makes listed parameters trigger automatic reinitialization. + + When a parameter declared as mutable is changed at runtime, the block's ``__init__`` + is re-executed with the updated parameter values. The integration engine state is + preserved across the reinitialization, ensuring continuity during simulation. + + A ``set(**kwargs)`` method is also generated for batched parameter updates that + triggers only a single reinitialization. + + Parameters + ---------- + params : str + names of the mutable parameters (must match ``__init__`` argument names) + + Example + ------- + .. code-block:: python + + @mutable("K", "T") + 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 + """ + + def decorator(cls): + + original_init = cls.__init__ + + # get all __init__ parameter names for reinit + init_params = [ + name for name in inspect.signature(original_init).parameters + if name != "self" + ] + + # validate that declared mutable params exist in __init__ + for p in params: + if p not in init_params: + raise ValueError( + f"Mutable parameter '{p}' not found in " + f"{cls.__name__}.__init__ signature {init_params}" + ) + + # -- install property descriptors for mutable 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): + _reinit(self) + + return property(getter, setter) + + setattr(cls, name, _make_property(storage)) + + # -- reinit function ----------------------------------------------------------- + + def _reinit(self): + """Re-run __init__ with current parameter values, preserving engine state.""" + + # gather current values for all init params + kwargs = {} + for name in init_params: + if hasattr(self, name): + kwargs[name] = getattr(self, name) + + # save engine state + engine = self.engine if hasattr(self, 'engine') else None + + # re-run init (unlock to prevent recursive reinit) + self._param_locked = False + original_init(self, **kwargs) + self._param_locked = True + + # restore engine + if engine is not None: + old_dim = len(engine) + new_dim = len(np.atleast_1d(self.initial_value)) if hasattr(self, 'initial_value') else 0 + + if old_dim == new_dim: + # same dimension - restore the engine directly + self.engine = engine + else: + # dimension changed - create new engine inheriting settings + self.engine = type(engine).create( + self.initial_value, + parent=engine.parent, + from_engine=None + ) + # inherit tolerances manually since from_engine=None + self.engine.tolerance_lte_abs = engine.tolerance_lte_abs + self.engine.tolerance_lte_rel = engine.tolerance_lte_rel + + # -- wrap __init__ to flip the lock after construction ------------------------- + + @functools.wraps(original_init) + def new_init(self, *args, **kwargs): + original_init(self, *args, **kwargs) + 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) + self._param_locked = True + _reinit(self) + + cls.set = set + + # -- store metadata for introspection ------------------------------------------ + + cls._mutable_params = params + + return cls + + return decorator From bc1c62e8ddc11197b6c3b7d47b7445d36c70b331 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 19 Feb 2026 21:53:57 +0100 Subject: [PATCH 37/60] Apply mutable decorator to ctrl, lti, and filter blocks --- src/pathsim/blocks/ctrl.py | 6 + src/pathsim/blocks/filters.py | 6 + src/pathsim/blocks/lti.py | 4 + src/pathsim/utils/mutable.py | 214 +++++++++++++++++----------------- 4 files changed, 126 insertions(+), 104 deletions(-) diff --git a/src/pathsim/blocks/ctrl.py b/src/pathsim/blocks/ctrl.py index 5f65cdc6..70fa72f6 100644 --- a/src/pathsim/blocks/ctrl.py +++ b/src/pathsim/blocks/ctrl.py @@ -14,10 +14,12 @@ from .dynsys import DynamicalSystem from ..optim.operator import Operator, DynamicOperator +from ..utils.mutable import mutable # LTI CONTROL BLOCKS (StateSpace subclasses) ============================================ +@mutable class PT1(StateSpace): """First-order lag element (PT1). @@ -65,6 +67,7 @@ def __init__(self, K=1.0, T=1.0): ) +@mutable class PT2(StateSpace): """Second-order lag element (PT2). @@ -124,6 +127,7 @@ def __init__(self, K=1.0, T=1.0, d=1.0): ) +@mutable class LeadLag(StateSpace): """Lead-Lag compensator. @@ -180,6 +184,7 @@ def __init__(self, K=1.0, T1=1.0, T2=1.0): ) +@mutable class PID(StateSpace): """Proportional-Integral-Differentiation (PID) controller. @@ -253,6 +258,7 @@ def __init__(self, Kp=0, Ki=0, Kd=0, f_max=100): ) +@mutable class AntiWindupPID(PID): """Proportional-Integral-Differentiation (PID) controller with anti-windup mechanism (back-calculation). 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/lti.py b/src/pathsim/blocks/lti.py index de85e99c..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 ============================================================================ @@ -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. @@ -227,6 +229,7 @@ class TransferFunction(TransferFunctionPRC): pass +@mutable class TransferFunctionZPG(StateSpace): """This block defines a LTI (SISO) transfer function. @@ -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. diff --git a/src/pathsim/utils/mutable.py b/src/pathsim/utils/mutable.py index 686ec00c..97888908 100644 --- a/src/pathsim/utils/mutable.py +++ b/src/pathsim/utils/mutable.py @@ -17,28 +17,79 @@ 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(*params): - """Class decorator that makes listed parameters trigger automatic reinitialization. +def mutable(cls): + """Class decorator that makes all ``__init__`` parameters trigger automatic + reinitialization when changed at runtime. - When a parameter declared as mutable is changed at runtime, the block's ``__init__`` - is re-executed with the updated parameter values. The integration engine state is - preserved across the reinitialization, ensuring continuity during simulation. + 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. - Parameters - ---------- - params : str - names of the mutable parameters (must match ``__init__`` argument names) + 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("K", "T") + @mutable class PT1(StateSpace): def __init__(self, K=1.0, T=1.0): self.K = K @@ -55,117 +106,72 @@ def __init__(self, K=1.0, T=1.0): pt1.set(K=5.0, T=0.3) # single reinitialization """ - def decorator(cls): + original_init = cls.__init__ - original_init = cls.__init__ + # auto-detect all __init__ parameters + params = [ + name for name in inspect.signature(original_init).parameters + if name != "self" + ] - # get all __init__ parameter names for reinit - init_params = [ - name for name in inspect.signature(original_init).parameters - if name != "self" - ] + # -- install property descriptors for all params ------------------------------- - # validate that declared mutable params exist in __init__ - for p in params: - if p not in init_params: - raise ValueError( - f"Mutable parameter '{p}' not found in " - f"{cls.__name__}.__init__ signature {init_params}" - ) + for name in params: + storage = f"_p_{name}" - # -- install property descriptors for mutable params --------------------------- + def _make_property(s): + def getter(self): + return getattr(self, s) - for name in params: - storage = f"_p_{name}" + def setter(self, value): + setattr(self, s, value) + if getattr(self, '_param_locked', False): + _do_reinit(self) - def _make_property(s): - def getter(self): - return getattr(self, s) + return property(getter, setter) - def setter(self, value): - setattr(self, s, value) - if getattr(self, '_param_locked', False): - _reinit(self) + setattr(cls, name, _make_property(storage)) - return property(getter, setter) + # -- wrap __init__ with depth counter ------------------------------------------ - setattr(cls, name, _make_property(storage)) - - # -- reinit function ----------------------------------------------------------- - - def _reinit(self): - """Re-run __init__ with current parameter values, preserving engine state.""" - - # gather current values for all init params - kwargs = {} - for name in init_params: - if hasattr(self, name): - kwargs[name] = getattr(self, name) - - # save engine state - engine = self.engine if hasattr(self, 'engine') else None - - # re-run init (unlock to prevent recursive reinit) - self._param_locked = False - original_init(self, **kwargs) - self._param_locked = True - - # restore engine - if engine is not None: - old_dim = len(engine) - new_dim = len(np.atleast_1d(self.initial_value)) if hasattr(self, 'initial_value') else 0 - - if old_dim == new_dim: - # same dimension - restore the engine directly - self.engine = engine - else: - # dimension changed - create new engine inheriting settings - self.engine = type(engine).create( - self.initial_value, - parent=engine.parent, - from_engine=None - ) - # inherit tolerances manually since from_engine=None - self.engine.tolerance_lte_abs = engine.tolerance_lte_abs - self.engine.tolerance_lte_rel = engine.tolerance_lte_rel - - # -- wrap __init__ to flip the lock after construction ------------------------- - - @functools.wraps(original_init) - def new_init(self, *args, **kwargs): + @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) - self._param_locked = True - - cls.__init__ = new_init + finally: + self._init_depth -= 1 + if self._init_depth == 0: + self._param_locked = True - # -- generate batched set() method --------------------------------------------- + cls.__init__ = new_init - def set(self, **kwargs): - """Set multiple parameters and reinitialize once. + # -- generate batched set() method --------------------------------------------- - Parameters - ---------- - kwargs : dict - parameter names and their new values + def set(self, **kwargs): + """Set multiple parameters and reinitialize once. - Example - ------- - .. code-block:: python + Parameters + ---------- + kwargs : dict + parameter names and their new values - block.set(K=5.0, T=0.3) - """ - self._param_locked = False - for key, value in kwargs.items(): - setattr(self, key, value) - self._param_locked = True - _reinit(self) + Example + ------- + .. code-block:: python - cls.set = set + 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) - # -- store metadata for introspection ------------------------------------------ + cls.set = set - cls._mutable_params = params + # -- store metadata for introspection ------------------------------------------ - return cls + existing = getattr(cls, '_mutable_params', ()) + cls._mutable_params = existing + tuple(p for p in params if p not in existing) - return decorator + return cls From 4a8691f651964d6d909e681c08d2e2c3a8cc2714 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 19 Feb 2026 21:56:08 +0100 Subject: [PATCH 38/60] Apply mutable decorator to sources, delay, spectrum, FIR, converters, samplehold --- src/pathsim/blocks/converters.py | 3 +++ src/pathsim/blocks/delay.py | 2 ++ src/pathsim/blocks/fir.py | 6 ++++-- src/pathsim/blocks/samplehold.py | 2 ++ src/pathsim/blocks/sources.py | 7 +++++++ src/pathsim/blocks/spectrum.py | 2 ++ 6 files changed, 20 insertions(+), 2 deletions(-) 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/delay.py b/src/pathsim/blocks/delay.py index 88baad10..174ec8a8 100644 --- a/src/pathsim/blocks/delay.py +++ b/src/pathsim/blocks/delay.py @@ -12,10 +12,12 @@ from ._block import Block from ..utils.adaptivebuffer import AdaptiveBuffer +from ..utils.mutable import mutable # BLOCKS ================================================================================ +@mutable class Delay(Block): """Delays the input signal by a time constant 'tau' in seconds. diff --git a/src/pathsim/blocks/fir.py b/src/pathsim/blocks/fir.py index 5cdebd66..8db1a8a3 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. 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/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..7b3a0878 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). From 766b2a5c7ea6854e8385aa3a1852a6d63df4a34d Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 19 Feb 2026 21:59:21 +0100 Subject: [PATCH 39/60] Add tests for mutable decorator --- tests/pathsim/utils/test_mutable.py | 267 ++++++++++++++++++++++++++++ 1 file changed, 267 insertions(+) create mode 100644 tests/pathsim/utils/test_mutable.py 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() From 314b616c16c136c6786d0ae52dfc950705f05ea3 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 26 Feb 2026 10:45:56 +0100 Subject: [PATCH 40/60] Apply mutable decorator to Divider block --- src/pathsim/blocks/divider.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pathsim/blocks/divider.py b/src/pathsim/blocks/divider.py index a0d684cc..4e623170 100644 --- a/src/pathsim/blocks/divider.py +++ b/src/pathsim/blocks/divider.py @@ -15,6 +15,7 @@ from ._block import Block from ..utils.register import Register from ..optim.operator import Operator +from ..utils.mutable import mutable # MISO BLOCKS =========================================================================== @@ -22,6 +23,7 @@ _ZERO_DIV_OPTIONS = ("warn", "raise", "clamp") +@mutable class Divider(Block): """Multiplies and divides input signals (MISO). From d08ab8563a2393156e0a2e1db5913d6536b18a21 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 26 Feb 2026 10:35:36 +0100 Subject: [PATCH 41/60] Add missing XPRT IR blocks: logic ops, Atan2, MapLin, Alias, discrete Delay --- src/pathsim/blocks/__init__.py | 1 + src/pathsim/blocks/delay.py | 125 ++++++++++++---- src/pathsim/blocks/logic.py | 229 +++++++++++++++++++++++++++++ src/pathsim/blocks/math.py | 149 +++++++++++++++++++ tests/pathsim/blocks/test_delay.py | 91 ++++++++++++ tests/pathsim/blocks/test_logic.py | 217 +++++++++++++++++++++++++++ tests/pathsim/blocks/test_math.py | 126 ++++++++++++++++ 7 files changed, 908 insertions(+), 30 deletions(-) create mode 100644 src/pathsim/blocks/logic.py create mode 100644 tests/pathsim/blocks/test_logic.py diff --git a/src/pathsim/blocks/__init__.py b/src/pathsim/blocks/__init__.py index a196856e..dd67152b 100644 --- a/src/pathsim/blocks/__init__.py +++ b/src/pathsim/blocks/__init__.py @@ -21,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/delay.py b/src/pathsim/blocks/delay.py index 174ec8a8..6bcbed8b 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,9 +9,12 @@ 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 @@ -19,60 +22,107 @@ @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): @@ -83,13 +133,18 @@ 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 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 ---------- @@ -97,13 +152,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 @@ -114,5 +173,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/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/math.py b/src/pathsim/blocks/math.py index 6a08f927..a3d89ef2 100644 --- a/src/pathsim/blocks/math.py +++ b/src/pathsim/blocks/math.py @@ -574,4 +574,153 @@ 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) + + +class MapLin(Math): + """Linear mapping / interpolation 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/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_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..37cb8427 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 TestMapLin(unittest.TestCase): + """ + Test the implementation of the 'MapLin' block class + """ + + def test_default_identity(self): + """test default mapping [0,1] -> [0,1] is identity""" + + B = MapLin() + + 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 = MapLin(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 = MapLin(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 = MapLin(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 = MapLin(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 From 250e699a30b24ebeeed8961a3b2e390e4423c659 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 26 Feb 2026 10:40:31 +0100 Subject: [PATCH 42/60] Rename MapLin to Rescale for consistency with pathsim naming conventions --- src/pathsim/blocks/math.py | 6 ++++-- tests/pathsim/blocks/test_math.py | 14 +++++++------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pathsim/blocks/math.py b/src/pathsim/blocks/math.py index a3d89ef2..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 ======================================================================= @@ -635,8 +636,9 @@ def update(self, t): self.outputs.update_from_array(y) -class MapLin(Math): - """Linear mapping / interpolation block. +@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]``. diff --git a/tests/pathsim/blocks/test_math.py b/tests/pathsim/blocks/test_math.py index 37cb8427..075e831f 100644 --- a/tests/pathsim/blocks/test_math.py +++ b/tests/pathsim/blocks/test_math.py @@ -579,15 +579,15 @@ def test_quadrants(self): self.assertAlmostEqual(B.outputs[0], expected) -class TestMapLin(unittest.TestCase): +class TestRescale(unittest.TestCase): """ - Test the implementation of the 'MapLin' block class + Test the implementation of the 'Rescale' block class """ def test_default_identity(self): """test default mapping [0,1] -> [0,1] is identity""" - B = MapLin() + B = Rescale() def src(t): return t * 0.1 def ref(t): return t * 0.1 @@ -598,7 +598,7 @@ def ref(t): return t * 0.1 def test_custom_mapping(self): """test custom linear mapping""" - B = MapLin(i0=0.0, i1=10.0, o0=0.0, o1=100.0) + 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 @@ -609,7 +609,7 @@ def ref(t): return float(t) * 10.0 def test_saturate(self): """test saturation clamping""" - B = MapLin(i0=0.0, i1=1.0, o0=0.0, o1=10.0, saturate=True) + B = Rescale(i0=0.0, i1=1.0, o0=0.0, o1=10.0, saturate=True) #input beyond range B.inputs[0] = 2.0 @@ -624,7 +624,7 @@ def test_saturate(self): def test_no_saturate(self): """test that without saturation, output can exceed range""" - B = MapLin(i0=0.0, i1=1.0, o0=0.0, o1=10.0, saturate=False) + B = Rescale(i0=0.0, i1=1.0, o0=0.0, o1=10.0, saturate=False) B.inputs[0] = 2.0 B.update(0) @@ -633,7 +633,7 @@ def test_no_saturate(self): def test_vector_input(self): """test with vector inputs""" - B = MapLin(i0=0.0, i1=10.0, o0=-1.0, o1=1.0) + 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 From f02e3cb5a6de809d3a9dcc81ac3836e7641cc231 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 26 Feb 2026 11:09:33 +0100 Subject: [PATCH 43/60] Add eval tests for logic, Rescale, Atan2, Alias, and discrete Delay --- tests/evals/test_logic_system.py | 265 ++++++++++++++++++++++ tests/evals/test_rescale_delay_system.py | 277 +++++++++++++++++++++++ 2 files changed, 542 insertions(+) create mode 100644 tests/evals/test_logic_system.py create mode 100644 tests/evals/test_rescale_delay_system.py 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_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) From bbd0f7755de160c310689059e85d656a9fc86ca6 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 4 Mar 2026 09:18:20 +0100 Subject: [PATCH 44/60] Deprecate RFNetwork in favour of pathsim-rf package --- src/pathsim/blocks/rf.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) 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 From 0d52b5421d1edc964fec2912b09894832a48808a Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Sun, 15 Mar 2026 20:59:57 +0100 Subject: [PATCH 45/60] Add checkpoint save/load system with JSON+NPZ format --- src/pathsim/blocks/_block.py | 91 ++++++++++ src/pathsim/blocks/delay.py | 35 ++++ src/pathsim/blocks/scope.py | 42 ++++- src/pathsim/blocks/spectrum.py | 18 ++ src/pathsim/blocks/switch.py | 9 + src/pathsim/events/_event.py | 63 ++++++- src/pathsim/simulation.py | 134 +++++++++++++++ src/pathsim/solvers/_solver.py | 64 +++++++ src/pathsim/solvers/gear.py | 44 +++++ src/pathsim/utils/adaptivebuffer.py | 45 ++++- tests/pathsim/test_checkpoint.py | 256 ++++++++++++++++++++++++++++ 11 files changed, 796 insertions(+), 5 deletions(-) create mode 100644 tests/pathsim/test_checkpoint.py diff --git a/src/pathsim/blocks/_block.py b/src/pathsim/blocks/_block.py index 4b4275fb..597195a4 100644 --- a/src/pathsim/blocks/_block.py +++ b/src/pathsim/blocks/_block.py @@ -11,6 +11,7 @@ # IMPORTS =============================================================================== import inspect +from uuid import uuid4 from functools import lru_cache from ..utils.deprecation import deprecated @@ -84,6 +85,9 @@ class definition for other blocks to be inherited. def __init__(self): + #unique identifier for checkpointing and diagnostics + self.id = uuid4().hex + #registers to hold input and output values self.inputs = Register( mapping=self.input_port_labels and self.input_port_labels.copy() @@ -524,6 +528,93 @@ def state(self, val): self.engine.state = val + # checkpoint methods ---------------------------------------------------------------- + + def to_checkpoint(self, recordings=False): + """Serialize block state for checkpointing. + + Parameters + ---------- + recordings : bool + include recording data (for Scope blocks) + + Returns + ------- + json_data : dict + JSON-serializable metadata + npz_data : dict + numpy arrays keyed by path + """ + prefix = self.id + + json_data = { + "id": self.id, + "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 event in self.events: + e_json, e_npz = event.to_checkpoint() + evt_jsons.append(e_json) + npz_data.update(e_npz) + json_data["events"] = evt_jsons + + return json_data, npz_data + + + def load_checkpoint(self, json_data, npz): + """Restore block state from checkpoint. + + Parameters + ---------- + json_data : dict + block metadata from checkpoint JSON + npz : dict-like + numpy arrays from checkpoint NPZ + """ + prefix = json_data["id"] + + #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 event, evt_data in zip(self.events, json_data["events"]): + event.load_checkpoint(evt_data, npz) + + # methods for block output and state updates ---------------------------------------- def update(self, t): diff --git a/src/pathsim/blocks/delay.py b/src/pathsim/blocks/delay.py index 6bcbed8b..6b42614c 100644 --- a/src/pathsim/blocks/delay.py +++ b/src/pathsim/blocks/delay.py @@ -142,6 +142,41 @@ def reset(self): self._ring.extend([0.0] * self._n) + def to_checkpoint(self, recordings=False): + """Serialize Delay state including buffer data.""" + json_data, npz_data = super().to_checkpoint(recordings=recordings) + prefix = self.id + + 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, json_data, npz): + """Restore Delay state including buffer data.""" + super().load_checkpoint(json_data, npz) + prefix = json_data["id"] + + 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 (continuous) or ring buffer lookup (discrete). diff --git a/src/pathsim/blocks/scope.py b/src/pathsim/blocks/scope.py index 4997f772..57854526 100644 --- a/src/pathsim/blocks/scope.py +++ b/src/pathsim/blocks/scope.py @@ -448,13 +448,49 @@ def save(self, path="scope.csv"): wrt.writerow(sample) + def to_checkpoint(self, recordings=False): + """Serialize Scope state including optional recording data.""" + json_data, npz_data = super().to_checkpoint(recordings=recordings) + prefix = self.id + + 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, json_data, npz): + """Restore Scope state including optional recording data.""" + super().load_checkpoint(json_data, npz) + prefix = json_data["id"] + + 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]] + else: + self.recording_time = [] + self.recording_data = [] + + 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/spectrum.py b/src/pathsim/blocks/spectrum.py index 7b3a0878..b4d37fed 100644 --- a/src/pathsim/blocks/spectrum.py +++ b/src/pathsim/blocks/spectrum.py @@ -283,6 +283,24 @@ def step(self, t, dt): return True, 0.0, None + def to_checkpoint(self, recordings=False): + """Serialize Spectrum state including integration time.""" + json_data, npz_data = super().to_checkpoint(recordings=recordings) + + json_data["time"] = self.time + json_data["t_sample"] = self.t_sample + + return json_data, npz_data + + + def load_checkpoint(self, json_data, npz): + """Restore Spectrum state including integration time.""" + super().load_checkpoint(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 dc60d887..3ce04b07 100644 --- a/src/pathsim/blocks/switch.py +++ b/src/pathsim/blocks/switch.py @@ -82,6 +82,15 @@ def select(self, switch_state=0): self.switch_state = switch_state + def to_checkpoint(self, recordings=False): + json_data, npz_data = super().to_checkpoint(recordings=recordings) + json_data["switch_state"] = self.switch_state + return json_data, npz_data + + def load_checkpoint(self, json_data, npz): + super().load_checkpoint(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. diff --git a/src/pathsim/events/_event.py b/src/pathsim/events/_event.py index fd911a5b..124c99d1 100644 --- a/src/pathsim/events/_event.py +++ b/src/pathsim/events/_event.py @@ -11,6 +11,8 @@ import numpy as np +from uuid import uuid4 + from .. _constants import EVT_TOLERANCE @@ -64,6 +66,9 @@ def __init__( tolerance=EVT_TOLERANCE ): + #unique identifier for checkpointing and diagnostics + self.id = uuid4().hex + #event detection function self.func_evt = func_evt @@ -201,4 +206,60 @@ 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): + """Serialize event state for checkpointing. + + Returns + ------- + json_data : dict + JSON-serializable metadata + npz_data : dict + numpy arrays keyed by path + """ + prefix = self.id + + #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 = { + "id": self.id, + "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, json_data, npz): + """Restore event state from checkpoint. + + Parameters + ---------- + json_data : dict + event metadata from checkpoint JSON + npz : dict-like + numpy arrays from checkpoint NPZ + """ + prefix = json_data["id"] + + 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 = [] \ No newline at end of file diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index 306c7e4c..ed6edbfb 100644 --- a/src/pathsim/simulation.py +++ b/src/pathsim/simulation.py @@ -331,6 +331,140 @@ def plot(self, *args, **kwargs): if block: block.plot(*args, **kwargs) + # checkpoint methods ---------------------------------------------------------- + + def save_checkpoint(self, path, recordings=False): + """Save simulation state to checkpoint files (JSON + NPZ). + + Creates two files: {path}.json (structure/metadata) and + {path}.npz (numerical data). + + Parameters + ---------- + path : str + base path without extension + recordings : bool + include scope/spectrum recording data (default: False) + """ + import json + + #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 UUID) + for block in self.blocks: + b_json, b_npz = block.to_checkpoint(recordings=recordings) + checkpoint["blocks"][block.id] = b_json + npz_data.update(b_npz) + + #checkpoint external events (keyed by UUID) + for event in self.events: + e_json, e_npz = event.to_checkpoint() + checkpoint["events"][event.id] = 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. The simulation must have the same + blocks and events as when the checkpoint was saved. + + Parameters + ---------- + path : str + base path without extension + """ + import json + import warnings + + #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__}'" + ) + + #restore blocks + block_data = checkpoint.get("blocks", {}) + for block in self.blocks: + if block.id in block_data: + block.load_checkpoint(block_data[block.id], npz) + else: + warnings.warn( + f"Block {block.__class__.__name__} (id={block.id[:8]}...) " + f"not found in checkpoint" + ) + + #restore external events + event_data = checkpoint.get("events", {}) + for event in self.events: + if event.id in event_data: + event.load_checkpoint(event_data[event.id], npz) + else: + warnings.warn( + f"Event {event.__class__.__name__} (id={event.id[:8]}...) " + f"not found in checkpoint" + ) + + finally: + npz.close() + + # adding system components ---------------------------------------------------- def add_block(self, block): diff --git a/src/pathsim/solvers/_solver.py b/src/pathsim/solvers/_solver.py index 9cf00de9..d235856e 100644 --- a/src/pathsim/solvers/_solver.py +++ b/src/pathsim/solvers/_solver.py @@ -353,6 +353,70 @@ 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() + + #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/gear.py b/src/pathsim/solvers/gear.py index e8cf269f..6f745371 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 n, _ in enumerate(self.history_dt, 1): + self.F[n], self.K[n] = compute_bdf_coefficients(n, np.array(self.history_dt)) + + def stages(self, t, dt): """Generator that yields the intermediate evaluation time during the timestep 't + ratio * dt'. diff --git a/src/pathsim/utils/adaptivebuffer.py b/src/pathsim/utils/adaptivebuffer.py index b24e2e5a..05dd82fa 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 @@ -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) \ No newline at end of file diff --git a/tests/pathsim/test_checkpoint.py b/tests/pathsim/test_checkpoint.py new file mode 100644 index 00000000..b0bcc470 --- /dev/null +++ b/tests/pathsim/test_checkpoint.py @@ -0,0 +1,256 @@ +"""Tests for checkpoint save/load functionality.""" + +import os +import json +import tempfile + +import numpy as np +import pytest + +from pathsim import Simulation, Connection +from pathsim.blocks import ( + Source, Integrator, Amplifier, Scope, Constant +) +from pathsim.blocks.delay import Delay +from pathsim.blocks.switch import Switch + + +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 + json_data, npz_data = b.to_checkpoint() + + assert json_data["type"] == "Integrator" + assert json_data["id"] == b.id + assert json_data["active"] is True + assert f"{b.id}/inputs" in npz_data + assert f"{b.id}/outputs" in npz_data + + def test_block_has_uuid(self): + """Each block gets a unique UUID.""" + b1 = Integrator() + b2 = Integrator() + assert b1.id != b2.id + assert len(b1.id) == 32 # hex UUID without dashes + + 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 + + json_data, npz_data = b.to_checkpoint() + + #reset block + b.reset() + assert b.inputs[0] == 0.0 + + #restore + b.load_checkpoint(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() + json_data, npz_data = b.to_checkpoint() + + b2 = Amplifier(1.0) + with pytest.raises(ValueError, match="type mismatch"): + b2.load_checkpoint(json_data, npz_data) + + +class TestEventCheckpoint: + """Test event-level checkpoint methods.""" + + def test_event_has_uuid(self): + from pathsim.events import ZeroCrossing + e = ZeroCrossing(func_evt=lambda t: t - 1.0) + assert len(e.id) == 32 + + 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 + + json_data, npz_data = e.to_checkpoint() + + e.reset() + assert e._active is True + assert len(e._times) == 0 + + e.load_checkpoint(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) + json_data, npz_data = s.to_checkpoint() + + s.select(None) + assert s.switch_state is None + + s.load_checkpoint(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 integ.id 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 f"{scope.id}/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 f"{scope.id}/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 From 93c065cdfefa44732a63096c10855f3ab51dfb51 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 09:13:55 +0100 Subject: [PATCH 46/60] Replace set with ordered list + shadow set for blocks, connections, events --- src/pathsim/simulation.py | 92 +++++++++++++++++++------------- src/pathsim/subsystem.py | 49 +++++++++-------- tests/pathsim/test_simulation.py | 16 +++--- 3 files changed, 91 insertions(+), 66 deletions(-) diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index ed6edbfb..924b4817 100644 --- a/src/pathsim/simulation.py +++ b/src/pathsim/simulation.py @@ -153,10 +153,10 @@ 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 """ @@ -176,10 +176,13 @@ def __init__( **solver_kwargs ): - #system definition - self.blocks = set() - self.connections = set() - self.events = set() + #system definition (ordered lists with shadow sets for O(1) lookup) + self.blocks = [] + self._block_set = set() + self.connections = [] + self._conn_set = set() + self.events = [] + self._event_set = set() #simulation timestep and bounds self.dt = dt @@ -215,10 +218,12 @@ def __init__( self.time = 0.0 #collection of blocks with internal ODE solvers - self._blocks_dyn = set() + self._blocks_dyn = [] + self._blocks_dyn_set = set() #collection of blocks with internal events - self._blocks_evt = set() + self._blocks_evt = [] + self._blocks_evt_set = set() #flag for setting the simulation active self._active = True @@ -269,9 +274,9 @@ def __contains__(self, other): bool """ return ( - other in self.blocks or - other in self.connections or - other in self.events + other in self._block_set or + other in self._conn_set or + other in self._event_set ) @@ -480,7 +485,7 @@ def add_block(self, block): """ #check if block already in block list - if block in self.blocks: + if block in self._block_set: _msg = f"block {block} already part of simulation" self.logger.error(_msg) raise ValueError(_msg) @@ -490,14 +495,17 @@ def add_block(self, block): #add to dynamic list if solver was initialized if block.engine: - self._blocks_dyn.add(block) + self._blocks_dyn.append(block) + self._blocks_dyn_set.add(block) #add to eventful list if internal events if block.events: - self._blocks_evt.add(block) + self._blocks_evt.append(block) + self._blocks_evt_set.add(block) #add block to global blocklist - self.blocks.add(block) + self.blocks.append(block) + self._block_set.add(block) #mark graph for rebuild if self.graph: @@ -517,19 +525,24 @@ def remove_block(self, block): """ #check if block is in block list - if block not in self.blocks: + if block not in self._block_set: _msg = f"block {block} not part of simulation" self.logger.error(_msg) raise ValueError(_msg) #remove from global blocklist - self.blocks.discard(block) + self.blocks.remove(block) + self._block_set.discard(block) #remove from dynamic list - self._blocks_dyn.discard(block) + if block in self._blocks_dyn_set: + self._blocks_dyn.remove(block) + self._blocks_dyn_set.discard(block) #remove from eventful list - self._blocks_evt.discard(block) + if block in self._blocks_evt_set: + self._blocks_evt.remove(block) + self._blocks_evt_set.discard(block) #mark graph for rebuild if self.graph: @@ -549,13 +562,14 @@ def add_connection(self, connection): """ #check if connection already in connection list - if connection in self.connections: + if connection in self._conn_set: _msg = f"{connection} already part of simulation" self.logger.error(_msg) raise ValueError(_msg) #add connection to global connection list - self.connections.add(connection) + self.connections.append(connection) + self._conn_set.add(connection) #mark graph for rebuild if self.graph: @@ -575,13 +589,14 @@ def remove_connection(self, connection): """ #check if connection is in connection list - if connection not in self.connections: + if connection not in self._conn_set: _msg = f"{connection} not part of simulation" self.logger.error(_msg) raise ValueError(_msg) #remove from global connection list - self.connections.discard(connection) + self.connections.remove(connection) + self._conn_set.discard(connection) #mark graph for rebuild if self.graph: @@ -600,13 +615,14 @@ def add_event(self, event): """ #check if event already in event list - if event in self.events: + if event in self._event_set: _msg = f"{event} already part of simulation" self.logger.error(_msg) raise ValueError(_msg) #add event to global event list - self.events.add(event) + self.events.append(event) + self._event_set.add(event) def remove_event(self, event): @@ -621,13 +637,14 @@ def remove_event(self, event): """ #check if event is in event list - if event not in self.events: + if event not in self._event_set: _msg = f"{event} not part of simulation" self.logger.error(_msg) raise ValueError(_msg) #remove from global event list - self.events.discard(event) + self.events.remove(event) + self._event_set.discard(event) # system assembly ------------------------------------------------------------- @@ -685,10 +702,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._block_set: + self.logger.warning( + f"{blk} in 'connections' but not in 'blocks'!" + ) # solver management ----------------------------------------------------------- @@ -719,13 +737,15 @@ 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 = [] + self._blocks_dyn_set = set() 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) + self._blocks_dyn_set.add(block) #logging message self.logger.info( diff --git a/src/pathsim/subsystem.py b/src/pathsim/subsystem.py index 2dd3640c..eedecbfe 100644 --- a/src/pathsim/subsystem.py +++ b/src/pathsim/subsystem.py @@ -181,27 +181,28 @@ def __init__(self, #internal algebraic loop solvers -> initialized later self.boosters = None - #internal connecions - self.connections = set() - if connections: - self.connections.update(connections) - + #internal connecions (ordered list with shadow set for O(1) lookup) + self.connections = list(connections) if connections else [] + self._conn_set = set(self.connections) + #collect and organize internal blocks - self.blocks = set() - self.interface = None + self.blocks = [] + self._block_set = set() + 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) + self._block_set.add(block) #check if interface is defined if self.interface is None: @@ -252,7 +253,7 @@ def __contains__(self, other): ------- bool """ - return other in self.blocks or other in self.connections + return other in self._block_set or other in self._conn_set # adding and removing system components --------------------------------------------------- @@ -267,7 +268,7 @@ def add_block(self, block): block : Block block to add to the subsystem """ - if block in self.blocks: + if block in self._block_set: raise ValueError(f"block {block} already part of subsystem") #initialize solver if available @@ -276,7 +277,8 @@ def add_block(self, block): if block.engine: self._blocks_dyn.append(block) - self.blocks.add(block) + self.blocks.append(block) + self._block_set.add(block) if self.graph: self._graph_dirty = True @@ -292,10 +294,11 @@ def remove_block(self, block): block : Block block to remove from the subsystem """ - if block not in self.blocks: + if block not in self._block_set: raise ValueError(f"block {block} not part of subsystem") - self.blocks.discard(block) + self.blocks.remove(block) + self._block_set.discard(block) #remove from dynamic list if hasattr(self, '_blocks_dyn') and block in self._blocks_dyn: @@ -315,10 +318,11 @@ def add_connection(self, connection): connection : Connection connection to add to the subsystem """ - if connection in self.connections: + if connection in self._conn_set: raise ValueError(f"{connection} already part of subsystem") - self.connections.add(connection) + self.connections.append(connection) + self._conn_set.add(connection) if self.graph: self._graph_dirty = True @@ -334,10 +338,11 @@ def remove_connection(self, connection): connection : Connection connection to remove from the subsystem """ - if connection not in self.connections: + if connection not in self._conn_set: raise ValueError(f"{connection} not part of subsystem") - self.connections.discard(connection) + self.connections.remove(connection) + self._conn_set.discard(connection) if self.graph: self._graph_dirty = True @@ -386,7 +391,7 @@ def _assemble_graph(self): for block in self.blocks: block.inputs.reset() - self.graph = Graph({*self.blocks, self.interface}, self.connections) + self.graph = Graph([*self.blocks, self.interface], self.connections) self._graph_dirty = False #create boosters for loop closing connections diff --git a/tests/pathsim/test_simulation.py b/tests/pathsim/test_simulation.py index beda6aae..d370b473 100644 --- a/tests/pathsim/test_simulation.py +++ b/tests/pathsim/test_simulation.py @@ -52,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) @@ -130,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): @@ -153,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): From 0f6a970f43da2b3e4e6a13f50ed0215466f401ec Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 09:20:36 +0100 Subject: [PATCH 47/60] Use type+index matching for checkpoints instead of UUIDs --- src/pathsim/blocks/_block.py | 22 ++++----- src/pathsim/blocks/delay.py | 10 ++-- src/pathsim/blocks/scope.py | 13 ++--- src/pathsim/blocks/spectrum.py | 8 ++-- src/pathsim/blocks/switch.py | 8 ++-- src/pathsim/events/_event.py | 16 ++++--- src/pathsim/simulation.py | 82 +++++++++++++++++++++++--------- tests/pathsim/test_checkpoint.py | 46 ++++++++---------- 8 files changed, 114 insertions(+), 91 deletions(-) diff --git a/src/pathsim/blocks/_block.py b/src/pathsim/blocks/_block.py index 597195a4..347be870 100644 --- a/src/pathsim/blocks/_block.py +++ b/src/pathsim/blocks/_block.py @@ -530,11 +530,13 @@ def state(self, val): # checkpoint methods ---------------------------------------------------------------- - def to_checkpoint(self, recordings=False): + 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) @@ -545,10 +547,7 @@ def to_checkpoint(self, recordings=False): npz_data : dict numpy arrays keyed by path """ - prefix = self.id - json_data = { - "id": self.id, "type": self.__class__.__name__, "active": self._active, } @@ -567,8 +566,9 @@ def to_checkpoint(self, recordings=False): #internal events if self.events: evt_jsons = [] - for event in self.events: - e_json, e_npz = event.to_checkpoint() + 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 @@ -576,18 +576,18 @@ def to_checkpoint(self, recordings=False): return json_data, npz_data - def load_checkpoint(self, json_data, npz): + 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 """ - prefix = json_data["id"] - #verify type if json_data["type"] != self.__class__.__name__: raise ValueError( @@ -611,8 +611,8 @@ def load_checkpoint(self, json_data, npz): #restore internal events if self.events and "events" in json_data: - for event, evt_data in zip(self.events, json_data["events"]): - event.load_checkpoint(evt_data, npz) + 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 ---------------------------------------- diff --git a/src/pathsim/blocks/delay.py b/src/pathsim/blocks/delay.py index 6b42614c..4e6d0a4f 100644 --- a/src/pathsim/blocks/delay.py +++ b/src/pathsim/blocks/delay.py @@ -142,10 +142,9 @@ def reset(self): self._ring.extend([0.0] * self._n) - def to_checkpoint(self, recordings=False): + def to_checkpoint(self, prefix, recordings=False): """Serialize Delay state including buffer data.""" - json_data, npz_data = super().to_checkpoint(recordings=recordings) - prefix = self.id + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) json_data["sampling_period"] = self.sampling_period @@ -160,10 +159,9 @@ def to_checkpoint(self, recordings=False): return json_data, npz_data - def load_checkpoint(self, json_data, npz): + def load_checkpoint(self, prefix, json_data, npz): """Restore Delay state including buffer data.""" - super().load_checkpoint(json_data, npz) - prefix = json_data["id"] + super().load_checkpoint(prefix, json_data, npz) if self.sampling_period is None: #continuous mode diff --git a/src/pathsim/blocks/scope.py b/src/pathsim/blocks/scope.py index 57854526..ec980785 100644 --- a/src/pathsim/blocks/scope.py +++ b/src/pathsim/blocks/scope.py @@ -448,10 +448,9 @@ def save(self, path="scope.csv"): wrt.writerow(sample) - def to_checkpoint(self, recordings=False): + def to_checkpoint(self, prefix, recordings=False): """Serialize Scope state including optional recording data.""" - json_data, npz_data = super().to_checkpoint(recordings=recordings) - prefix = self.id + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) json_data["_incremental_idx"] = self._incremental_idx if hasattr(self, '_sample_next_timestep'): @@ -464,10 +463,9 @@ def to_checkpoint(self, recordings=False): return json_data, npz_data - def load_checkpoint(self, json_data, npz): + def load_checkpoint(self, prefix, json_data, npz): """Restore Scope state including optional recording data.""" - super().load_checkpoint(json_data, npz) - prefix = json_data["id"] + super().load_checkpoint(prefix, json_data, npz) self._incremental_idx = json_data.get("_incremental_idx", 0) if hasattr(self, '_sample_next_timestep'): @@ -479,9 +477,6 @@ def load_checkpoint(self, json_data, npz): 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]] - else: - self.recording_time = [] - self.recording_data = [] def update(self, t): diff --git a/src/pathsim/blocks/spectrum.py b/src/pathsim/blocks/spectrum.py index b4d37fed..0dec61fe 100644 --- a/src/pathsim/blocks/spectrum.py +++ b/src/pathsim/blocks/spectrum.py @@ -283,9 +283,9 @@ def step(self, t, dt): return True, 0.0, None - def to_checkpoint(self, recordings=False): + def to_checkpoint(self, prefix, recordings=False): """Serialize Spectrum state including integration time.""" - json_data, npz_data = super().to_checkpoint(recordings=recordings) + json_data, npz_data = super().to_checkpoint(prefix, recordings=recordings) json_data["time"] = self.time json_data["t_sample"] = self.t_sample @@ -293,9 +293,9 @@ def to_checkpoint(self, recordings=False): return json_data, npz_data - def load_checkpoint(self, json_data, npz): + def load_checkpoint(self, prefix, json_data, npz): """Restore Spectrum state including integration time.""" - super().load_checkpoint(json_data, npz) + 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) diff --git a/src/pathsim/blocks/switch.py b/src/pathsim/blocks/switch.py index 3ce04b07..8ee707be 100644 --- a/src/pathsim/blocks/switch.py +++ b/src/pathsim/blocks/switch.py @@ -82,13 +82,13 @@ def select(self, switch_state=0): self.switch_state = switch_state - def to_checkpoint(self, recordings=False): - json_data, npz_data = super().to_checkpoint(recordings=recordings) + 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, json_data, npz): - super().load_checkpoint(json_data, npz) + 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): diff --git a/src/pathsim/events/_event.py b/src/pathsim/events/_event.py index 124c99d1..85a14625 100644 --- a/src/pathsim/events/_event.py +++ b/src/pathsim/events/_event.py @@ -211,9 +211,14 @@ def resolve(self, t): # checkpoint methods ---------------------------------------------------------------- - def to_checkpoint(self): + 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 @@ -221,15 +226,12 @@ def to_checkpoint(self): npz_data : dict numpy arrays keyed by path """ - prefix = self.id - #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 = { - "id": self.id, "type": self.__class__.__name__, "active": self._active, "history_eval": hist_eval, @@ -243,18 +245,18 @@ def to_checkpoint(self): return json_data, npz_data - def load_checkpoint(self, json_data, npz): + 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 """ - prefix = json_data["id"] - self._active = json_data["active"] self._history = json_data["history_eval"], json_data["history_time"] diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index 924b4817..279820ae 100644 --- a/src/pathsim/simulation.py +++ b/src/pathsim/simulation.py @@ -338,11 +338,34 @@ def plot(self, *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=False): """Save simulation state to checkpoint files (JSON + NPZ). Creates two files: {path}.json (structure/metadata) and - {path}.npz (numerical data). + {path}.npz (numerical data). Blocks and events are keyed by + type and insertion order for deterministic cross-instance matching. Parameters ---------- @@ -371,22 +394,28 @@ def save_checkpoint(self, path, recordings=False): "tolerance_fpi": self.tolerance_fpi, "iterations_max": self.iterations_max, }, - "blocks": {}, - "events": {}, + "blocks": [], + "events": [], } npz_data = {} - #checkpoint all blocks (keyed by UUID) + #checkpoint all blocks (keyed by type + insertion index) + type_counts = {} for block in self.blocks: - b_json, b_npz = block.to_checkpoint(recordings=recordings) - checkpoint["blocks"][block.id] = b_json + 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 UUID) + #checkpoint external events (keyed by type + insertion index) + type_counts = {} for event in self.events: - e_json, e_npz = event.to_checkpoint() - checkpoint["events"][event.id] = e_json + 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 @@ -400,8 +429,9 @@ 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. The simulation must have the same - blocks and events as when the checkpoint was saved. + 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 ---------- @@ -444,26 +474,32 @@ def load_checkpoint(self, path): f"current solver '{self.Solver.__name__}'" ) - #restore blocks - block_data = checkpoint.get("blocks", {}) + #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: - if block.id in block_data: - block.load_checkpoint(block_data[block.id], npz) + 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 {block.__class__.__name__} (id={block.id[:8]}...) " - f"not found in checkpoint" + f"Block '{key}' not found in checkpoint" ) - #restore external events - event_data = checkpoint.get("events", {}) + #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: - if event.id in event_data: - event.load_checkpoint(event_data[event.id], npz) + 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 {event.__class__.__name__} (id={event.id[:8]}...) " - f"not found in checkpoint" + f"Event '{key}' not found in checkpoint" ) finally: diff --git a/tests/pathsim/test_checkpoint.py b/tests/pathsim/test_checkpoint.py index b0bcc470..b8af6b0d 100644 --- a/tests/pathsim/test_checkpoint.py +++ b/tests/pathsim/test_checkpoint.py @@ -22,70 +22,61 @@ def test_basic_block_to_checkpoint(self): """Block produces valid checkpoint data.""" b = Integrator(1.0) b.inputs[0] = 3.14 - json_data, npz_data = b.to_checkpoint() + prefix = "Integrator_0" + json_data, npz_data = b.to_checkpoint(prefix) assert json_data["type"] == "Integrator" - assert json_data["id"] == b.id assert json_data["active"] is True - assert f"{b.id}/inputs" in npz_data - assert f"{b.id}/outputs" in npz_data - - def test_block_has_uuid(self): - """Each block gets a unique UUID.""" - b1 = Integrator() - b2 = Integrator() - assert b1.id != b2.id - assert len(b1.id) == 32 # hex UUID without dashes + 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() + json_data, npz_data = b.to_checkpoint(prefix) #reset block b.reset() assert b.inputs[0] == 0.0 #restore - b.load_checkpoint(json_data, npz_data) + 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() - json_data, npz_data = b.to_checkpoint() + 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(json_data, npz_data) + b2.load_checkpoint(prefix, json_data, npz_data) class TestEventCheckpoint: """Test event-level checkpoint methods.""" - def test_event_has_uuid(self): - from pathsim.events import ZeroCrossing - e = ZeroCrossing(func_evt=lambda t: t - 1.0) - assert len(e.id) == 32 - 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() + json_data, npz_data = e.to_checkpoint(prefix) e.reset() assert e._active is True assert len(e._times) == 0 - e.load_checkpoint(json_data, npz_data) + 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) @@ -96,12 +87,13 @@ class TestSwitchCheckpoint: def test_switch_state_preserved(self): s = Switch(switch_state=2) - json_data, npz_data = s.to_checkpoint() + prefix = "Switch_0" + json_data, npz_data = s.to_checkpoint(prefix) s.select(None) assert s.switch_state is None - s.load_checkpoint(json_data, npz_data) + s.load_checkpoint(prefix, json_data, npz_data) assert s.switch_state == 2 @@ -140,7 +132,7 @@ def test_save_load_simple(self): data = json.load(f) assert data["version"] == "1.0.0" assert data["simulation"]["time"] == time_after_run - assert integ.id in data["blocks"] + assert any(b["_key"] == "Integrator_0" for b in data["blocks"]) #reset and reload sim.time = 0.0 @@ -198,14 +190,14 @@ def test_scope_recordings(self): path1 = os.path.join(tmpdir, "no_rec") sim.save_checkpoint(path1, recordings=False) npz1 = np.load(f"{path1}.npz") - assert f"{scope.id}/recording_time" not in npz1 + 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 f"{scope.id}/recording_time" in npz2 + assert "Scope_0/recording_time" in npz2 npz2.close() def test_delay_continuous_checkpoint(self): From 11697a068f56c09812ec8db64a82447288020586 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 09:22:49 +0100 Subject: [PATCH 48/60] Move json/warnings imports to top-level, fix missing trailing newlines --- src/pathsim/events/_event.py | 2 +- src/pathsim/simulation.py | 8 +++----- src/pathsim/utils/adaptivebuffer.py | 2 +- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pathsim/events/_event.py b/src/pathsim/events/_event.py index 85a14625..58f657b5 100644 --- a/src/pathsim/events/_event.py +++ b/src/pathsim/events/_event.py @@ -264,4 +264,4 @@ def load_checkpoint(self, prefix, json_data, npz): if times_key in npz: self._times = npz[times_key].tolist() else: - self._times = [] \ No newline at end of file + self._times = [] diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index 279820ae..64402bd7 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 @@ -374,8 +377,6 @@ def save_checkpoint(self, path, recordings=False): recordings : bool include scope/spectrum recording data (default: False) """ - import json - #strip extension if provided if path.endswith('.json') or path.endswith('.npz'): path = path.rsplit('.', 1)[0] @@ -438,9 +439,6 @@ def load_checkpoint(self, path): path : str base path without extension """ - import json - import warnings - #strip extension if provided if path.endswith('.json') or path.endswith('.npz'): path = path.rsplit('.', 1)[0] diff --git a/src/pathsim/utils/adaptivebuffer.py b/src/pathsim/utils/adaptivebuffer.py index 05dd82fa..5b37fa05 100644 --- a/src/pathsim/utils/adaptivebuffer.py +++ b/src/pathsim/utils/adaptivebuffer.py @@ -163,4 +163,4 @@ def load_checkpoint(self, npz, prefix): values = npz[v_key] for t, v in zip(times, values): self.buffer_t.append(float(t)) - self.buffer_v.append(v) \ No newline at end of file + self.buffer_v.append(v) From 13189ff62023fb1470b3d01fecb7c91003fdc2bd Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 09:30:51 +0100 Subject: [PATCH 49/60] Add checkpoint overrides for FIR, KalmanFilter, noise blocks, RNG, Subsystem --- src/pathsim/blocks/fir.py | 15 ++++++ src/pathsim/blocks/kalman.py | 16 ++++++ src/pathsim/blocks/noise.py | 27 ++++++++++ src/pathsim/blocks/rng.py | 14 ++++++ src/pathsim/solvers/gear.py | 4 +- src/pathsim/subsystem.py | 96 ++++++++++++++++++++++++++++++++++++ 6 files changed, 170 insertions(+), 2 deletions(-) diff --git a/src/pathsim/blocks/fir.py b/src/pathsim/blocks/fir.py index 8db1a8a3..c2766bc1 100644 --- a/src/pathsim/blocks/fir.py +++ b/src/pathsim/blocks/fir.py @@ -114,6 +114,21 @@ 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 diff --git a/src/pathsim/blocks/kalman.py b/src/pathsim/blocks/kalman.py index 783ae537..98374a38 100644 --- a/src/pathsim/blocks/kalman.py +++ b/src/pathsim/blocks/kalman.py @@ -143,6 +143,22 @@ 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/noise.py b/src/pathsim/blocks/noise.py index 101828ea..555eb5be 100644 --- a/src/pathsim/blocks/noise.py +++ b/src/pathsim/blocks/noise.py @@ -44,6 +44,17 @@ class WhiteNoise(Block): random seed for reproducibility """ + 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) + input_port_labels = {} output_port_labels = {"out": 0} @@ -156,6 +167,22 @@ class PinkNoise(Block): random seed for reproducibility """ + 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"] + input_port_labels = {} output_port_labels = {"out": 0} diff --git a/src/pathsim/blocks/rng.py b/src/pathsim/blocks/rng.py index 5841b5a5..974e181e 100644 --- a/src/pathsim/blocks/rng.py +++ b/src/pathsim/blocks/rng.py @@ -96,6 +96,20 @@ 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/solvers/gear.py b/src/pathsim/solvers/gear.py index 6f745371..22968194 100644 --- a/src/pathsim/solvers/gear.py +++ b/src/pathsim/solvers/gear.py @@ -250,8 +250,8 @@ def load_checkpoint(self, json_data, npz, prefix): #recompute BDF coefficients from restored history if not self._needs_startup and len(self.history_dt) > 0: self.F, self.K = {}, {} - for n, _ in enumerate(self.history_dt, 1): - self.F[n], self.K[n] = compute_bdf_coefficients(n, np.array(self.history_dt)) + 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): diff --git a/src/pathsim/subsystem.py b/src/pathsim/subsystem.py index eedecbfe..b515bd34 100644 --- a/src/pathsim/subsystem.py +++ b/src/pathsim/subsystem.py @@ -462,6 +462,102 @@ def reset(self): block.reset() + 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: + type_name = block.__class__.__name__ + idx = type_counts.get(type_name, 0) + type_counts[type_name] = idx + 1 + key = f"{prefix}/{type_name}_{idx}" + 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: + type_name = block.__class__.__name__ + idx = type_counts.get(type_name, 0) + type_counts[type_name] = idx + 1 + key = f"{prefix}/{type_name}_{idx}" + 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'. From 00e4f30c9b3d34330b5bbc36597c434b0f64d04e Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 09:33:02 +0100 Subject: [PATCH 50/60] Add comprehensive checkpoint tests: cross-instance, FIR, Kalman, noise, Subsystem --- tests/pathsim/test_checkpoint.py | 280 ++++++++++++++++++++++++++++++- 1 file changed, 278 insertions(+), 2 deletions(-) diff --git a/tests/pathsim/test_checkpoint.py b/tests/pathsim/test_checkpoint.py index b8af6b0d..ac4df056 100644 --- a/tests/pathsim/test_checkpoint.py +++ b/tests/pathsim/test_checkpoint.py @@ -7,12 +7,16 @@ import numpy as np import pytest -from pathsim import Simulation, Connection +from pathsim import Simulation, Connection, Subsystem, Interface from pathsim.blocks import ( - Source, Integrator, Amplifier, Scope, Constant + 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: @@ -246,3 +250,275 @@ def test_delay_discrete_checkpoint(self): 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 + ) + + #UUIDs differ + assert src1.id != src2.id + assert integ1.id != integ2.id + + 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) From 528b26ed6b352d2bd73990b60e3d9f565da57e65 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 09:42:02 +0100 Subject: [PATCH 51/60] Clean up: remove dead UUID code, fix method placement and spacing, deduplicate checkpoint key logic --- src/pathsim/blocks/_block.py | 4 --- src/pathsim/blocks/fir.py | 3 +- src/pathsim/blocks/kalman.py | 1 + src/pathsim/blocks/noise.py | 60 +++++++++++++++++--------------- src/pathsim/blocks/rng.py | 1 + src/pathsim/blocks/switch.py | 1 + src/pathsim/events/_event.py | 5 --- src/pathsim/subsystem.py | 20 ++++++----- tests/pathsim/test_checkpoint.py | 4 --- 9 files changed, 49 insertions(+), 50 deletions(-) diff --git a/src/pathsim/blocks/_block.py b/src/pathsim/blocks/_block.py index 347be870..6a3c3e3c 100644 --- a/src/pathsim/blocks/_block.py +++ b/src/pathsim/blocks/_block.py @@ -11,7 +11,6 @@ # IMPORTS =============================================================================== import inspect -from uuid import uuid4 from functools import lru_cache from ..utils.deprecation import deprecated @@ -85,9 +84,6 @@ class definition for other blocks to be inherited. def __init__(self): - #unique identifier for checkpointing and diagnostics - self.id = uuid4().hex - #registers to hold input and output values self.inputs = Register( mapping=self.input_port_labels and self.input_port_labels.copy() diff --git a/src/pathsim/blocks/fir.py b/src/pathsim/blocks/fir.py index c2766bc1..c9dc8ff7 100644 --- a/src/pathsim/blocks/fir.py +++ b/src/pathsim/blocks/fir.py @@ -120,6 +120,7 @@ def to_checkpoint(self, prefix, recordings=False): 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) @@ -131,4 +132,4 @@ def load_checkpoint(self, prefix, json_data, npz): 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/kalman.py b/src/pathsim/blocks/kalman.py index 98374a38..c835a7cc 100644 --- a/src/pathsim/blocks/kalman.py +++ b/src/pathsim/blocks/kalman.py @@ -150,6 +150,7 @@ def to_checkpoint(self, prefix, recordings=False): 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) diff --git a/src/pathsim/blocks/noise.py b/src/pathsim/blocks/noise.py index 555eb5be..0206a1b6 100644 --- a/src/pathsim/blocks/noise.py +++ b/src/pathsim/blocks/noise.py @@ -44,17 +44,6 @@ class WhiteNoise(Block): random seed for reproducibility """ - 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) - input_port_labels = {} output_port_labels = {"out": 0} @@ -135,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. @@ -167,22 +169,6 @@ class PinkNoise(Block): random seed for reproducibility """ - 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"] - input_port_labels = {} output_port_labels = {"out": 0} @@ -295,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/rng.py b/src/pathsim/blocks/rng.py index 974e181e..72824107 100644 --- a/src/pathsim/blocks/rng.py +++ b/src/pathsim/blocks/rng.py @@ -103,6 +103,7 @@ def to_checkpoint(self, prefix, recordings=False): 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) diff --git a/src/pathsim/blocks/switch.py b/src/pathsim/blocks/switch.py index 8ee707be..f89f28ca 100644 --- a/src/pathsim/blocks/switch.py +++ b/src/pathsim/blocks/switch.py @@ -87,6 +87,7 @@ def to_checkpoint(self, prefix, recordings=False): 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) diff --git a/src/pathsim/events/_event.py b/src/pathsim/events/_event.py index 58f657b5..1ebd1a9e 100644 --- a/src/pathsim/events/_event.py +++ b/src/pathsim/events/_event.py @@ -11,8 +11,6 @@ import numpy as np -from uuid import uuid4 - from .. _constants import EVT_TOLERANCE @@ -66,9 +64,6 @@ def __init__( tolerance=EVT_TOLERANCE ): - #unique identifier for checkpointing and diagnostics - self.id = uuid4().hex - #event detection function self.func_evt = func_evt diff --git a/src/pathsim/subsystem.py b/src/pathsim/subsystem.py index b515bd34..cea9740c 100644 --- a/src/pathsim/subsystem.py +++ b/src/pathsim/subsystem.py @@ -462,6 +462,16 @@ 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. @@ -494,10 +504,7 @@ def to_checkpoint(self, prefix, recordings=False): #checkpoint internal blocks by type + insertion order type_counts = {} for block in self.blocks: - type_name = block.__class__.__name__ - idx = type_counts.get(type_name, 0) - type_counts[type_name] = idx + 1 - key = f"{prefix}/{type_name}_{idx}" + 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) @@ -545,10 +552,7 @@ def load_checkpoint(self, prefix, json_data, npz): block_data = {b["_key"]: b for b in json_data.get("blocks", [])} type_counts = {} for block in self.blocks: - type_name = block.__class__.__name__ - idx = type_counts.get(type_name, 0) - type_counts[type_name] = idx + 1 - key = f"{prefix}/{type_name}_{idx}" + key = f"{prefix}/{self._checkpoint_key(block.__class__.__name__, type_counts)}" if key in block_data: block.load_checkpoint(key, block_data[key], npz) diff --git a/tests/pathsim/test_checkpoint.py b/tests/pathsim/test_checkpoint.py index ac4df056..6a930198 100644 --- a/tests/pathsim/test_checkpoint.py +++ b/tests/pathsim/test_checkpoint.py @@ -277,10 +277,6 @@ def test_cross_instance_load(self): dt=0.01 ) - #UUIDs differ - assert src1.id != src2.id - assert integ1.id != integ2.id - sim2.load_checkpoint(path) assert sim2.time == saved_time assert np.allclose(integ2.state, saved_state) From 18e6ef10343f4d17c82612bf55787dd80252edea Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 10:21:46 +0100 Subject: [PATCH 52/60] Fix solver n restoration, add GEAR/Spectrum/Scope/event coverage tests --- src/pathsim/solvers/_solver.py | 1 + tests/pathsim/test_checkpoint.py | 223 +++++++++++++++++++++++++++++++ 2 files changed, 224 insertions(+) diff --git a/src/pathsim/solvers/_solver.py b/src/pathsim/solvers/_solver.py index d235856e..d10bf16e 100644 --- a/src/pathsim/solvers/_solver.py +++ b/src/pathsim/solvers/_solver.py @@ -403,6 +403,7 @@ def load_checkpoint(self, json_data, npz, prefix): """ 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: diff --git a/tests/pathsim/test_checkpoint.py b/tests/pathsim/test_checkpoint.py index 6a930198..db480d44 100644 --- a/tests/pathsim/test_checkpoint.py +++ b/tests/pathsim/test_checkpoint.py @@ -518,3 +518,226 @@ def test_subsystem_cross_instance(self): ) 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) + + +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) From 1cb68470aacac59cc7130ef7b7852b086f63e803 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 10:43:22 +0100 Subject: [PATCH 53/60] Add checkpoint example notebook for docs --- docs/source/examples/checkpoints.ipynb | 211 +++++++++++++++++++++++++ 1 file changed, 211 insertions(+) create mode 100644 docs/source/examples/checkpoints.ipynb diff --git a/docs/source/examples/checkpoints.ipynb b/docs/source/examples/checkpoints.ipynb new file mode 100644 index 00000000..e77eb6b8 --- /dev/null +++ b/docs/source/examples/checkpoints.ipynb @@ -0,0 +1,211 @@ +{ + "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 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": [ + "## Setup\n", + "\n", + "We'll use a damped harmonic oscillator as our test system. First, let's run it continuously for 25 seconds as our reference." + ] + }, + { + "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 Integrator, Amplifier, Adder, Scope" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# System parameters\n", + "x0, v0 = 2, 5\n", + "m, c, k = 0.8, 0.2, 1.5\n", + "\n", + "def make_system():\n", + " \"\"\"Helper to create a fresh harmonic oscillator simulation.\"\"\"\n", + " I1 = Integrator(v0)\n", + " I2 = Integrator(x0)\n", + " A1 = Amplifier(c)\n", + " A2 = Amplifier(k)\n", + " A3 = Amplifier(-1/m)\n", + " P1 = Adder()\n", + " Sc = Scope(labels=[\"velocity\", \"position\"])\n", + "\n", + " blocks = [I1, I2, A1, A2, A3, P1, Sc]\n", + " connections = [\n", + " Connection(I1, I2, A1, Sc), \n", + " Connection(I2, A2, Sc[1]),\n", + " Connection(A1, P1), \n", + " Connection(A2, P1[1]), \n", + " Connection(P1, A3),\n", + " Connection(A3, I1)\n", + " ]\n", + "\n", + " sim = Simulation(blocks, connections, dt=0.01)\n", + " return sim, Sc" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Reference Run\n", + "\n", + "Run the full simulation continuously for 25 seconds. This is our ground truth." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sim_ref, scope_ref = make_system()\n", + "sim_ref.run(25)\n", + "\n", + "time_ref, data_ref = scope_ref.read()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Save Checkpoint\n", + "\n", + "Now let's run a second simulation, but only for the first 10 seconds. Then we save a checkpoint." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sim_a, scope_a = make_system()\n", + "sim_a.run(10)\n", + "\n", + "# Save checkpoint (creates checkpoint.json and checkpoint.npz)\n", + "sim_a.save_checkpoint(\"checkpoint\")\n", + "print(f\"Saved checkpoint at t = {sim_a.time:.1f}s\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load Checkpoint and Resume\n", + "\n", + "Create an entirely new simulation with fresh block objects, load the checkpoint, and continue for the remaining 15 seconds. The key point is that the new simulation has completely different Python objects, yet the checkpoint restores the exact state by matching blocks by type and insertion order." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "sim_b, scope_b = make_system()\n", + "\n", + "# Load checkpoint into the fresh simulation\n", + "sim_b.load_checkpoint(\"checkpoint\")\n", + "print(f\"Resumed from t = {sim_b.time:.1f}s\")\n", + "\n", + "# Continue the simulation for the remaining 15 seconds\n", + "sim_b.run(15)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Compare Results\n", + "\n", + "Now let's overlay the reference (continuous run) with the checkpointed run (first 10s + resumed 15s). If checkpointing works correctly, they should be identical." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": "# Read data from both scopes\ntime_a, data_a = scope_a.read()\ntime_b, data_b = scope_b.read()\n\nfig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 5), sharex=True)\n\n# Position (channel 1)\nax1.plot(time_ref, data_ref[1], \"k-\", alpha=0.3, lw=3, label=\"reference (continuous)\")\nax1.plot(time_a, data_a[1], \"C0-\", label=\"first half (0-10s)\")\nax1.plot(time_b, data_b[1], \"C1--\", label=\"resumed (10-25s)\")\nax1.axvline(10, color=\"gray\", ls=\":\", alpha=0.5, label=\"checkpoint\")\nax1.set_ylabel(\"position\")\nax1.legend(loc=\"upper right\", fontsize=8)\n\n# Velocity (channel 0)\nax2.plot(time_ref, data_ref[0], \"k-\", alpha=0.3, lw=3, label=\"reference (continuous)\")\nax2.plot(time_a, data_a[0], \"C0-\", label=\"first half (0-10s)\")\nax2.plot(time_b, data_b[0], \"C1--\", label=\"resumed (10-25s)\")\nax2.axvline(10, color=\"gray\", ls=\":\", alpha=0.5)\nax2.set_ylabel(\"velocity\")\nax2.set_xlabel(\"time [s]\")\n\nfig.suptitle(\"Checkpoint Save / Load\")\nfig.tight_layout()\nplt.show()" + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The resumed simulation (dashed) seamlessly continues the reference (gray), confirming that the complete simulation state was correctly saved and restored across different Python objects." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Checkpoint File Contents\n", + "\n", + "The JSON file contains human-readable metadata about the simulation state. Let's inspect it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "\n", + "with open(\"checkpoint.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: {len(cp['blocks'])}\")\n", + "for b in cp[\"blocks\"]:\n", + " print(f\" {b['_key']} ({b['type']})\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Blocks are matched by type and insertion order (`Integrator_0`, `Integrator_1`, etc.), which means the checkpoint can be loaded into any simulation with the same block structure, regardless of the specific Python objects." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} \ No newline at end of file From 456e7cef3c14cb7a2294d15c48a23f44e5a49a44 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 10:55:55 +0100 Subject: [PATCH 54/60] Rework checkpoint notebook: driven oscillator with rollback scenarios --- docs/source/examples/checkpoints.ipynb | 100 +++---------------------- 1 file changed, 12 insertions(+), 88 deletions(-) diff --git a/docs/source/examples/checkpoints.ipynb b/docs/source/examples/checkpoints.ipynb index e77eb6b8..aaa86177 100644 --- a/docs/source/examples/checkpoints.ipynb +++ b/docs/source/examples/checkpoints.ipynb @@ -3,22 +3,12 @@ { "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 use a split format: a JSON file for metadata and structure, and an NPZ file for numerical data (block states, solver histories, etc.)." - ] + "source": "# Checkpoints\n\nPathSim 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\nCheckpoints also enable **rollback**, where you return to a saved state and explore different what-if scenarios by changing parameters.\n\nCheckpoints 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": [ - "## Setup\n", - "\n", - "We'll use a damped harmonic oscillator as our test system. First, let's run it continuously for 25 seconds as our reference." - ] + "source": "## Setup\n\nWe'll simulate a driven harmonic oscillator — a mass-spring system excited by an external sinusoidal force. The system produces a sustained periodic response, making it easy to visually verify that checkpoints preserve continuity." }, { "cell_type": "code", @@ -38,126 +28,60 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "# System parameters\n", - "x0, v0 = 2, 5\n", - "m, c, k = 0.8, 0.2, 1.5\n", - "\n", - "def make_system():\n", - " \"\"\"Helper to create a fresh harmonic oscillator simulation.\"\"\"\n", - " I1 = Integrator(v0)\n", - " I2 = Integrator(x0)\n", - " A1 = Amplifier(c)\n", - " A2 = Amplifier(k)\n", - " A3 = Amplifier(-1/m)\n", - " P1 = Adder()\n", - " Sc = Scope(labels=[\"velocity\", \"position\"])\n", - "\n", - " blocks = [I1, I2, A1, A2, A3, P1, Sc]\n", - " connections = [\n", - " Connection(I1, I2, A1, Sc), \n", - " Connection(I2, A2, Sc[1]),\n", - " Connection(A1, P1), \n", - " Connection(A2, P1[1]), \n", - " Connection(P1, A3),\n", - " Connection(A3, I1)\n", - " ]\n", - "\n", - " sim = Simulation(blocks, connections, dt=0.01)\n", - " return sim, Sc" - ] + "source": "import numpy as np\n\n# System parameters\nm = 1.0 # mass\nc = 0.1 # light damping\nk = 4.0 # spring stiffness\nF0 = 1.0 # forcing amplitude\nw = 1.8 # forcing frequency (near resonance for k/m=4 -> w0=2)\n\ndef make_system(damping=c, stiffness=k):\n \"\"\"Create a driven harmonic oscillator with configurable parameters.\"\"\"\n from pathsim.blocks import Source, Integrator, Amplifier, Adder, Scope\n\n Src = Source(lambda t: F0/m * np.sin(w * t)) # external acceleration\n I1 = Integrator(0.0) # velocity\n I2 = Integrator(0.5) # position (start displaced)\n Ac = Amplifier(-damping/m)\n Ak = Amplifier(-stiffness/m)\n P1 = Adder()\n Sc = Scope(labels=[\"position\"])\n\n blocks = [Src, I1, I2, Ac, Ak, P1, Sc]\n connections = [\n Connection(I1, I2, Ac), # velocity -> position integrator, damper\n Connection(I2, Ak, Sc), # position -> spring, scope\n Connection(Ac, P1), # -c/m * v -> adder\n Connection(Ak, P1[1]), # -k/m * x -> adder\n Connection(Src, P1[2]), # F/m -> adder\n Connection(P1, I1), # acceleration -> velocity integrator\n ]\n\n sim = Simulation(blocks, connections, dt=0.01)\n return sim, Sc" }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Reference Run\n", - "\n", - "Run the full simulation continuously for 25 seconds. This is our ground truth." - ] + "source": "## Save Checkpoint\n\nRun the simulation for 20 seconds, then save a checkpoint. The system will be in a sustained oscillation by this point." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "sim_ref, scope_ref = make_system()\n", - "sim_ref.run(25)\n", - "\n", - "time_ref, data_ref = scope_ref.read()" - ] + "source": "sim, scope = make_system()\nsim.run(20)\n\n# Save checkpoint\nsim.save_checkpoint(\"checkpoint\")\nprint(f\"Saved checkpoint at t = {sim.time:.1f}s\")" }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Save Checkpoint\n", - "\n", - "Now let's run a second simulation, but only for the first 10 seconds. Then we save a checkpoint." - ] + "source": "## Resume from Checkpoint\n\nLoad the checkpoint into a fresh simulation and continue for another 20 seconds. The new simulation has completely different Python objects, yet the checkpoint restores the exact state by matching blocks by type and insertion order." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "sim_a, scope_a = make_system()\n", - "sim_a.run(10)\n", - "\n", - "# Save checkpoint (creates checkpoint.json and checkpoint.npz)\n", - "sim_a.save_checkpoint(\"checkpoint\")\n", - "print(f\"Saved checkpoint at t = {sim_a.time:.1f}s\")" - ] + "source": "sim_resumed, scope_resumed = make_system()\nsim_resumed.load_checkpoint(\"checkpoint\")\nprint(f\"Resumed from t = {sim_resumed.time:.1f}s\")\n\nsim_resumed.run(20)" }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Load Checkpoint and Resume\n", - "\n", - "Create an entirely new simulation with fresh block objects, load the checkpoint, and continue for the remaining 15 seconds. The key point is that the new simulation has completely different Python objects, yet the checkpoint restores the exact state by matching blocks by type and insertion order." - ] + "source": "## Rollback: What-If Scenarios\n\nThis is where checkpoints really shine. We reload the same checkpoint but with **different parameters** — increasing the damping significantly. Both branches start from the exact same state at t=20, but evolve differently." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": [ - "sim_b, scope_b = make_system()\n", - "\n", - "# Load checkpoint into the fresh simulation\n", - "sim_b.load_checkpoint(\"checkpoint\")\n", - "print(f\"Resumed from t = {sim_b.time:.1f}s\")\n", - "\n", - "# Continue the simulation for the remaining 15 seconds\n", - "sim_b.run(15)" - ] + "source": "# Scenario A: same parameters (continuation)\nsim_a, scope_a = make_system(damping=0.1)\nsim_a.load_checkpoint(\"checkpoint\")\nsim_a.run(20)\n\n# Scenario B: increased damping (what-if)\nsim_b, scope_b = make_system(damping=1.5)\nsim_b.load_checkpoint(\"checkpoint\")\nsim_b.run(20)\n\n# Scenario C: stiffer spring (what-if)\nsim_c, scope_c = make_system(stiffness=9.0)\nsim_c.load_checkpoint(\"checkpoint\")\nsim_c.run(20)" }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "## Compare Results\n", - "\n", - "Now let's overlay the reference (continuous run) with the checkpointed run (first 10s + resumed 15s). If checkpointing works correctly, they should be identical." - ] + "source": "## Compare Results\n\nThe plot shows the original simulation (0–20s), followed by three different futures branching from the same checkpoint." }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "# Read data from both scopes\ntime_a, data_a = scope_a.read()\ntime_b, data_b = scope_b.read()\n\nfig, (ax1, ax2) = plt.subplots(2, 1, figsize=(8, 5), sharex=True)\n\n# Position (channel 1)\nax1.plot(time_ref, data_ref[1], \"k-\", alpha=0.3, lw=3, label=\"reference (continuous)\")\nax1.plot(time_a, data_a[1], \"C0-\", label=\"first half (0-10s)\")\nax1.plot(time_b, data_b[1], \"C1--\", label=\"resumed (10-25s)\")\nax1.axvline(10, color=\"gray\", ls=\":\", alpha=0.5, label=\"checkpoint\")\nax1.set_ylabel(\"position\")\nax1.legend(loc=\"upper right\", fontsize=8)\n\n# Velocity (channel 0)\nax2.plot(time_ref, data_ref[0], \"k-\", alpha=0.3, lw=3, label=\"reference (continuous)\")\nax2.plot(time_a, data_a[0], \"C0-\", label=\"first half (0-10s)\")\nax2.plot(time_b, data_b[0], \"C1--\", label=\"resumed (10-25s)\")\nax2.axvline(10, color=\"gray\", ls=\":\", alpha=0.5)\nax2.set_ylabel(\"velocity\")\nax2.set_xlabel(\"time [s]\")\n\nfig.suptitle(\"Checkpoint Save / Load\")\nfig.tight_layout()\nplt.show()" + "source": "time_orig, data_orig = scope.read()\ntime_a, data_a = scope_a.read()\ntime_b, data_b = scope_b.read()\ntime_c, data_c = scope_c.read()\n\nfig, ax = plt.subplots(figsize=(10, 4))\n\n# Original run (0-20s)\nax.plot(time_orig, data_orig[0], \"k-\", lw=1.5, label=\"original (c=0.1, k=4)\")\n\n# Three futures from checkpoint\nax.plot(time_a, data_a[0], \"C0-\", alpha=0.8, label=\"resumed (c=0.1, k=4)\")\nax.plot(time_b, data_b[0], \"C1-\", alpha=0.8, label=\"what-if: heavy damping (c=1.5)\")\nax.plot(time_c, data_c[0], \"C2-\", alpha=0.8, label=\"what-if: stiffer spring (k=9)\")\n\nax.axvline(20, color=\"gray\", ls=\":\", alpha=0.5, lw=2, label=\"checkpoint (t=20s)\")\nax.set_xlabel(\"time [s]\")\nax.set_ylabel(\"position\")\nax.set_title(\"Checkpoint Rollback: Three Futures from the Same State\")\nax.legend(loc=\"upper right\", fontsize=8)\nfig.tight_layout()\nplt.show()" }, { "cell_type": "markdown", "metadata": {}, - "source": [ - "The resumed simulation (dashed) seamlessly continues the reference (gray), confirming that the complete simulation state was correctly saved and restored across different Python objects." - ] + "source": "All three scenarios start from the exact same state at t=20s. The blue continuation matches the original trajectory perfectly, while the heavy damping scenario (orange) decays rapidly and the stiffer spring scenario (green) shifts to a higher natural frequency." }, { "cell_type": "markdown", From c88fc29ef9e391e1baa2d8c3acc1eebcf1f255c3 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 10:59:27 +0100 Subject: [PATCH 55/60] Rewrite checkpoint notebook: coupled oscillators, flat style, rollback demo --- docs/source/examples/checkpoints.ipynb | 229 ++++++++++++++++++++++--- 1 file changed, 201 insertions(+), 28 deletions(-) diff --git a/docs/source/examples/checkpoints.ipynb b/docs/source/examples/checkpoints.ipynb index aaa86177..7d7f4eb7 100644 --- a/docs/source/examples/checkpoints.ipynb +++ b/docs/source/examples/checkpoints.ipynb @@ -3,12 +3,31 @@ { "cell_type": "markdown", "metadata": {}, - "source": "# Checkpoints\n\nPathSim 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\nCheckpoints also enable **rollback**, where you return to a saved state and explore different what-if scenarios by changing parameters.\n\nCheckpoints use a split format: a JSON file for metadata and structure, and an NPZ file for numerical data (block states, solver histories, etc.)." + "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": "## Setup\n\nWe'll simulate a driven harmonic oscillator — a mass-spring system excited by an external sinusoidal force. The system produces a sustained periodic response, making it easy to visually verify that checkpoints preserve continuity." + "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", @@ -20,7 +39,14 @@ "import matplotlib.pyplot as plt\n", "\n", "from pathsim import Simulation, Connection\n", - "from pathsim.blocks import Integrator, Amplifier, Adder, Scope" + "from pathsim.blocks import ODE, Function, Scope" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Define the system parameters:" ] }, { @@ -28,68 +54,207 @@ "execution_count": null, "metadata": {}, "outputs": [], - "source": "import numpy as np\n\n# System parameters\nm = 1.0 # mass\nc = 0.1 # light damping\nk = 4.0 # spring stiffness\nF0 = 1.0 # forcing amplitude\nw = 1.8 # forcing frequency (near resonance for k/m=4 -> w0=2)\n\ndef make_system(damping=c, stiffness=k):\n \"\"\"Create a driven harmonic oscillator with configurable parameters.\"\"\"\n from pathsim.blocks import Source, Integrator, Amplifier, Adder, Scope\n\n Src = Source(lambda t: F0/m * np.sin(w * t)) # external acceleration\n I1 = Integrator(0.0) # velocity\n I2 = Integrator(0.5) # position (start displaced)\n Ac = Amplifier(-damping/m)\n Ak = Amplifier(-stiffness/m)\n P1 = Adder()\n Sc = Scope(labels=[\"position\"])\n\n blocks = [Src, I1, I2, Ac, Ak, P1, Sc]\n connections = [\n Connection(I1, I2, Ac), # velocity -> position integrator, damper\n Connection(I2, Ak, Sc), # position -> spring, scope\n Connection(Ac, P1), # -c/m * v -> adder\n Connection(Ak, P1[1]), # -k/m * x -> adder\n Connection(Src, P1[2]), # F/m -> adder\n Connection(P1, I1), # acceleration -> velocity integrator\n ]\n\n sim = Simulation(blocks, connections, dt=0.01)\n return sim, Sc" + "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": "markdown", + "cell_type": "raw", "metadata": {}, - "source": "## Save Checkpoint\n\nRun the simulation for 20 seconds, then save a checkpoint. The system will be in a sustained oscillation by this point." + "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": "sim, scope = make_system()\nsim.run(20)\n\n# Save checkpoint\nsim.save_checkpoint(\"checkpoint\")\nprint(f\"Saved checkpoint at t = {sim.time:.1f}s\")" + "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": "markdown", + "cell_type": "raw", "metadata": {}, - "source": "## Resume from Checkpoint\n\nLoad the checkpoint into a fresh simulation and continue for another 20 seconds. The new simulation has completely different Python objects, yet the checkpoint restores the exact state by matching blocks by type and insertion order." + "source": [ + "Create the :class:`.Simulation` and run for 60 seconds:" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "sim_resumed, scope_resumed = make_system()\nsim_resumed.load_checkpoint(\"checkpoint\")\nprint(f\"Resumed from t = {sim_resumed.time:.1f}s\")\n\nsim_resumed.run(20)" + "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": "## Rollback: What-If Scenarios\n\nThis is where checkpoints really shine. We reload the same checkpoint but with **different parameters** — increasing the damping significantly. Both branches start from the exact same state at t=20, but evolve differently." + "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": "# Scenario A: same parameters (continuation)\nsim_a, scope_a = make_system(damping=0.1)\nsim_a.load_checkpoint(\"checkpoint\")\nsim_a.run(20)\n\n# Scenario B: increased damping (what-if)\nsim_b, scope_b = make_system(damping=1.5)\nsim_b.load_checkpoint(\"checkpoint\")\nsim_b.run(20)\n\n# Scenario C: stiffer spring (what-if)\nsim_c, scope_c = make_system(stiffness=9.0)\nsim_c.load_checkpoint(\"checkpoint\")\nsim_c.run(20)" + "source": [ + "sim.save_checkpoint(\"coupled\")\n", + "print(f\"Checkpoint saved at t = {sim.time:.1f}s\")" + ] }, { "cell_type": "markdown", "metadata": {}, - "source": "## Compare Results\n\nThe plot shows the original simulation (0–20s), followed by three different futures branching from the same checkpoint." + "source": [ + "We can inspect the JSON file to see what was saved:" + ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], - "source": "time_orig, data_orig = scope.read()\ntime_a, data_a = scope_a.read()\ntime_b, data_b = scope_b.read()\ntime_c, data_c = scope_c.read()\n\nfig, ax = plt.subplots(figsize=(10, 4))\n\n# Original run (0-20s)\nax.plot(time_orig, data_orig[0], \"k-\", lw=1.5, label=\"original (c=0.1, k=4)\")\n\n# Three futures from checkpoint\nax.plot(time_a, data_a[0], \"C0-\", alpha=0.8, label=\"resumed (c=0.1, k=4)\")\nax.plot(time_b, data_b[0], \"C1-\", alpha=0.8, label=\"what-if: heavy damping (c=1.5)\")\nax.plot(time_c, data_c[0], \"C2-\", alpha=0.8, label=\"what-if: stiffer spring (k=9)\")\n\nax.axvline(20, color=\"gray\", ls=\":\", alpha=0.5, lw=2, label=\"checkpoint (t=20s)\")\nax.set_xlabel(\"time [s]\")\nax.set_ylabel(\"position\")\nax.set_title(\"Checkpoint Rollback: Three Futures from the Same State\")\nax.legend(loc=\"upper right\", fontsize=8)\nfig.tight_layout()\nplt.show()" + "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": "All three scenarios start from the exact same state at t=20s. The blue continuation matches the original trajectory perfectly, while the heavy damping scenario (orange) decays rapidly and the stiffer spring scenario (green) shifts to a higher natural frequency." + "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": [ - "## Checkpoint File Contents\n", + "## Comparing the Scenarios\n", "\n", - "The JSON file contains human-readable metadata about the simulation state. Let's inspect it." + "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." ] }, { @@ -98,24 +263,32 @@ "metadata": {}, "outputs": [], "source": [ - "import json\n", + "time_orig, data_orig = sc.read()\n", "\n", - "with open(\"checkpoint.json\") as f:\n", - " cp = json.load(f)\n", + "fig, ax = plt.subplots(figsize=(10, 4))\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: {len(cp['blocks'])}\")\n", - "for b in cp[\"blocks\"]:\n", - " print(f\" {b['_key']} ({b['type']})\")" + "# 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": [ - "Blocks are matched by type and insertion order (`Integrator_0`, `Integrator_1`, etc.), which means the checkpoint can be loaded into any simulation with the same block structure, regardless of the specific Python objects." + "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." ] } ], @@ -132,4 +305,4 @@ }, "nbformat": 4, "nbformat_minor": 4 -} \ No newline at end of file +} From 81f2cff62196a3b9ae0372fc9b88b575da41c2d5 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 11:14:34 +0100 Subject: [PATCH 56/60] Include scope recordings in checkpoints by default --- src/pathsim/simulation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index 64402bd7..ed50a5ae 100644 --- a/src/pathsim/simulation.py +++ b/src/pathsim/simulation.py @@ -363,7 +363,7 @@ class name of the block or event return f"{type_name}_{idx}" - def save_checkpoint(self, path, recordings=False): + def save_checkpoint(self, path, recordings=True): """Save simulation state to checkpoint files (JSON + NPZ). Creates two files: {path}.json (structure/metadata) and @@ -375,7 +375,7 @@ def save_checkpoint(self, path, recordings=False): path : str base path without extension recordings : bool - include scope/spectrum recording data (default: False) + include scope/spectrum recording data (default: True) """ #strip extension if provided if path.endswith('.json') or path.endswith('.npz'): From 64efb9168ae5eacd34bf0890ee515467b22c82fc Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Tue, 17 Mar 2026 11:18:35 +0100 Subject: [PATCH 57/60] Add test verifying recordings are included by default --- tests/pathsim/test_checkpoint.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/pathsim/test_checkpoint.py b/tests/pathsim/test_checkpoint.py index db480d44..94fed3d6 100644 --- a/tests/pathsim/test_checkpoint.py +++ b/tests/pathsim/test_checkpoint.py @@ -669,6 +669,32 @@ def test_scope_recordings_roundtrip(self): 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.""" From 9fe1d2617a21a8016d7a64ca5ad571caf17ae0e1 Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Thu, 19 Mar 2026 11:36:49 +0100 Subject: [PATCH 58/60] Drop shadow sets, use plain lists for blocks/connections/events --- src/pathsim/simulation.py | 43 ++++++++++++--------------------------- src/pathsim/subsystem.py | 23 ++++++++------------- 2 files changed, 21 insertions(+), 45 deletions(-) diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index ed50a5ae..6f6dc9ed 100644 --- a/src/pathsim/simulation.py +++ b/src/pathsim/simulation.py @@ -179,13 +179,10 @@ def __init__( **solver_kwargs ): - #system definition (ordered lists with shadow sets for O(1) lookup) + #system definition self.blocks = [] - self._block_set = set() self.connections = [] - self._conn_set = set() self.events = [] - self._event_set = set() #simulation timestep and bounds self.dt = dt @@ -222,11 +219,9 @@ def __init__( #collection of blocks with internal ODE solvers self._blocks_dyn = [] - self._blocks_dyn_set = set() #collection of blocks with internal events self._blocks_evt = [] - self._blocks_evt_set = set() #flag for setting the simulation active self._active = True @@ -277,9 +272,9 @@ def __contains__(self, other): bool """ return ( - other in self._block_set or - other in self._conn_set or - other in self._event_set + other in self.blocks or + other in self.connections or + other in self.events ) @@ -519,7 +514,7 @@ def add_block(self, block): """ #check if block already in block list - if block in self._block_set: + if block in self.blocks: _msg = f"block {block} already part of simulation" self.logger.error(_msg) raise ValueError(_msg) @@ -530,16 +525,13 @@ def add_block(self, block): #add to dynamic list if solver was initialized if block.engine: self._blocks_dyn.append(block) - self._blocks_dyn_set.add(block) #add to eventful list if internal events if block.events: self._blocks_evt.append(block) - self._blocks_evt_set.add(block) #add block to global blocklist self.blocks.append(block) - self._block_set.add(block) #mark graph for rebuild if self.graph: @@ -559,24 +551,21 @@ def remove_block(self, block): """ #check if block is in block list - if block not in self._block_set: + 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) - self._block_set.discard(block) #remove from dynamic list - if block in self._blocks_dyn_set: + if block in self._blocks_dyn: self._blocks_dyn.remove(block) - self._blocks_dyn_set.discard(block) #remove from eventful list - if block in self._blocks_evt_set: + if block in self._blocks_evt: self._blocks_evt.remove(block) - self._blocks_evt_set.discard(block) #mark graph for rebuild if self.graph: @@ -596,14 +585,13 @@ def add_connection(self, connection): """ #check if connection already in connection list - if connection in self._conn_set: + if connection in self.connections: _msg = f"{connection} already part of simulation" self.logger.error(_msg) raise ValueError(_msg) #add connection to global connection list self.connections.append(connection) - self._conn_set.add(connection) #mark graph for rebuild if self.graph: @@ -623,14 +611,13 @@ def remove_connection(self, connection): """ #check if connection is in connection list - if connection not in self._conn_set: + 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) - self._conn_set.discard(connection) #mark graph for rebuild if self.graph: @@ -649,14 +636,13 @@ def add_event(self, event): """ #check if event already in event list - if event in self._event_set: + if event in self.events: _msg = f"{event} already part of simulation" self.logger.error(_msg) raise ValueError(_msg) #add event to global event list self.events.append(event) - self._event_set.add(event) def remove_event(self, event): @@ -671,14 +657,13 @@ def remove_event(self, event): """ #check if event is in event list - if event not in self._event_set: + 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) - self._event_set.discard(event) # system assembly ------------------------------------------------------------- @@ -737,7 +722,7 @@ def _check_blocks_are_managed(self): # Check subset actively managed for blk in conn_blocks: - if blk not in self._block_set: + if blk not in self.blocks: self.logger.warning( f"{blk} in 'connections' but not in 'blocks'!" ) @@ -772,14 +757,12 @@ def _set_solver(self, Solver=None, **solver_kwargs): #iterate all blocks and set integration engines with tolerances self._blocks_dyn = [] - self._blocks_dyn_set = set() 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.append(block) - self._blocks_dyn_set.add(block) #logging message self.logger.info( diff --git a/src/pathsim/subsystem.py b/src/pathsim/subsystem.py index cea9740c..6dec9691 100644 --- a/src/pathsim/subsystem.py +++ b/src/pathsim/subsystem.py @@ -181,14 +181,12 @@ def __init__(self, #internal algebraic loop solvers -> initialized later self.boosters = None - #internal connecions (ordered list with shadow set for O(1) lookup) + #internal connecions self.connections = list(connections) if connections else [] - self._conn_set = set(self.connections) #collect and organize internal blocks - self.blocks = [] - self._block_set = set() - self.interface = None + self.blocks = [] + self.interface = None if blocks: for block in blocks: @@ -202,7 +200,6 @@ def __init__(self, else: #regular blocks self.blocks.append(block) - self._block_set.add(block) #check if interface is defined if self.interface is None: @@ -253,7 +250,7 @@ def __contains__(self, other): ------- bool """ - return other in self._block_set or other in self._conn_set + return other in self.blocks or other in self.connections # adding and removing system components --------------------------------------------------- @@ -268,7 +265,7 @@ def add_block(self, block): block : Block block to add to the subsystem """ - if block in self._block_set: + if block in self.blocks: raise ValueError(f"block {block} already part of subsystem") #initialize solver if available @@ -278,7 +275,6 @@ def add_block(self, block): self._blocks_dyn.append(block) self.blocks.append(block) - self._block_set.add(block) if self.graph: self._graph_dirty = True @@ -294,11 +290,10 @@ def remove_block(self, block): block : Block block to remove from the subsystem """ - if block not in self._block_set: + if block not in self.blocks: raise ValueError(f"block {block} not part of subsystem") self.blocks.remove(block) - self._block_set.discard(block) #remove from dynamic list if hasattr(self, '_blocks_dyn') and block in self._blocks_dyn: @@ -318,11 +313,10 @@ def add_connection(self, connection): connection : Connection connection to add to the subsystem """ - if connection in self._conn_set: + if connection in self.connections: raise ValueError(f"{connection} already part of subsystem") self.connections.append(connection) - self._conn_set.add(connection) if self.graph: self._graph_dirty = True @@ -338,11 +332,10 @@ def remove_connection(self, connection): connection : Connection connection to remove from the subsystem """ - if connection not in self._conn_set: + if connection not in self.connections: raise ValueError(f"{connection} not part of subsystem") self.connections.remove(connection) - self._conn_set.discard(connection) if self.graph: self._graph_dirty = True From e198b21b2d9ef5a8072704a7ef7a0e3eb2a75f7d Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Wed, 25 Mar 2026 09:56:21 +0100 Subject: [PATCH 59/60] Add auto-redirect from RTD to docs.pathsim.org --- docs/source/_static/redirect.js | 8 ++++++++ docs/source/conf.py | 2 ++ docs/source/index.rst | 6 +++--- 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 docs/source/_static/redirect.js 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/index.rst b/docs/source/index.rst index 129aa25c..bb32c065 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -8,11 +8,11 @@ PathSim

- 📢 We've moved to a new documentation site! + 📢 Redirecting to the new documentation site...

- This legacy documentation will remain available but is no longer updated. - Visit docs.pathsim.org for the latest docs with interactive examples, and pathsim.org for the new homepage. + This legacy documentation is no longer updated. You will be redirected to + docs.pathsim.org in a few seconds.

From 3db0ca6e7a1fdca4274b1c629276dba1b25bea9a Mon Sep 17 00:00:00 2001 From: Milan Rother Date: Sat, 21 Mar 2026 09:25:08 +0100 Subject: [PATCH 60/60] Add ConvergenceTracker/StepTracker classes with optional diagnostics snapshots --- src/pathsim/simulation.py | 141 +++++++----- src/pathsim/utils/diagnostics.py | 244 +++++++++++++++++++++ tests/pathsim/test_diagnostics.py | 353 ++++++++++++++++++++++++++++++ 3 files changed, 679 insertions(+), 59 deletions(-) create mode 100644 src/pathsim/utils/diagnostics.py create mode 100644 tests/pathsim/test_diagnostics.py diff --git a/src/pathsim/simulation.py b/src/pathsim/simulation.py index 6f6dc9ed..11f08aff 100644 --- a/src/pathsim/simulation.py +++ b/src/pathsim/simulation.py @@ -37,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 @@ -165,17 +166,18 @@ class Simulation: """ 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 ): @@ -226,6 +228,17 @@ def __init__( #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), @@ -815,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) @@ -1026,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) @@ -1080,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 @@ -1156,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) @@ -1276,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 ---------------------------------------------------------------- @@ -1469,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) @@ -1511,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/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/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)