{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "a35c1436",
   "metadata": {},
   "source": [
    "# Load"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "383c7550",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "Available engines: ['cythonized_euler_engine', 'cythonized_rk4_engine', 'euler_engine', 'rk4_engine', 'scipy_engine', 'verlet_engine']\n"
     ]
    }
   ],
   "source": [
    "import io\n",
    "import logging\n",
    "import math\n",
    "import sys\n",
    "import pandas as pd\n",
    "from typing import get_args\n",
    "from py_ballisticcalc import Ammo, Vacuum, Shot, Calculator, HitResult\n",
    "from py_ballisticcalc import BaseEngineConfigDict, SciPyEngineConfigDict\n",
    "from py_ballisticcalc import TableG1, logger\n",
    "from py_ballisticcalc.drag_model import DragModel\n",
    "from py_ballisticcalc.helpers import must_fire\n",
    "from py_ballisticcalc.unit import *\n",
    "from py_ballisticcalc.interface import _EngineLoader\n",
    "logger.setLevel(logging.WARNING)\n",
    "print(\"\\nAvailable engines: \" + str(sorted([e.name for e in _EngineLoader.iter_engines()])))\n",
    "PreferredUnits.drop = Distance.Feet\n",
    "PreferredUnits.distance = Distance.Feet"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c2f892ee",
   "metadata": {},
   "source": [
    "# Vacuum Trajectory\n",
    "\n",
    "Here is a convenient vacuum scenario so that we can check engine precision against a known exact solution.  A vacuum ballistic trajectory is a simple parabola, but the integration engines don't know that, so they attack it as they would even the most complicated ballistic scenario.\n",
    "\n",
    "This trajectory returns to ground level after exactly 60 seconds.  We will ask the engines to return the trajectory at exactly that time, and **define the error as the distance from the correct point** (x = range_to_zero, y = 0)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "bf7b92b5",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "60.0 seconds to return to zero, launching at 1930.4430000000002fps with 30.0 degrees.  Range=100308.7607ft.\n"
     ]
    }
   ],
   "source": [
    "from py_ballisticcalc.helpers import vacuum_velocity_to_zero, vacuum_time_to_zero, vacuum_range\n",
    "cGravityConstant: float = -32.17405  # feet per second^2\n",
    "launch_angle_deg = 30.0\n",
    "launch_angle_rad = math.radians(launch_angle_deg)\n",
    "time_to_zero = 60.0  # seconds\n",
    "launch_velocity_fps = vacuum_velocity_to_zero(time_to_zero=time_to_zero, launch_angle_deg=launch_angle_deg, gravity=-cGravityConstant)\n",
    "time = vacuum_time_to_zero(velocity=launch_velocity_fps, launch_angle_deg=launch_angle_deg, gravity=-cGravityConstant)\n",
    "range_to_zero = vacuum_range(velocity=launch_velocity_fps, angle_in_degrees=launch_angle_deg, gravity=-cGravityConstant)\n",
    "print(f'{time} seconds to return to zero, launching at {launch_velocity_fps}fps with {launch_angle_deg} degrees.  '\n",
    "      f'Range={range_to_zero:.4f}ft.')\n",
    "\n",
    "shot = Shot(ammo=Ammo(DragModel(bc=0.759, drag_table=TableG1), Velocity.FPS(launch_velocity_fps)), atmo=Vacuum())\n",
    "shot.relative_angle = Angular.Degree(launch_angle_deg)\n",
    "\n",
    "def check_error(hit: HitResult, output: bool = False) -> float:\n",
    "    chkpt = hit.get_at_time(60.0)\n",
    "    if chkpt is not None:\n",
    "        chk_x = chkpt.distance >> Distance.Feet\n",
    "        chk_h = chkpt.height >> Distance.Feet\n",
    "        chk_err = math.sqrt((chk_x-range_to_zero)**2 + chk_h**2)\n",
    "        if output:\n",
    "            print(f'At {chkpt.time}s: ({chk_x}, {chk_h})ft ==> Error = {chk_err:.8f}ft')\n",
    "        return chk_err\n",
    "    return float('inf')\n",
    "\n",
    "summary = []"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3a1621a1",
   "metadata": {},
   "source": [
    "# SciPy\n",
    "\n",
    "Here's a simple example to show what we're working with:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 3,
   "id": "06c689c2",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "At 59.9999999999988s: (100308.7607134686, 1.3404160230204362e-12)ft ==> Error = 0.00000000ft\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>time</th>\n",
       "      <th>distance</th>\n",
       "      <th>velocity</th>\n",
       "      <th>mach</th>\n",
       "      <th>height</th>\n",
       "      <th>angle</th>\n",
       "      <th>flag</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>0.000 s</td>\n",
       "      <td>0.00 ft</td>\n",
       "      <td>1930.4 ft/s</td>\n",
       "      <td>1.73 mach</td>\n",
       "      <td>0.00 ft</td>\n",
       "      <td>30.0000 °</td>\n",
       "      <td>ZERO_UP|RANGE</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>30.000 s</td>\n",
       "      <td>50154.38 ft</td>\n",
       "      <td>1671.8 ft/s</td>\n",
       "      <td>1.58 mach</td>\n",
       "      <td>14478.32 ft</td>\n",
       "      <td>0.0000 °</td>\n",
       "      <td>APEX</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>60.000 s</td>\n",
       "      <td>100308.76 ft</td>\n",
       "      <td>1930.4 ft/s</td>\n",
       "      <td>1.73 mach</td>\n",
       "      <td>0.00 ft</td>\n",
       "      <td>-30.0000 °</td>\n",
       "      <td>ZERO_DOWN|RANGE</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "       time      distance     velocity       mach       height       angle  \\\n",
       "0   0.000 s       0.00 ft  1930.4 ft/s  1.73 mach      0.00 ft   30.0000 °   \n",
       "1  30.000 s   50154.38 ft  1671.8 ft/s  1.58 mach  14478.32 ft    0.0000 °   \n",
       "2  60.000 s  100308.76 ft  1930.4 ft/s  1.73 mach      0.00 ft  -30.0000 °   \n",
       "\n",
       "              flag  \n",
       "0    ZERO_UP|RANGE  \n",
       "1             APEX  \n",
       "2  ZERO_DOWN|RANGE  "
      ]
     },
     "execution_count": 3,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "config = SciPyEngineConfigDict(\n",
    "    cMinimumAltitude=-1,\n",
    "    integration_method=\"LSODA\",\n",
    ")\n",
    "calc = Calculator(config, engine='scipy_engine')\n",
    "hit, e = must_fire(calc, shot, Distance.Feet(range_to_zero), extra_data=True, time_step=60.0)\n",
    "check_error(hit, True)\n",
    "hit.dataframe(True).drop(columns=['windage', 'slant_height', 'drop_angle', 'windage', 'windage_angle', 'slant_distance', 'density_ratio', 'drag', 'energy', 'ogw'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5fff7090",
   "metadata": {},
   "source": [
    "Here's the function we'll use to test the `SciPyIntegrationEngine`:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "fe080c5c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.72 ms ± 76.8 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n",
      "Error=0.00000000ft.  Integration steps: 16.  Speed: 0.00172s\n"
     ]
    }
   ],
   "source": [
    "def scipy_chk(timeit: bool = False, **kwargs):\n",
    "    config = SciPyEngineConfigDict(\n",
    "        cMinimumAltitude=-1,\n",
    "        **kwargs,\n",
    "    )\n",
    "    calc = Calculator(config, engine='scipy_engine')\n",
    "    t = calc.fire(shot, Distance.Feet(range_to_zero), time_step=60.0)\n",
    "    err = check_error(t, False)\n",
    "    evals = calc.integration_step_count\n",
    "    if timeit:\n",
    "        speed = %timeit -o calc.fire(shot, Distance.Feet(range_to_zero), time_step=60.0)\n",
    "        return err, evals, speed.average\n",
    "    return err, evals\n",
    "err, count, speed = scipy_chk(timeit=True, integration_method=\"LSODA\")\n",
    "print(f'Error={err:.8f}ft.  Integration steps: {calc.integration_step_count}.  Speed: {speed:.5f}s')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7384d0c2",
   "metadata": {},
   "source": [
    "### Integration Methods\n",
    "\n",
    "Here's a quick look at each of the integration methods listed in the SciPyIntegrationEngine, using its default error tolerance settings."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "6a1d6969",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Engine</th>\n",
       "      <th>Setting</th>\n",
       "      <th>Error (ft)</th>\n",
       "      <th>Integration Steps</th>\n",
       "      <th>Speed</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>SciPy</td>\n",
       "      <td>LSODA</td>\n",
       "      <td>1.154649e-09</td>\n",
       "      <td>16</td>\n",
       "      <td>0.001495</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>SciPy</td>\n",
       "      <td>RK23</td>\n",
       "      <td>1.091394e-11</td>\n",
       "      <td>23</td>\n",
       "      <td>0.001797</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>SciPy</td>\n",
       "      <td>RK45</td>\n",
       "      <td>5.823608e-11</td>\n",
       "      <td>32</td>\n",
       "      <td>0.001822</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>SciPy</td>\n",
       "      <td>DOP853</td>\n",
       "      <td>1.091394e-10</td>\n",
       "      <td>77</td>\n",
       "      <td>0.002812</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>SciPy</td>\n",
       "      <td>Radau</td>\n",
       "      <td>1.818989e-11</td>\n",
       "      <td>53</td>\n",
       "      <td>0.003684</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>SciPy</td>\n",
       "      <td>BDF</td>\n",
       "      <td>1.088265e-07</td>\n",
       "      <td>58</td>\n",
       "      <td>0.007764</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "  Engine Setting    Error (ft)  Integration Steps     Speed\n",
       "5  SciPy   LSODA  1.154649e-09                 16  0.001495\n",
       "0  SciPy    RK23  1.091394e-11                 23  0.001797\n",
       "1  SciPy    RK45  5.823608e-11                 32  0.001822\n",
       "2  SciPy  DOP853  1.091394e-10                 77  0.002812\n",
       "3  SciPy   Radau  1.818989e-11                 53  0.003684\n",
       "4  SciPy     BDF  1.088265e-07                 58  0.007764"
      ]
     },
     "execution_count": 5,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "from py_ballisticcalc.engines.scipy_engine import INTEGRATION_METHOD\n",
    "stdout = sys.stdout  # Capture stdout because timeit prints to it\n",
    "sys.stdout = io.StringIO()\n",
    "for method in get_args(INTEGRATION_METHOD):\n",
    "    err, count, speed = scipy_chk(timeit=True, integration_method=method)\n",
    "    summary.append(('SciPy', method, err, count, speed))\n",
    "sys.stdout = stdout\n",
    "pd.DataFrame(summary, columns=['Engine', 'Setting', 'Error (ft)', 'Integration Steps', 'Speed']).sort_values(by='Speed', ascending=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d692cbc",
   "metadata": {},
   "source": [
    "### Points computed\n",
    "\n",
    "We can actually look at the points evaluated by SciPy: Uncomment `diff_eq()::self.eval_points.append(t)` in `scipy_engine.py`.  We'll see something interesting when we use LSODA: It appears that it is evaluating the first two steps *twice*.\n",
    "\n",
    "We must remember that the function being solved is not just `f(t)`, but `f(t, s)`, where `s` is the state vector `[x, y, z, vx, vy, vz]`.\n",
    "\n",
    "**Even if the time `t` is identical, the state `s` is different.**\n",
    "\n",
    "Here are the primary reasons why a solver like LSODA would evaluate `f(t, s)` with the same `t` but a different `s`:\n",
    "\n",
    "1.  **Jacobian Calculation (for Stiff Problems):** LSODA can switch to a stiff solver (BDF). To do this, it needs to compute the Jacobian matrix, which tells it how the derivative changes with respect to the state (`df/ds`). It computes this numerically by slightly perturbing each component of the state vector `s` and re-evaluating the function. All of these evaluations happen at the *same time `t`* but with slightly different state vectors `s`.\n",
    "\n",
    "2.  **Error Estimation:** Some adaptive methods (though more common in Runge-Kutta methods) estimate the error by comparing the results of two different calculations that end at the same time `t`. This inherently requires multiple evaluations at intermediate points that can sometimes coincide in time.\n",
    "\n",
    "3.  **Corrector Iterations (for Stiff Solvers):** Stiff solvers often use a \"predictor-corrector\" method. They predict a new state and then iterate to \"correct\" it until it converges. These correction iterations all happen at the same future time point `t`, but the state `s` is refined with each iteration, requiring a new function evaluation."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 6,
   "id": "9d295739",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/plain": [
       "[0.0,\n",
       " 5.968364397369551e-06,\n",
       " 5.968364397369551e-06,\n",
       " 1.1936728794739102e-05,\n",
       " 1.1936728794739102e-05,\n",
       " 0.059695580702490245,\n",
       " 0.11937922467618575,\n",
       " 0.17906286864988125,\n",
       " 0.7758993083868364,\n",
       " 1.3727357481237914,\n",
       " 1.9695721878607464,\n",
       " 7.937936585230297,\n",
       " 13.906300982599848,\n",
       " 19.8746653799694,\n",
       " 79.55830935366491]"
      ]
     },
     "execution_count": 6,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "config = SciPyEngineConfigDict(\n",
    "    cMinimumAltitude=-1,\n",
    "    relative_tolerance=1e-12,\n",
    "    absolute_tolerance=1e-8,\n",
    "    integration_method=\"LSODA\",\n",
    ")\n",
    "calc = Calculator(config, engine='scipy_engine')\n",
    "hit, e = must_fire(calc, shot, Distance.Feet(range_to_zero), time_step=60.0)\n",
    "calc.eval_points  # Uncomment diff_eq()::self.eval_points.append(t) in scipy_engine.py to see eval points"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9bf3210e",
   "metadata": {},
   "source": [
    "### Tolerance\n",
    "\n",
    "The SciPy solver works until it satisfies the least demanding error tolerance (between _relative_ and _absolute_). The most demanding error tolerance is just over 1e-14 (a value based on the limits of _float_ precision).  As we can see in the results from the next block: with relative tolerance set near the maximum, the absolute tolerance (`atol`) doesn't require more steps to reduce error until `atol < 1e-8`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 7,
   "id": "939e5522",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Absolute Tolerance</th>\n",
       "      <th>Relative Tolerance</th>\n",
       "      <th>Error (ft)</th>\n",
       "      <th>Integration Steps</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>0</th>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>1.000000e-02</td>\n",
       "      <td>4.729372e-11</td>\n",
       "      <td>86</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>1</th>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>1.000000e-03</td>\n",
       "      <td>1.001973e-10</td>\n",
       "      <td>80</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>2</th>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>1.000000e-04</td>\n",
       "      <td>4.688090e-11</td>\n",
       "      <td>74</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>3</th>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>1.000000e-05</td>\n",
       "      <td>7.730705e-11</td>\n",
       "      <td>68</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>4</th>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>1.000000e-06</td>\n",
       "      <td>6.821210e-11</td>\n",
       "      <td>62</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>5</th>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>1.000000e-07</td>\n",
       "      <td>1.155166e-10</td>\n",
       "      <td>56</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>1.000000e-08</td>\n",
       "      <td>4.092726e-11</td>\n",
       "      <td>50</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>1.000000e-09</td>\n",
       "      <td>6.442022e-11</td>\n",
       "      <td>44</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>7.912604e-11</td>\n",
       "      <td>38</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>1.000000e-11</td>\n",
       "      <td>3.637979e-11</td>\n",
       "      <td>38</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>1.000000e-02</td>\n",
       "      <td>1.000000e-12</td>\n",
       "      <td>1.455192e-11</td>\n",
       "      <td>32</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>1.000000e-03</td>\n",
       "      <td>1.000000e-12</td>\n",
       "      <td>2.182787e-11</td>\n",
       "      <td>32</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>1.000000e-04</td>\n",
       "      <td>1.000000e-12</td>\n",
       "      <td>4.547474e-11</td>\n",
       "      <td>32</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>1.000000e-05</td>\n",
       "      <td>1.000000e-12</td>\n",
       "      <td>1.455192e-11</td>\n",
       "      <td>32</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>1.000000e-06</td>\n",
       "      <td>1.000000e-12</td>\n",
       "      <td>8.003553e-11</td>\n",
       "      <td>32</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>1.000000e-07</td>\n",
       "      <td>1.000000e-12</td>\n",
       "      <td>1.091394e-11</td>\n",
       "      <td>38</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>1.000000e-08</td>\n",
       "      <td>1.000000e-12</td>\n",
       "      <td>1.818989e-11</td>\n",
       "      <td>38</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>1.000000e-09</td>\n",
       "      <td>1.000000e-12</td>\n",
       "      <td>5.456968e-11</td>\n",
       "      <td>38</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>1.000000e-10</td>\n",
       "      <td>1.000000e-12</td>\n",
       "      <td>5.820766e-11</td>\n",
       "      <td>38</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>19</th>\n",
       "      <td>1.000000e-11</td>\n",
       "      <td>1.000000e-12</td>\n",
       "      <td>6.093615e-11</td>\n",
       "      <td>38</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    Absolute Tolerance  Relative Tolerance    Error (ft)  Integration Steps\n",
       "0         1.000000e-10        1.000000e-02  4.729372e-11                 86\n",
       "1         1.000000e-10        1.000000e-03  1.001973e-10                 80\n",
       "2         1.000000e-10        1.000000e-04  4.688090e-11                 74\n",
       "3         1.000000e-10        1.000000e-05  7.730705e-11                 68\n",
       "4         1.000000e-10        1.000000e-06  6.821210e-11                 62\n",
       "5         1.000000e-10        1.000000e-07  1.155166e-10                 56\n",
       "6         1.000000e-10        1.000000e-08  4.092726e-11                 50\n",
       "7         1.000000e-10        1.000000e-09  6.442022e-11                 44\n",
       "8         1.000000e-10        1.000000e-10  7.912604e-11                 38\n",
       "9         1.000000e-10        1.000000e-11  3.637979e-11                 38\n",
       "10        1.000000e-02        1.000000e-12  1.455192e-11                 32\n",
       "11        1.000000e-03        1.000000e-12  2.182787e-11                 32\n",
       "12        1.000000e-04        1.000000e-12  4.547474e-11                 32\n",
       "13        1.000000e-05        1.000000e-12  1.455192e-11                 32\n",
       "14        1.000000e-06        1.000000e-12  8.003553e-11                 32\n",
       "15        1.000000e-07        1.000000e-12  1.091394e-11                 38\n",
       "16        1.000000e-08        1.000000e-12  1.818989e-11                 38\n",
       "17        1.000000e-09        1.000000e-12  5.456968e-11                 38\n",
       "18        1.000000e-10        1.000000e-12  5.820766e-11                 38\n",
       "19        1.000000e-11        1.000000e-12  6.093615e-11                 38"
      ]
     },
     "execution_count": 7,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "results = []\n",
    "method = 'RK45'\n",
    "# Run with rtol as limiting factor\n",
    "rtol = 1e-1\n",
    "atol = 1e-10\n",
    "while rtol > 1e-10:\n",
    "    rtol /= 10.0\n",
    "    err, count = scipy_chk(integration_method=method, relative_tolerance=rtol, absolute_tolerance=atol)\n",
    "    results.append((atol, rtol, err, count))\n",
    "# Run with atol as limiting factor\n",
    "atol = 1e-1\n",
    "rtol = 1e-12\n",
    "while atol > 1e-10:\n",
    "    atol /= 10.0\n",
    "    err, count = scipy_chk(integration_method=method, relative_tolerance=rtol, absolute_tolerance=atol)\n",
    "    results.append((atol, rtol, err, count))\n",
    "pd.DataFrame(results, columns=['Absolute Tolerance', 'Relative Tolerance', 'Error (ft)', 'Integration Steps'])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8874d237",
   "metadata": {},
   "source": [
    "But something counterintuitive is going on: In this test when we fix `atol=1e-10` we observe that the solver uses _fewer_ function evaluations as `rtol` _decreases_ from 1e-2 to 1e-10.  In general, decreasing `rtol` (holding all else constant) demands more precision from the solver and so should force the solver to take smaller steps, thus _increasing_ the number of function evaluations.\n",
    "\n",
    "Here’s a breakdown of what is likely happening:\n",
    "\n",
    "#### The Interaction of Tolerances and Step-Size Control\n",
    "\n",
    "The solver's goal at each step is to keep the estimated local error (`err_est`) below a threshold:\n",
    "\n",
    "`err_est <= atol + rtol * |y|`\n",
    "\n",
    "It then uses this relationship to decide the size of the *next* step. A simplified view of the logic is:\n",
    "\n",
    "1.  Take a trial step.\n",
    "2.  Estimate the error `err_est`.\n",
    "3.  If the error is too large, reject the step, shrink the step size, and try again.\n",
    "4.  If the error is acceptable, accept the step.\n",
    "5.  Calculate an \"optimal\" size for the *next* step based on the error from the current one. The goal is to make the next step as large as possible while still meeting the tolerance.\n",
    "\n",
    "#### The Counter-Intuitive Behavior Here\n",
    "\n",
    "In this experiment, `atol` is fixed at a very small `1e-10`.\n",
    "\n",
    "1.  **When `rtol` is large (e.g., `1e-2`):**\n",
    "    *   The total allowed error (`atol + rtol * |y|`) is quite large and dominated by the `rtol` term.\n",
    "    *   The solver's step-size controller is encouraged to take very large steps.\n",
    "    *   It might \"overshoot\" significantly, trying a huge step. The error estimate for this large step will also be large. The controller might then have to reject the step and retry with a smaller one. This process of trying a big step, failing, and retrying with smaller steps can lead to a higher total number of function evaluations.\n",
    "\n",
    "2.  **When `rtol` is smaller (e.g., `1e-8`):**\n",
    "    *   The total allowed error is now much smaller.\n",
    "    *   The step-size controller is immediately more conservative. It doesn't even *attempt* the huge, optimistic steps it tried before.\n",
    "    *   It starts with and continues to propose more reasonably sized steps. Because these steps are more likely to be accepted on the first try (without rejection), the total number of function evaluations can actually go down.\n",
    "\n",
    "Here we are essentially seeing a transition from a \"sloppy and aggressive\" strategy to a \"cautious and efficient\" one.\n",
    "\n",
    "| `rtol` | Solver Strategy | Step Rejections | Function Evaluations |\n",
    "| :--- | :--- | :--- | :--- |\n",
    "| **Large** | Tries very large, optimistic steps. | High (many steps are rejected and retried) | **Higher** |\n",
    "| **Small** | Tries smaller, more conservative steps. | Low (most steps are accepted on first try) | **Lower** |\n",
    "\n",
    "This is not a universal rule and is highly dependent on the specific solver (`RK45` in this case), the smoothness of the problem (a vacuum trajectory is extremely smooth), and the specific values of `atol` and `rtol`.\n",
    "\n",
    "We can inspect the actual points being attempted to confirm that this is what's happening here:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 8,
   "id": "99e5bff7",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\"Strict\" rtol=1e-10 made 38 evaluations.\n",
      "\"Sloppy\" rtol=1e-02 made 86 evaluations.\n",
      "\n",
      "Smallest time step with strict rtol: 7.32e-06 s\n",
      "Smallest time step with sloppy rtol: 7.33e-14 s\n",
      "\n",
      "Apparent retries (dt < 1e-5s) with strict rtol: 7\n",
      "\t Strict evals within first microsecond: 1\n",
      "Apparent retries (dt < 1e-5s) with sloppy rtol: 53\n",
      "\t Sloppy evals within first microsecond: 38\n"
     ]
    }
   ],
   "source": [
    "# *** THIS WILL NOT WORK unless you uncomment diff_eq()::self.eval_points.append(t) in scipy_engine.py ***\n",
    "#region \"strict\" setup\n",
    "config = SciPyEngineConfigDict(\n",
    "    cMinimumAltitude=-1,\n",
    "    relative_tolerance=1e-10,\n",
    "    absolute_tolerance=1e-10,\n",
    "    integration_method=\"RK45\",\n",
    ")\n",
    "calc = Calculator(config, engine='scipy_engine')\n",
    "hit, e = must_fire(calc, shot, Distance.Feet(range_to_zero), time_step=60.0)\n",
    "strict_eval_times = calc.eval_points\n",
    "print(f'\"Strict\" rtol=1e-10 made {len(strict_eval_times)} evaluations.')\n",
    "#endregion\n",
    "#region \"sloppy\" setup\n",
    "config = SciPyEngineConfigDict(\n",
    "    cMinimumAltitude=-1,\n",
    "    relative_tolerance=1e-2,\n",
    "    absolute_tolerance=1e-10,\n",
    "    integration_method=\"RK45\",\n",
    ")\n",
    "calc = Calculator(config, engine='scipy_engine')\n",
    "hit, e = must_fire(calc, shot, Distance.Feet(range_to_zero), time_step=60.0)\n",
    "sloppy_eval_times = calc.eval_points\n",
    "print(f'\"Sloppy\" rtol=1e-02 made {len(sloppy_eval_times)} evaluations.')\n",
    "#endregion\n",
    "import numpy as np\n",
    "# Look for evidence of retries: small forward steps in time\n",
    "strict_diffs = np.diff(strict_eval_times)\n",
    "sloppy_diffs = np.diff(sloppy_eval_times)\n",
    "print(f\"\\nSmallest time step with strict rtol: {np.min(strict_diffs[strict_diffs > 0]):.2e} s\")\n",
    "print(f\"Smallest time step with sloppy rtol: {np.min(sloppy_diffs[sloppy_diffs > 0]):.2e} s\")\n",
    "# A rejected step often involves re-evaluating at the same or a very close time point.\n",
    "# Let's count how many time steps are extremely small, indicating a retry.\n",
    "print(f\"\\nApparent retries (dt < 1e-5s) with strict rtol: {np.sum(strict_diffs < 1e-5)}\")\n",
    "strict_eval_times = np.asarray(strict_eval_times)\n",
    "print(f'\\t Strict evals within first microsecond: {np.sum(strict_eval_times < 1e-6)}')\n",
    "print(f\"Apparent retries (dt < 1e-5s) with sloppy rtol: {np.sum(sloppy_diffs < 1e-5)}\")\n",
    "sloppy_eval_times = np.asarray(sloppy_eval_times)\n",
    "print(f'\\t Sloppy evals within first microsecond: {np.sum(sloppy_eval_times < 1e-6)}')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc909d83",
   "metadata": {},
   "source": [
    "The following chart shows that in the end this algorithm sampled virtually the same points.  It's just that the \"sloppy\" approach hit points within the first microsecond of the trajectory 38 times!"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "477fcb5a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 55700 characters]",
      "text/plain": [
       "<Figure size 1200x600 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 6), sharey=True)\n",
    "fig.suptitle('Comparison of Solver Function Evaluation Times', fontsize=16)\n",
    "# --- Plot 1: Full Trajectory ---\n",
    "ax1.eventplot(sloppy_eval_times, lineoffsets=1, colors='red', label='Sloppy (rtol=1e-02)', linelengths=0.8)\n",
    "ax1.eventplot(strict_eval_times, lineoffsets=0, colors='blue', label='Strict (rtol=1e-10)', linelengths=0.8)\n",
    "ax1.set_title('Full Trajectory')\n",
    "ax1.set_xlabel('Time (s)')\n",
    "ax1.grid(axis='x', linestyle='--', alpha=0.6)\n",
    "# --- Plot 2: Zoomed-in on the start ---\n",
    "ax2.eventplot(sloppy_eval_times, lineoffsets=1, colors='red', linelengths=0.8)\n",
    "ax2.eventplot(strict_eval_times, lineoffsets=0, colors='blue', linelengths=0.8)\n",
    "ax2.set_title('Zoomed View: First 0.1 Seconds')\n",
    "ax2.set_xlabel('Time (s)')\n",
    "ax2.set_xlim(-0.005, 0.1)\n",
    "ax2.grid(axis='x', linestyle='--', alpha=0.6)\n",
    "# --- Formatting ---\n",
    "plt.yticks([0, 1], ['Strict', 'Sloppy'])  # Set shared y-axis labels\n",
    "fig.legend(loc='upper right')\n",
    "plt.tight_layout(rect=[0, 0, 1, 0.96])  # Adjust layout to make room for suptitle\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "191ea610",
   "metadata": {},
   "source": [
    "# Other engines\n",
    "\n",
    "The simplistic Euler engine sets the time-step size for each iteration based on a constant distance-like `DEFAULT_STEP` setting that is normalized by the projectile's velocity at the beginning of each step.  This makes it take smaller steps when the projectile is moving fast, and larger steps when it is slower.  The effect is to create roughly constant distance steps over the trajectory.\n",
    "\n",
    "The RK4 and Verlet engines use a constant `DEFAULT_TIME_STEP`.\n",
    "\n",
    "For all three engines we can change their speed and step sizes using the `cStepMultiplier` config setting.\n",
    "\n",
    "Following is the function we'll use to check these engines:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "d401998f",
   "metadata": {},
   "outputs": [],
   "source": [
    "def vacuum_chk(engine_name: str, timeit: bool = False, step_multiplier: float = 1.0, min_altitude: float = -1.0):\n",
    "    config = BaseEngineConfigDict(\n",
    "        cMinimumAltitude=min_altitude,\n",
    "        cStepMultiplier=step_multiplier,\n",
    "    )\n",
    "    calc = Calculator(config, engine=engine_name)\n",
    "    hit, e = must_fire(calc, shot, Distance.Feet(range_to_zero), time_step=60.0)\n",
    "    err = check_error(hit, False)\n",
    "    evals = calc.integration_step_count\n",
    "    if timeit:\n",
    "        speed = %timeit -o must_fire(calc, shot, Distance.Feet(range_to_zero), time_step=60.0)\n",
    "        return err, evals, speed.average\n",
    "    return err, evals"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "00f38cea",
   "metadata": {},
   "source": [
    "## Euler steps"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 11,
   "id": "750969dc",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "10.1 s ± 434 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n",
      "1.96 s ± 49 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n",
      "385 ms ± 11.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n",
      "80.4 ms ± 3.15 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n",
      "16.4 ms ± 644 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n",
      "4.22 ms ± 1.72 ms per loop (mean ± std. dev. of 7 runs, 100 loops each)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Engine</th>\n",
       "      <th>Multiplier</th>\n",
       "      <th>Error (ft)</th>\n",
       "      <th>Integration Steps</th>\n",
       "      <th>Speed</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>6</th>\n",
       "      <td>Euler</td>\n",
       "      <td>0.2</td>\n",
       "      <td>0.054931</td>\n",
       "      <td>1056316</td>\n",
       "      <td>10.145724</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>7</th>\n",
       "      <td>Euler</td>\n",
       "      <td>1.0</td>\n",
       "      <td>0.274653</td>\n",
       "      <td>211265</td>\n",
       "      <td>1.963174</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>8</th>\n",
       "      <td>Euler</td>\n",
       "      <td>5.0</td>\n",
       "      <td>1.373272</td>\n",
       "      <td>42254</td>\n",
       "      <td>0.385004</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>9</th>\n",
       "      <td>Euler</td>\n",
       "      <td>25.0</td>\n",
       "      <td>6.866495</td>\n",
       "      <td>8452</td>\n",
       "      <td>0.080411</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>10</th>\n",
       "      <td>Euler</td>\n",
       "      <td>125.0</td>\n",
       "      <td>34.333201</td>\n",
       "      <td>1692</td>\n",
       "      <td>0.016355</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>11</th>\n",
       "      <td>Euler</td>\n",
       "      <td>625.0</td>\n",
       "      <td>171.666727</td>\n",
       "      <td>340</td>\n",
       "      <td>0.004216</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   Engine Multiplier  Error (ft)  Integration Steps      Speed\n",
       "6   Euler        0.2    0.054931            1056316  10.145724\n",
       "7   Euler        1.0    0.274653             211265   1.963174\n",
       "8   Euler        5.0    1.373272              42254   0.385004\n",
       "9   Euler       25.0    6.866495               8452   0.080411\n",
       "10  Euler      125.0   34.333201               1692   0.016355\n",
       "11  Euler      625.0  171.666727                340   0.004216"
      ]
     },
     "execution_count": 11,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "step = 5.0\n",
    "multiplier = 1.0/step\n",
    "while multiplier <= 1000.0:\n",
    "    err, count, speed = vacuum_chk('euler_engine', timeit=True, step_multiplier=multiplier, min_altitude=-1000.0)\n",
    "    summary.append(('Euler', multiplier, err, count, speed))\n",
    "    multiplier *= step\n",
    "df = pd.DataFrame(summary, columns=['Engine', 'Multiplier', 'Error (ft)', 'Integration Steps', 'Speed'])\n",
    "df[df.Engine == 'Euler']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9a85bb5e",
   "metadata": {},
   "source": [
    "## RK4"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 12,
   "id": "a62bc8ca",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "1.42 ms ± 26.5 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n"
     ]
    },
    {
     "data": {
      "text/plain": [
       "(3.17464884975649e-11, 41, 0.001415420871423391)"
      ]
     },
     "execution_count": 12,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "vacuum_chk('rk4_engine', timeit=True, step_multiplier=1000.0)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 13,
   "id": "d7843bd3",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "235 ms ± 4.59 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)\n",
      "47.5 ms ± 1.36 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n",
      "9.7 ms ± 140 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n",
      "2.08 ms ± 58.1 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n",
      "552 μs ± 18.8 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Engine</th>\n",
       "      <th>Multiplier</th>\n",
       "      <th>Error (ft)</th>\n",
       "      <th>Integration Steps</th>\n",
       "      <th>Speed</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>12</th>\n",
       "      <td>RK4</td>\n",
       "      <td>5.0</td>\n",
       "      <td>6.457498e-09</td>\n",
       "      <td>8001</td>\n",
       "      <td>0.235167</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>13</th>\n",
       "      <td>RK4</td>\n",
       "      <td>25.0</td>\n",
       "      <td>3.176930e-09</td>\n",
       "      <td>1601</td>\n",
       "      <td>0.047537</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>14</th>\n",
       "      <td>RK4</td>\n",
       "      <td>125.0</td>\n",
       "      <td>2.562459e-10</td>\n",
       "      <td>321</td>\n",
       "      <td>0.009695</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>15</th>\n",
       "      <td>RK4</td>\n",
       "      <td>625.0</td>\n",
       "      <td>5.815080e-11</td>\n",
       "      <td>65</td>\n",
       "      <td>0.002079</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>16</th>\n",
       "      <td>RK4</td>\n",
       "      <td>3125.0</td>\n",
       "      <td>5.655595e+01</td>\n",
       "      <td>13</td>\n",
       "      <td>0.000552</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "   Engine Multiplier    Error (ft)  Integration Steps     Speed\n",
       "12    RK4        5.0  6.457498e-09               8001  0.235167\n",
       "13    RK4       25.0  3.176930e-09               1601  0.047537\n",
       "14    RK4      125.0  2.562459e-10                321  0.009695\n",
       "15    RK4      625.0  5.815080e-11                 65  0.002079\n",
       "16    RK4     3125.0  5.655595e+01                 13  0.000552"
      ]
     },
     "execution_count": 13,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "step = 5.0\n",
    "while step <= 5000.0:\n",
    "    err, count, speed = vacuum_chk('rk4_engine', timeit=True, step_multiplier=step)\n",
    "    summary.append(('RK4', step, err, count, speed))\n",
    "    step *= 5.0\n",
    "df = pd.DataFrame(summary, columns=['Engine', 'Multiplier', 'Error (ft)', 'Integration Steps', 'Speed'])\n",
    "df[df.Engine == 'RK4']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5ac62af7",
   "metadata": {},
   "source": [
    "## Verlet"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 14,
   "id": "e5ac706c",
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "149 ms ± 3.01 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n",
      "29.5 ms ± 654 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)\n",
      "6.61 ms ± 563 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)\n",
      "1.36 ms ± 21.4 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n",
      "426 μs ± 38.6 μs per loop (mean ± std. dev. of 7 runs, 1,000 loops each)\n"
     ]
    },
    {
     "data": {
      "text/html": [
       "<div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>Engine</th>\n",
       "      <th>Multiplier</th>\n",
       "      <th>Error (ft)</th>\n",
       "      <th>Integration Steps</th>\n",
       "      <th>Speed</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>17</th>\n",
       "      <td>Verlet</td>\n",
       "      <td>5.0</td>\n",
       "      <td>1.100426e-08</td>\n",
       "      <td>12001</td>\n",
       "      <td>0.148593</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>18</th>\n",
       "      <td>Verlet</td>\n",
       "      <td>25.0</td>\n",
       "      <td>2.611067e-09</td>\n",
       "      <td>2401</td>\n",
       "      <td>0.029496</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>19</th>\n",
       "      <td>Verlet</td>\n",
       "      <td>125.0</td>\n",
       "      <td>2.098737e-10</td>\n",
       "      <td>481</td>\n",
       "      <td>0.006610</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>20</th>\n",
       "      <td>Verlet</td>\n",
       "      <td>625.0</td>\n",
       "      <td>3.399236e-11</td>\n",
       "      <td>97</td>\n",
       "      <td>0.001359</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>21</th>\n",
       "      <td>Verlet</td>\n",
       "      <td>3125.0</td>\n",
       "      <td>2.513598e+01</td>\n",
       "      <td>20</td>\n",
       "      <td>0.000426</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>"
      ],
      "text/plain": [
       "    Engine Multiplier    Error (ft)  Integration Steps     Speed\n",
       "17  Verlet        5.0  1.100426e-08              12001  0.148593\n",
       "18  Verlet       25.0  2.611067e-09               2401  0.029496\n",
       "19  Verlet      125.0  2.098737e-10                481  0.006610\n",
       "20  Verlet      625.0  3.399236e-11                 97  0.001359\n",
       "21  Verlet     3125.0  2.513598e+01                 20  0.000426"
      ]
     },
     "execution_count": 14,
     "metadata": {},
     "output_type": "execute_result"
    }
   ],
   "source": [
    "step = 5.0\n",
    "while step <= 5000.0:\n",
    "    err, count, speed = vacuum_chk('verlet_engine', timeit=True, step_multiplier=step)\n",
    "    summary.append(('Verlet', step, err, count, speed))\n",
    "    step *= 5.0\n",
    "df = pd.DataFrame(summary, columns=['Engine', 'Multiplier', 'Error (ft)', 'Integration Steps', 'Speed'])\n",
    "df[df.Engine == 'Verlet']"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "673b85e0",
   "metadata": {},
   "source": [
    "# Summary\n",
    "\n",
    "All of the higher-order engines can fit this parabola with fewer than 100 function evaluations.  Only the simplistic Euler engine struggles to approximate it, carrying a measurable error even with hundreds of thousands of steps."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 15,
   "id": "ef83109a",
   "metadata": {},
   "outputs": [
    {
     "data": {
      "text/html": [
       "<style type=\"text/css\">\n",
       "</style>\n",
       "<table id=\"T_81f22\">\n",
       "  <thead>\n",
       "    <tr>\n",
       "      <th class=\"blank level0\" >&nbsp;</th>\n",
       "      <th id=\"T_81f22_level0_col0\" class=\"col_heading level0 col0\" >Engine</th>\n",
       "      <th id=\"T_81f22_level0_col1\" class=\"col_heading level0 col1\" >Multiplier</th>\n",
       "      <th id=\"T_81f22_level0_col2\" class=\"col_heading level0 col2\" >Error (ft)</th>\n",
       "      <th id=\"T_81f22_level0_col3\" class=\"col_heading level0 col3\" >Integration Steps</th>\n",
       "      <th id=\"T_81f22_level0_col4\" class=\"col_heading level0 col4\" >Speed</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row0\" class=\"row_heading level0 row0\" >0</th>\n",
       "      <td id=\"T_81f22_row0_col0\" class=\"data row0 col0\" >SciPy</td>\n",
       "      <td id=\"T_81f22_row0_col1\" class=\"data row0 col1\" >RK23</td>\n",
       "      <td id=\"T_81f22_row0_col2\" class=\"data row0 col2\" >0.000000000011</td>\n",
       "      <td id=\"T_81f22_row0_col3\" class=\"data row0 col3\" >23</td>\n",
       "      <td id=\"T_81f22_row0_col4\" class=\"data row0 col4\" >0.001797</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row1\" class=\"row_heading level0 row1\" >1</th>\n",
       "      <td id=\"T_81f22_row1_col0\" class=\"data row1 col0\" >SciPy</td>\n",
       "      <td id=\"T_81f22_row1_col1\" class=\"data row1 col1\" >RK45</td>\n",
       "      <td id=\"T_81f22_row1_col2\" class=\"data row1 col2\" >0.000000000058</td>\n",
       "      <td id=\"T_81f22_row1_col3\" class=\"data row1 col3\" >32</td>\n",
       "      <td id=\"T_81f22_row1_col4\" class=\"data row1 col4\" >0.001822</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row2\" class=\"row_heading level0 row2\" >2</th>\n",
       "      <td id=\"T_81f22_row2_col0\" class=\"data row2 col0\" >SciPy</td>\n",
       "      <td id=\"T_81f22_row2_col1\" class=\"data row2 col1\" >DOP853</td>\n",
       "      <td id=\"T_81f22_row2_col2\" class=\"data row2 col2\" >0.000000000109</td>\n",
       "      <td id=\"T_81f22_row2_col3\" class=\"data row2 col3\" >77</td>\n",
       "      <td id=\"T_81f22_row2_col4\" class=\"data row2 col4\" >0.002812</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row3\" class=\"row_heading level0 row3\" >3</th>\n",
       "      <td id=\"T_81f22_row3_col0\" class=\"data row3 col0\" >SciPy</td>\n",
       "      <td id=\"T_81f22_row3_col1\" class=\"data row3 col1\" >Radau</td>\n",
       "      <td id=\"T_81f22_row3_col2\" class=\"data row3 col2\" >0.000000000018</td>\n",
       "      <td id=\"T_81f22_row3_col3\" class=\"data row3 col3\" >53</td>\n",
       "      <td id=\"T_81f22_row3_col4\" class=\"data row3 col4\" >0.003684</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row4\" class=\"row_heading level0 row4\" >4</th>\n",
       "      <td id=\"T_81f22_row4_col0\" class=\"data row4 col0\" >SciPy</td>\n",
       "      <td id=\"T_81f22_row4_col1\" class=\"data row4 col1\" >BDF</td>\n",
       "      <td id=\"T_81f22_row4_col2\" class=\"data row4 col2\" >0.000000108826</td>\n",
       "      <td id=\"T_81f22_row4_col3\" class=\"data row4 col3\" >58</td>\n",
       "      <td id=\"T_81f22_row4_col4\" class=\"data row4 col4\" >0.007764</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row5\" class=\"row_heading level0 row5\" >5</th>\n",
       "      <td id=\"T_81f22_row5_col0\" class=\"data row5 col0\" >SciPy</td>\n",
       "      <td id=\"T_81f22_row5_col1\" class=\"data row5 col1\" >LSODA</td>\n",
       "      <td id=\"T_81f22_row5_col2\" class=\"data row5 col2\" >0.000000001155</td>\n",
       "      <td id=\"T_81f22_row5_col3\" class=\"data row5 col3\" >16</td>\n",
       "      <td id=\"T_81f22_row5_col4\" class=\"data row5 col4\" >0.001495</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row6\" class=\"row_heading level0 row6\" >6</th>\n",
       "      <td id=\"T_81f22_row6_col0\" class=\"data row6 col0\" >Euler</td>\n",
       "      <td id=\"T_81f22_row6_col1\" class=\"data row6 col1\" >0.200000</td>\n",
       "      <td id=\"T_81f22_row6_col2\" class=\"data row6 col2\" >0.054930622434</td>\n",
       "      <td id=\"T_81f22_row6_col3\" class=\"data row6 col3\" >1056316</td>\n",
       "      <td id=\"T_81f22_row6_col4\" class=\"data row6 col4\" >10.145724</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row7\" class=\"row_heading level0 row7\" >7</th>\n",
       "      <td id=\"T_81f22_row7_col0\" class=\"data row7 col0\" >Euler</td>\n",
       "      <td id=\"T_81f22_row7_col1\" class=\"data row7 col1\" >1.000000</td>\n",
       "      <td id=\"T_81f22_row7_col2\" class=\"data row7 col2\" >0.274653115431</td>\n",
       "      <td id=\"T_81f22_row7_col3\" class=\"data row7 col3\" >211265</td>\n",
       "      <td id=\"T_81f22_row7_col4\" class=\"data row7 col4\" >1.963174</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row8\" class=\"row_heading level0 row8\" >8</th>\n",
       "      <td id=\"T_81f22_row8_col0\" class=\"data row8 col0\" >Euler</td>\n",
       "      <td id=\"T_81f22_row8_col1\" class=\"data row8 col1\" >5.000000</td>\n",
       "      <td id=\"T_81f22_row8_col2\" class=\"data row8 col2\" >1.373271879239</td>\n",
       "      <td id=\"T_81f22_row8_col3\" class=\"data row8 col3\" >42254</td>\n",
       "      <td id=\"T_81f22_row8_col4\" class=\"data row8 col4\" >0.385004</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row9\" class=\"row_heading level0 row9\" >9</th>\n",
       "      <td id=\"T_81f22_row9_col0\" class=\"data row9 col0\" >Euler</td>\n",
       "      <td id=\"T_81f22_row9_col1\" class=\"data row9 col1\" >25.000000</td>\n",
       "      <td id=\"T_81f22_row9_col2\" class=\"data row9 col2\" >6.866495202007</td>\n",
       "      <td id=\"T_81f22_row9_col3\" class=\"data row9 col3\" >8452</td>\n",
       "      <td id=\"T_81f22_row9_col4\" class=\"data row9 col4\" >0.080411</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row10\" class=\"row_heading level0 row10\" >10</th>\n",
       "      <td id=\"T_81f22_row10_col0\" class=\"data row10 col0\" >Euler</td>\n",
       "      <td id=\"T_81f22_row10_col1\" class=\"data row10 col1\" >125.000000</td>\n",
       "      <td id=\"T_81f22_row10_col2\" class=\"data row10 col2\" >34.333200772961</td>\n",
       "      <td id=\"T_81f22_row10_col3\" class=\"data row10 col3\" >1692</td>\n",
       "      <td id=\"T_81f22_row10_col4\" class=\"data row10 col4\" >0.016355</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row11\" class=\"row_heading level0 row11\" >11</th>\n",
       "      <td id=\"T_81f22_row11_col0\" class=\"data row11 col0\" >Euler</td>\n",
       "      <td id=\"T_81f22_row11_col1\" class=\"data row11 col1\" >625.000000</td>\n",
       "      <td id=\"T_81f22_row11_col2\" class=\"data row11 col2\" >171.666727268538</td>\n",
       "      <td id=\"T_81f22_row11_col3\" class=\"data row11 col3\" >340</td>\n",
       "      <td id=\"T_81f22_row11_col4\" class=\"data row11 col4\" >0.004216</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row12\" class=\"row_heading level0 row12\" >12</th>\n",
       "      <td id=\"T_81f22_row12_col0\" class=\"data row12 col0\" >RK4</td>\n",
       "      <td id=\"T_81f22_row12_col1\" class=\"data row12 col1\" >5.000000</td>\n",
       "      <td id=\"T_81f22_row12_col2\" class=\"data row12 col2\" >0.000000006457</td>\n",
       "      <td id=\"T_81f22_row12_col3\" class=\"data row12 col3\" >8001</td>\n",
       "      <td id=\"T_81f22_row12_col4\" class=\"data row12 col4\" >0.235167</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row13\" class=\"row_heading level0 row13\" >13</th>\n",
       "      <td id=\"T_81f22_row13_col0\" class=\"data row13 col0\" >RK4</td>\n",
       "      <td id=\"T_81f22_row13_col1\" class=\"data row13 col1\" >25.000000</td>\n",
       "      <td id=\"T_81f22_row13_col2\" class=\"data row13 col2\" >0.000000003177</td>\n",
       "      <td id=\"T_81f22_row13_col3\" class=\"data row13 col3\" >1601</td>\n",
       "      <td id=\"T_81f22_row13_col4\" class=\"data row13 col4\" >0.047537</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row14\" class=\"row_heading level0 row14\" >14</th>\n",
       "      <td id=\"T_81f22_row14_col0\" class=\"data row14 col0\" >RK4</td>\n",
       "      <td id=\"T_81f22_row14_col1\" class=\"data row14 col1\" >125.000000</td>\n",
       "      <td id=\"T_81f22_row14_col2\" class=\"data row14 col2\" >0.000000000256</td>\n",
       "      <td id=\"T_81f22_row14_col3\" class=\"data row14 col3\" >321</td>\n",
       "      <td id=\"T_81f22_row14_col4\" class=\"data row14 col4\" >0.009695</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row15\" class=\"row_heading level0 row15\" >15</th>\n",
       "      <td id=\"T_81f22_row15_col0\" class=\"data row15 col0\" >RK4</td>\n",
       "      <td id=\"T_81f22_row15_col1\" class=\"data row15 col1\" >625.000000</td>\n",
       "      <td id=\"T_81f22_row15_col2\" class=\"data row15 col2\" >0.000000000058</td>\n",
       "      <td id=\"T_81f22_row15_col3\" class=\"data row15 col3\" >65</td>\n",
       "      <td id=\"T_81f22_row15_col4\" class=\"data row15 col4\" >0.002079</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row16\" class=\"row_heading level0 row16\" >16</th>\n",
       "      <td id=\"T_81f22_row16_col0\" class=\"data row16 col0\" >RK4</td>\n",
       "      <td id=\"T_81f22_row16_col1\" class=\"data row16 col1\" >3125.000000</td>\n",
       "      <td id=\"T_81f22_row16_col2\" class=\"data row16 col2\" >56.555947265646</td>\n",
       "      <td id=\"T_81f22_row16_col3\" class=\"data row16 col3\" >13</td>\n",
       "      <td id=\"T_81f22_row16_col4\" class=\"data row16 col4\" >0.000552</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row17\" class=\"row_heading level0 row17\" >17</th>\n",
       "      <td id=\"T_81f22_row17_col0\" class=\"data row17 col0\" >Verlet</td>\n",
       "      <td id=\"T_81f22_row17_col1\" class=\"data row17 col1\" >5.000000</td>\n",
       "      <td id=\"T_81f22_row17_col2\" class=\"data row17 col2\" >0.000000011004</td>\n",
       "      <td id=\"T_81f22_row17_col3\" class=\"data row17 col3\" >12001</td>\n",
       "      <td id=\"T_81f22_row17_col4\" class=\"data row17 col4\" >0.148593</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row18\" class=\"row_heading level0 row18\" >18</th>\n",
       "      <td id=\"T_81f22_row18_col0\" class=\"data row18 col0\" >Verlet</td>\n",
       "      <td id=\"T_81f22_row18_col1\" class=\"data row18 col1\" >25.000000</td>\n",
       "      <td id=\"T_81f22_row18_col2\" class=\"data row18 col2\" >0.000000002611</td>\n",
       "      <td id=\"T_81f22_row18_col3\" class=\"data row18 col3\" >2401</td>\n",
       "      <td id=\"T_81f22_row18_col4\" class=\"data row18 col4\" >0.029496</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row19\" class=\"row_heading level0 row19\" >19</th>\n",
       "      <td id=\"T_81f22_row19_col0\" class=\"data row19 col0\" >Verlet</td>\n",
       "      <td id=\"T_81f22_row19_col1\" class=\"data row19 col1\" >125.000000</td>\n",
       "      <td id=\"T_81f22_row19_col2\" class=\"data row19 col2\" >0.000000000210</td>\n",
       "      <td id=\"T_81f22_row19_col3\" class=\"data row19 col3\" >481</td>\n",
       "      <td id=\"T_81f22_row19_col4\" class=\"data row19 col4\" >0.006610</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row20\" class=\"row_heading level0 row20\" >20</th>\n",
       "      <td id=\"T_81f22_row20_col0\" class=\"data row20 col0\" >Verlet</td>\n",
       "      <td id=\"T_81f22_row20_col1\" class=\"data row20 col1\" >625.000000</td>\n",
       "      <td id=\"T_81f22_row20_col2\" class=\"data row20 col2\" >0.000000000034</td>\n",
       "      <td id=\"T_81f22_row20_col3\" class=\"data row20 col3\" >97</td>\n",
       "      <td id=\"T_81f22_row20_col4\" class=\"data row20 col4\" >0.001359</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th id=\"T_81f22_level0_row21\" class=\"row_heading level0 row21\" >21</th>\n",
       "      <td id=\"T_81f22_row21_col0\" class=\"data row21 col0\" >Verlet</td>\n",
       "      <td id=\"T_81f22_row21_col1\" class=\"data row21 col1\" >3125.000000</td>\n",
       "      <td id=\"T_81f22_row21_col2\" class=\"data row21 col2\" >25.135976562473</td>\n",
       "      <td id=\"T_81f22_row21_col3\" class=\"data row21 col3\" >20</td>\n",
       "      <td id=\"T_81f22_row21_col4\" class=\"data row21 col4\" >0.000426</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n"
      ],
      "text/plain": [
       "<pandas.io.formats.style.Styler at 0x21f67e80d10>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "display(df.style.format({'Error (ft)': '{:.12f}'}))"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "py-ballisticcalc",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
