<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="../assets/xml/rss.xsl" media="all"?><rss version="2.0" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>TinyComputers.io (Posts about scientific computing)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/scientific-computing.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 A.C. Jokela 
&lt;!-- div style="width: 100%" --&gt;
&lt;a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"&gt;&lt;img alt="" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png" /&gt; Creative Commons Attribution-ShareAlike&lt;/a&gt;&amp;nbsp;|&amp;nbsp;
&lt;!-- /div --&gt;
</copyright><lastBuildDate>Wed, 11 Mar 2026 00:05:41 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Discretizing Continuous ML Models: Offline Ballistic Coefficient Corrections via Lookup Table Approximation</title><link>https://tinycomputers.io/posts/discretizing-continuous-ml-models-offline-ballistic-coefficient-corrections.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/discretizing-bc_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;38 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/bc5d-5d-architecture.png" alt="BC5D 5-Dimensional Lookup Table Architecture" style="width: 100%; max-width: 700px; display: block; margin: 20px auto; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;h3&gt;Abstract&lt;/h3&gt;
&lt;p&gt;Machine learning models for ballistic coefficient (BC) correction have demonstrated significant improvements in trajectory prediction accuracy by capturing velocity-dependent drag variations that traditional constant-BC assumptions cannot model. However, deploying such models in field conditions presents challenges: network connectivity requirements, latency constraints, and computational overhead on resource-limited devices. This paper presents a methodology for discretizing continuous ML models into offline lookup tables, specifically addressing the problem of ballistic coefficient corrections across the flight envelope. We construct caliber-specific 5-dimensional lookup tables (BC5D) indexed by bullet weight, base BC, muzzle velocity, instantaneous velocity, and drag model type. Our approach samples the continuous ML function at fixed intervals and relies on piecewise-linear interpolation for queries between sample points. Empirical evaluation demonstrates that this discretization achieves velocity predictions within 5% of the continuous ML model through supersonic and early transonic regimes, with predictable divergence of 10-15% in deep transonic regions (Mach 0.8-1.2) where the underlying physics exhibit pronounced non-linearities. We argue that this accuracy-connectivity trade-off represents a practical compromise for field deployment, analogous to the relationship between analog signals and digital sampling in audio engineering.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;1. Introduction and Thesis&lt;/h3&gt;
&lt;p&gt;The ballistic coefficient (BC) serves as the primary aerodynamic descriptor for projectile flight, encoding the bullet's ability to overcome air resistance into a single dimensionless quantity. Traditionally, manufacturers publish BC values measured under specific conditions—typically referenced to standard atmospheric density at sea level—and these values are treated as constants throughout the trajectory calculation. This simplification, while computationally convenient, ignores a well-documented physical reality: drag characteristics vary substantially with velocity, particularly as projectiles decelerate through transonic regimes where the relationship between Mach number and drag coefficient undergoes rapid, non-linear transitions [1, 2].&lt;/p&gt;
&lt;p&gt;Machine learning approaches have emerged as a promising solution to this limitation. By training models on empirical drag data—obtained through Doppler radar tracking, spark range measurements, or computational fluid dynamics simulations—researchers can capture the complex, velocity-dependent nature of aerodynamic drag with greater fidelity than constant-BC assumptions permit [3, 4]. These ML models accept multiple input parameters (bullet geometry, muzzle velocity, current velocity, atmospheric conditions) and output a correction factor that adjusts the published BC to reflect instantaneous flight conditions.&lt;/p&gt;
&lt;p&gt;However, ML model deployment introduces practical constraints that conflict with many real-world use cases. Precision shooting applications often occur in environments lacking reliable network connectivity. Mobile devices and embedded systems may lack the computational resources for real-time model inference. Latency requirements for interactive ballistics calculators may preclude round-trip API calls to remote servers. These constraints motivate investigation into methods for deploying ML-derived insights without the ML infrastructure.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Thesis:&lt;/strong&gt; Continuous machine learning models for ballistic coefficient correction can be effectively discretized into offline lookup tables that preserve the essential predictive improvements while eliminating connectivity and computational dependencies. The discretization introduces a piecewise-linear approximation that follows the general trend of the continuous model but exhibits stair-step behavior at sample boundaries—a trade-off analogous to digital audio sampling, where sufficiently fine discretization renders the steps imperceptible for practical applications.&lt;/p&gt;
&lt;p&gt;This paper makes three primary contributions:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;A methodology for constructing caliber-specific 5-dimensional BC correction tables from continuous ML models&lt;/li&gt;
&lt;li&gt;Empirical analysis of approximation fidelity across the velocity envelope, with particular attention to transonic degradation&lt;/li&gt;
&lt;li&gt;A practical deployment architecture enabling offline operation while maintaining compatibility with online systems&lt;/li&gt;
&lt;/ol&gt;
&lt;hr&gt;
&lt;h3&gt;2. Background and Related Work&lt;/h3&gt;
&lt;h4&gt;2.1 Ballistic Coefficient Fundamentals&lt;/h4&gt;
&lt;p&gt;The ballistic coefficient, as formalized by Ingalls and later refined by the Sporting Arms and Ammunition Manufacturers' Institute (SAAMI), relates a projectile's drag characteristics to a standard reference projectile [5]. The G1 and G7 drag models, representing flat-base and boat-tail projectile shapes respectively, define these reference functions. A projectile's BC expresses the ratio of its sectional density to its form factor relative to the standard:&lt;/p&gt;
&lt;p&gt;$$BC = \frac{SD}{i} = \frac{m/d^2}{C_D/C_{D_{ref}}}$$&lt;/p&gt;
&lt;p&gt;where $m$ is mass, $d$ is diameter, $C_D$ is the projectile's drag coefficient, and $C_{D_{ref}}$ is the reference projectile's drag coefficient at the same Mach number [6].&lt;/p&gt;
&lt;p&gt;The critical insight motivating this work is that the form factor $i$ is not constant—it varies with Mach number, particularly in the transonic regime (Mach 0.8-1.2) where shock wave formation and boundary layer interactions produce complex aerodynamic effects [7]. Modern Doppler radar measurements have quantified these variations, revealing that effective BC can change by 20-40% between supersonic cruise and transonic deceleration [8].&lt;/p&gt;
&lt;h4&gt;2.2 Model Compression and Quantization&lt;/h4&gt;
&lt;p&gt;The challenge of deploying complex models in resource-constrained environments has driven extensive research in model compression techniques. Neural network quantization reduces model precision from 32-bit floating point to lower bit widths (16-bit, 8-bit, or even binary), achieving 4-32x compression with modest accuracy degradation [9, 10]. Knowledge distillation trains smaller "student" models to mimic larger "teacher" models, transferring predictive capability without the full parameter count [11].&lt;/p&gt;
&lt;p&gt;Lookup table (LUT) approximation represents an extreme form of model compression: rather than deploying a parameterized model, we pre-compute outputs for a grid of input values and interpolate between them. This approach has deep roots in computer graphics (texture mapping, color correction) [12], signal processing (trigonometric function evaluation) [13], and embedded systems (sensor linearization) [14].&lt;/p&gt;
&lt;p&gt;The key insight from this literature is that LUT approximation quality depends on three factors: (1) the smoothness of the underlying function, (2) the density of the sampling grid, and (3) the interpolation scheme employed. For sufficiently smooth functions, linear interpolation over a fine grid achieves arbitrarily low approximation error. Non-linearities and discontinuities require finer sampling in affected regions or higher-order interpolation schemes.&lt;/p&gt;
&lt;h4&gt;2.3 Lookup Tables in Physics Simulation&lt;/h4&gt;
&lt;p&gt;Lookup table approaches have a long history in physics simulation, particularly for computationally expensive functions that must be evaluated repeatedly. Atmospheric models commonly employ tabulated thermodynamic properties, interpolating between pre-computed values for temperature, pressure, and density [15]. Real-time graphics engines use LUTs for physically-based rendering calculations, trading memory for computation [16].&lt;/p&gt;
&lt;p&gt;In ballistics specifically, tabulated drag functions have been standard since the 19th century. The original Ingalls tables provided drag coefficient values at discrete Mach numbers, with interpolation for intermediate velocities [17]. Modern implementations like JBM Ballistics and Applied Ballistics continue this tradition, albeit with finer discretization and more sophisticated interpolation [18].&lt;/p&gt;
&lt;p&gt;Our contribution extends this paradigm by tabulating not the drag function itself but the &lt;em&gt;correction&lt;/em&gt; to a drag function—the multiplicative factor that transforms a published BC into an effective BC accounting for velocity-dependent variations captured by ML models.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;3. Methodology&lt;/h3&gt;
&lt;h4&gt;3.1 BC5D Table Architecture&lt;/h4&gt;
&lt;p&gt;We construct lookup tables spanning five dimensions, hence the designation "BC5D":&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Bullet weight&lt;/strong&gt; (grains): Captures mass-dependent momentum retention characteristics&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base BC&lt;/strong&gt; (dimensionless): The manufacturer-published ballistic coefficient&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Muzzle velocity&lt;/strong&gt; (fps): Initial conditions affecting Reynolds number and flight regime&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Current velocity&lt;/strong&gt; (fps): Instantaneous velocity determining Mach-dependent drag&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drag model type&lt;/strong&gt; (categorical): G1, G7, or custom drag functions&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This 5-dimensional parameterization follows from the input signature of our continuous ML correction model, which accepts these parameters and returns a multiplicative correction factor in the range [0.5, 1.5]. A correction of 1.0 indicates no adjustment; values below 1.0 indicate reduced effective drag (higher effective BC), while values above 1.0 indicate increased drag.&lt;/p&gt;
&lt;h4&gt;3.2 Caliber-Specific Tables&lt;/h4&gt;
&lt;p&gt;Rather than constructing a single monolithic table covering all calibers, we generate separate tables for each bullet diameter: .224 (5.56mm), .243 (6mm), .264 (6.5mm), .277 (6.8mm), .284 (7mm), .308 (7.62mm), and .338 (8.6mm). This caliber-specific approach offers several advantages:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Reduced file size:&lt;/strong&gt; Each table covers only the weight and BC ranges relevant to that caliber. A .224 table need not include entries for 300-grain bullets, nor does a .338 table require entries for 55-grain bullets. Typical table sizes range from 1.0-1.5 MB per caliber.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Targeted accuracy:&lt;/strong&gt; Bin boundaries can be optimized for each caliber's typical parameter ranges. The .224 table uses weight bins from 50-90 grains, while the .308 table spans 125-220 grains.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Independent updates:&lt;/strong&gt; Refinements to one caliber's model can be deployed without forcing users to re-download tables for calibers they don't use.&lt;/p&gt;
&lt;h4&gt;3.3 Sampling and Bin Definition&lt;/h4&gt;
&lt;p&gt;For each dimension, we define discrete bins that balance granularity against storage requirements:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Weight:&lt;/strong&gt; 12 bins spanning caliber-appropriate range (e.g., 125-220 gr for .308)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Base BC:&lt;/strong&gt; 16 bins from 0.200 to 0.800&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Muzzle velocity:&lt;/strong&gt; 10 bins from 1800 to 3500 fps&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Current velocity:&lt;/strong&gt; 20 bins from 600 to 3200 fps&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Drag model:&lt;/strong&gt; 3 values (G1, G7, G8)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The current velocity dimension receives the finest discretization because BC corrections vary most rapidly with instantaneous velocity, particularly in transonic regimes. The resulting 5D grid contains approximately 115,000 cells per drag model type, yielding total table sizes of 1.0-1.5 MB depending on caliber-specific range spans.&lt;/p&gt;
&lt;h4&gt;3.4 Table Generation Process&lt;/h4&gt;
&lt;p&gt;Table generation proceeds by exhaustively querying the continuous ML model at each grid point:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;for each drag_model in [G1, G7, G8]:
    for each weight_bin in weight_bins:
        for each bc_bin in bc_bins:
            for each mv_bin in muzzle_velocity_bins:
                for each cv_bin in current_velocity_bins:
                    correction = ml_model.predict(
                        weight=weight_bin,
                        bc=bc_bin,
                        muzzle_velocity=mv_bin,
                        current_velocity=cv_bin,
                        drag_model=drag_model
                    )
                    store(correction)
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The resulting values are stored in a binary format with an 80-byte header containing metadata (version, caliber, dimensions, timestamp, CRC32 checksum) followed by float32 correction values in row-major order.&lt;/p&gt;
&lt;h4&gt;3.5 Runtime Interpolation&lt;/h4&gt;
&lt;p&gt;At query time, the lookup procedure locates the surrounding grid points in each dimension and performs multi-linear interpolation. For a 5D query point, this involves identifying 32 surrounding vertices (2^5) and computing the weighted average based on the query point's position within the hypercube.&lt;/p&gt;
&lt;p&gt;For efficiency, the implementation uses vectorized operations where possible, pre-computes dimension strides for direct array indexing, and caches recently accessed tables to avoid repeated disk I/O.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/bc5d-stair-step-vs-smooth.png" alt="Stair-Step vs Smooth Curve Approximation" style="width: 100%; max-width: 750px; display: block; margin: 30px auto; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;p style="text-align: center; font-style: italic; color: #666; margin-top: -15px;"&gt;Figure 1: The continuous ML model (red) produces smooth BC corrections across the velocity range. The discretized lookup table (blue) samples at fixed intervals, creating a stair-step approximation. Note the increased correction factors in the transonic region (900-1300 fps).&lt;/p&gt;

&lt;h3&gt;4. Results and Analysis&lt;/h3&gt;
&lt;h4&gt;4.1 Approximation Fidelity&lt;/h4&gt;
&lt;p&gt;We evaluated the BC5D lookup tables against the continuous ML model across a comprehensive test suite: 168-grain .308 projectiles with G1 BC of 0.475, fired at 2700 fps muzzle velocity. Table 1 presents velocity predictions at distances from 200 to 1000 yards.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table 1: Remaining Velocity Comparison (fps)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;Physics Only&lt;/th&gt;
&lt;th&gt;BC5D Lookup&lt;/th&gt;
&lt;th&gt;Online ML&lt;/th&gt;
&lt;th&gt;Δ (Lookup vs ML)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200 yd&lt;/td&gt;
&lt;td&gt;2334&lt;/td&gt;
&lt;td&gt;2330&lt;/td&gt;
&lt;td&gt;2298&lt;/td&gt;
&lt;td&gt;+32 fps (+1.4%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400 yd&lt;/td&gt;
&lt;td&gt;2002&lt;/td&gt;
&lt;td&gt;1994&lt;/td&gt;
&lt;td&gt;1951&lt;/td&gt;
&lt;td&gt;+43 fps (+2.2%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;600 yd&lt;/td&gt;
&lt;td&gt;1703&lt;/td&gt;
&lt;td&gt;1688&lt;/td&gt;
&lt;td&gt;1642&lt;/td&gt;
&lt;td&gt;+46 fps (+2.8%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;800 yd&lt;/td&gt;
&lt;td&gt;1444&lt;/td&gt;
&lt;td&gt;1416&lt;/td&gt;
&lt;td&gt;1364&lt;/td&gt;
&lt;td&gt;+52 fps (+3.8%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000 yd&lt;/td&gt;
&lt;td&gt;1198&lt;/td&gt;
&lt;td&gt;1154&lt;/td&gt;
&lt;td&gt;1008&lt;/td&gt;
&lt;td&gt;+146 fps (+14.5%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Several patterns emerge from this comparison. First, both BC5D lookup and online ML show substantially more velocity decay than physics-only calculations using constant BC—validating that both approaches capture drag enhancement effects invisible to traditional methods. Second, the lookup tables track the ML model within 3-4% through 800 yards, representing the supersonic and early transonic portions of the flight. Third, significant divergence appears at 1000 yards (+14.5%), where the projectile has decelerated deep into the transonic regime.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/bc5d-velocity-comparison.png" alt="Velocity Predictions Comparison" style="width: 100%; max-width: 750px; display: block; margin: 30px auto; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;h4&gt;4.2 Energy Predictions&lt;/h4&gt;
&lt;p&gt;Table 2 presents the same comparison for remaining kinetic energy, which exhibits squared sensitivity to velocity errors.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Table 2: Remaining Energy Comparison (ft-lb)&lt;/strong&gt;&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Range&lt;/th&gt;
&lt;th&gt;Physics Only&lt;/th&gt;
&lt;th&gt;BC5D Lookup&lt;/th&gt;
&lt;th&gt;Online ML&lt;/th&gt;
&lt;th&gt;Δ (Lookup vs ML)&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;200 yd&lt;/td&gt;
&lt;td&gt;2033&lt;/td&gt;
&lt;td&gt;2024&lt;/td&gt;
&lt;td&gt;1970&lt;/td&gt;
&lt;td&gt;+54 ft-lb (+2.7%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;400 yd&lt;/td&gt;
&lt;td&gt;1495&lt;/td&gt;
&lt;td&gt;1483&lt;/td&gt;
&lt;td&gt;1420&lt;/td&gt;
&lt;td&gt;+63 ft-lb (+4.4%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;600 yd&lt;/td&gt;
&lt;td&gt;1081&lt;/td&gt;
&lt;td&gt;1062&lt;/td&gt;
&lt;td&gt;1005&lt;/td&gt;
&lt;td&gt;+57 ft-lb (+5.7%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;800 yd&lt;/td&gt;
&lt;td&gt;778&lt;/td&gt;
&lt;td&gt;748&lt;/td&gt;
&lt;td&gt;694&lt;/td&gt;
&lt;td&gt;+54 ft-lb (+7.8%)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;1000 yd&lt;/td&gt;
&lt;td&gt;535&lt;/td&gt;
&lt;td&gt;497&lt;/td&gt;
&lt;td&gt;379&lt;/td&gt;
&lt;td&gt;+118 ft-lb (+31.1%)&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Energy predictions show proportionally larger deviations due to the v² relationship, reaching 31% at 1000 yards. However, for practical shooting applications, the 800-yard accuracy of 7.8% remains within acceptable bounds for most use cases.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/bc5d-energy-comparison.png" alt="Energy Predictions Comparison" style="width: 100%; max-width: 750px; display: block; margin: 30px auto; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/bc5d-deviation-analysis.png" alt="BC5D Deviation Analysis" style="width: 100%; max-width: 750px; display: block; margin: 30px auto; box-shadow: 0 4px 12px rgba(0,0,0,0.15);"&gt;&lt;/p&gt;
&lt;p style="text-align: center; font-style: italic; color: #666; margin-top: -15px;"&gt;Figure 2: Deviation of BC5D lookup table predictions from the continuous ML model. Note that velocity deviations remain under 5% through 800 yards, with pronounced divergence at 1000 yards where transonic effects dominate.&lt;/p&gt;

&lt;h4&gt;4.3 Transonic Degradation Analysis&lt;/h4&gt;
&lt;p&gt;The pronounced divergence at 1000 yards reflects a fundamental characteristic of our discretization approach: piecewise-linear interpolation cannot faithfully reproduce the rapid, non-linear BC variations occurring in transonic flow. Between Mach 1.2 and Mach 0.8 (approximately 1300-900 fps at sea level), shock wave formation and detachment produce drag coefficient changes that defy smooth approximation.&lt;/p&gt;
&lt;p&gt;The continuous ML model, trained on Doppler-derived measurements through this regime, captures these non-linearities through its learned function representation. The lookup table, sampling at fixed velocity intervals, necessarily smooths over rapid transitions between samples. This smoothing introduces systematic bias: the lookup table predicts more gradual drag increases than actually occur, resulting in optimistic velocity and energy predictions.&lt;/p&gt;
&lt;p&gt;Three potential mitigations exist for this transonic fidelity gap:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Finer sampling:&lt;/strong&gt; Reducing velocity bin spacing in the transonic region (e.g., 25 fps instead of 100 fps) would capture more of the non-linear structure, at the cost of increased table size.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Non-linear interpolation:&lt;/strong&gt; Cubic or spline interpolation could better approximate curved function behavior between samples, with increased computational cost.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hybrid approaches:&lt;/strong&gt; Using lookup tables for supersonic flight and falling back to simplified analytical transonic models could bound worst-case errors without requiring connectivity.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;4.4 Stair-Step vs. Smooth Curve Analogy&lt;/h4&gt;
&lt;p&gt;The relationship between continuous ML and discretized lookup tables parallels the distinction between analog and digital signals in audio engineering. The ML model evaluates its learned function continuously—every input maps to a precisely computed output through the model's parameter space, drawing a smooth curve through the correction landscape. The lookup table samples this smooth curve at fixed intervals, storing discrete values that are linearly interpolated at query time.&lt;/p&gt;
&lt;p&gt;Consider a CD's 44.1 kHz sampling rate: by capturing 44,100 amplitude values per second, digital audio achieves perceptual equivalence to the analog source because the samples are dense enough that interpolation artifacts fall below human hearing thresholds. The same principle applies here—our velocity bins are fine enough (typically 100 fps spacing) that for most of the flight envelope, the stair-step approximation is imperceptible in practical shooting applications.&lt;/p&gt;
&lt;p&gt;The transonic regime represents our "high-frequency content"—rapid changes that require proportionally finer sampling to capture faithfully. Just as audio systems may exhibit aliasing when sampling signals containing frequencies above the Nyquist limit, our lookup tables exhibit approximation error when the underlying function changes faster than our sampling density can track.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;5. Discussion&lt;/h3&gt;
&lt;h4&gt;5.1 Practical Deployment Considerations&lt;/h4&gt;
&lt;p&gt;The BC5D tables have been deployed via a content delivery network with caliber-specific downloads. Users retrieve only the tables for calibers they actually shoot, with typical total downloads of 3-5 MB for a two-caliber configuration. Tables are cached locally with CRC32 validation ensuring data integrity after download.&lt;/p&gt;
&lt;p&gt;The command-line interface supports three operational modes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Online ML:&lt;/strong&gt; Direct API queries for maximum accuracy (requires connectivity)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Offline BC5D:&lt;/strong&gt; Lookup table interpolation (no connectivity required)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Physics only:&lt;/strong&gt; Traditional constant-BC calculation (baseline fallback)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This tiered approach allows users to select the accuracy-connectivity trade-off appropriate to their situation: competitive shooters may prefer online ML for load development, while field use may necessitate offline tables.&lt;/p&gt;
&lt;h4&gt;5.2 Comparison to Related Approaches&lt;/h4&gt;
&lt;p&gt;Our work relates to several established techniques in the model compression literature. Unlike neural network quantization, which reduces precision of model parameters, we compute exact outputs at sample points and interpolate between them—the stored values are full-precision, only the input space is discretized. Unlike knowledge distillation, we make no attempt to train a smaller model; the "student" is simply a lookup table with no learned parameters.&lt;/p&gt;
&lt;p&gt;The closest analogue is the function tabulation commonly employed in embedded systems and real-time simulation. Our contribution extends this paradigm to ML model outputs, demonstrating that the technique transfers effectively to learned functions trained on empirical data rather than analytical expressions.&lt;/p&gt;
&lt;h4&gt;5.3 Limitations and Future Work&lt;/h4&gt;
&lt;p&gt;Several limitations merit acknowledgment. First, the tables capture only the correction function learned by our specific ML model; improvements to the model require regenerating all tables. Second, atmospheric variations (temperature, pressure, humidity) are not currently parameterized—tables assume standard conditions, with atmospheric corrections applied as separate multiplicative factors. Third, the 14% transonic deviation may be unacceptable for applications requiring high precision at extreme range.&lt;/p&gt;
&lt;p&gt;Future work may address these limitations through:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Finer transonic sampling with adaptive bin spacing&lt;/li&gt;
&lt;li&gt;Additional dimensions for atmospheric parameters&lt;/li&gt;
&lt;li&gt;Version 2 tables with drag-model-specific optimization&lt;/li&gt;
&lt;li&gt;Exploration of non-linear interpolation schemes&lt;/li&gt;
&lt;/ul&gt;
&lt;hr&gt;
&lt;h3&gt;6. Conclusion&lt;/h3&gt;
&lt;p&gt;This paper has presented a methodology for discretizing continuous machine learning models into offline lookup tables, specifically addressing ballistic coefficient corrections for trajectory prediction. The BC5D table architecture spans five dimensions (weight, BC, muzzle velocity, current velocity, drag model) with caliber-specific instantiation, achieving file sizes of 1.0-1.5 MB per caliber.&lt;/p&gt;
&lt;p&gt;Empirical evaluation demonstrates that piecewise-linear interpolation over this discretized space achieves velocity predictions within 5% of the continuous ML model through supersonic and early transonic flight regimes, with predictable degradation to 14% deviation in deep transonic regions where non-linear drag variations exceed the approximation capacity of fixed-interval sampling.&lt;/p&gt;
&lt;p&gt;We have argued that this accuracy-connectivity trade-off represents a practical compromise for field deployment, drawing analogy to digital audio sampling where sufficiently fine discretization renders quantization artifacts imperceptible for typical use cases. The transonic regime, exhibiting rapid non-linearities analogous to high-frequency audio content, requires proportionally finer sampling to capture faithfully—a trade-off that can be addressed through adaptive bin spacing in future table versions.&lt;/p&gt;
&lt;p&gt;The broader contribution of this work lies in demonstrating that ML model outputs can be effectively tabulated for offline deployment without resorting to model compression techniques that sacrifice learned representations. For application domains where the input space is bounded and query patterns are predictable, lookup table approximation offers a deployment pathway that preserves ML-derived insights while eliminating infrastructure dependencies.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;References&lt;/h3&gt;
&lt;p&gt;[1] McCoy, R. L. (1999). &lt;em&gt;Modern Exterior Ballistics: The Launch and Flight Dynamics of Symmetric Projectiles&lt;/em&gt;. Schiffer Publishing.&lt;/p&gt;
&lt;p&gt;[2] Carlucci, D. E., &amp;amp; Jacobson, S. S. (2018). &lt;em&gt;Ballistics: Theory and Design of Guns and Ammunition&lt;/em&gt; (3rd ed.). CRC Press.&lt;/p&gt;
&lt;p&gt;[3] Weinacht, P., Cooper, G. R., &amp;amp; Newill, J. F. (2005). "Analytical Prediction of Projectile Flight." Army Research Laboratory Technical Report ARL-TR-3567.&lt;/p&gt;
&lt;p&gt;[4] Silton, S. I. (2005). "Navier-Stokes Computations for a Spinning Projectile from Subsonic to Supersonic Speeds." Journal of Spacecraft and Rockets, 42(2), 223-231.&lt;/p&gt;
&lt;p&gt;[5] Litz, B. (2015). &lt;em&gt;Applied Ballistics for Long Range Shooting&lt;/em&gt; (3rd ed.). Applied Ballistics LLC.&lt;/p&gt;
&lt;p&gt;[6] SAAMI (2015). "Voluntary Industry Performance Standards for Pressure and Velocity of Centerfire Rifle Sporting Ammunition." Sporting Arms and Ammunition Manufacturers' Institute.&lt;/p&gt;
&lt;p&gt;[7] Anderson, J. D. (2017). &lt;em&gt;Fundamentals of Aerodynamics&lt;/em&gt; (6th ed.). McGraw-Hill Education.&lt;/p&gt;
&lt;p&gt;[8] Courtney, M., &amp;amp; Courtney, A. (2012). "Experimental Tests of the Litz Model for Ballistic Coefficient Variation with Velocity." arXiv:1201.3621.&lt;/p&gt;
&lt;p&gt;[9] Han, S., Mao, H., &amp;amp; Dally, W. J. (2016). "Deep Compression: Compressing Deep Neural Networks with Pruning, Trained Quantization and Huffman Coding." ICLR 2016.&lt;/p&gt;
&lt;p&gt;[10] Jacob, B., et al. (2018). "Quantization and Training of Neural Networks for Efficient Integer-Arithmetic-Only Inference." CVPR 2018.&lt;/p&gt;
&lt;p&gt;[11] Hinton, G., Vinyals, O., &amp;amp; Dean, J. (2015). "Distilling the Knowledge in a Neural Network." arXiv:1503.02531.&lt;/p&gt;
&lt;p&gt;[12] Heckbert, P. S. (1986). "Survey of Texture Mapping." IEEE Computer Graphics and Applications, 6(11), 56-67.&lt;/p&gt;
&lt;p&gt;[13] Jeong, K., &amp;amp; Kim, S. (2003). "Lookup Table-Based FPGA Implementation of Trigonometric Functions." Journal of the Korean Physical Society, 43, 843-847.&lt;/p&gt;
&lt;p&gt;[14] Fraden, J. (2016). &lt;em&gt;Handbook of Modern Sensors: Physics, Designs, and Applications&lt;/em&gt; (5th ed.). Springer.&lt;/p&gt;
&lt;p&gt;[15] Rienecker, M. M., et al. (2011). "MERRA: NASA's Modern-Era Retrospective Analysis for Research and Applications." Journal of Climate, 24(14), 3624-3648.&lt;/p&gt;
&lt;p&gt;[16] Karis, B. (2013). "Real Shading in Unreal Engine 4." SIGGRAPH 2013 Course Notes.&lt;/p&gt;
&lt;p&gt;[17] Ingalls, J. M. (1893). &lt;em&gt;Exterior Ballistics in the Plane of Fire&lt;/em&gt;. D. Van Nostrand Company.&lt;/p&gt;
&lt;p&gt;[18] Litz, B. (2011). "Ballistic Coefficient Testing of the .308 175gr Sierra Matchking." Applied Ballistics Technical Note.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The author develops ballistics simulation software and maintains the trajectory prediction API at ballistics.7.62x51mm.sh. Source code for the BC5D table generator is available at github.com/ajokela/ballistics-engine.&lt;/em&gt;&lt;/p&gt;</description><category>aerodynamics</category><category>approximation theory</category><category>ballistics</category><category>caliber-specific models</category><category>drag coefficients</category><category>edge computing</category><category>embedded systems</category><category>interpolation</category><category>lookup tables</category><category>machine learning</category><category>model compression</category><category>model deployment</category><category>neural networks</category><category>numerical methods</category><category>offline computing</category><category>physics simulation</category><category>piecewise linear approximation</category><category>quantization</category><category>scientific computing</category><category>trajectory calculation</category><guid>https://tinycomputers.io/posts/discretizing-continuous-ml-models-offline-ballistic-coefficient-corrections.html</guid><pubDate>Wed, 28 Jan 2026 14:30:00 GMT</pubDate></item><item><title>Physics-First ML: Why AI Should Correct, Not Replace, Scientific Models</title><link>https://tinycomputers.io/posts/physics-first-ml-why-ai-should-correct-not-replace-scientific-models.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/physics-first-ml-why-ai-should-correct-not-replace-scientific-models_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;16 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h4&gt;The Seductive Trap of Pure ML&lt;/h4&gt;
&lt;p&gt;There's a pattern I've seen repeated across scientific computing: a team has a physics-based model that works reasonably well. Someone suggests "we could use machine learning to improve this." Six months later, they've replaced the physics entirely with a neural network trained on historical data. The model works great—until it doesn't.&lt;/p&gt;
&lt;p&gt;In ballistics, this failure mode isn't just embarrassing; it's dangerous. A 10% error in predicting bullet drop at 1000 yards translates to missing a target by nearly a foot. In hunting, that's a wounded animal. In defense applications, the consequences are graver still.&lt;/p&gt;
&lt;p&gt;After spending a considerable amount of time thinking about and studying ballistics systems, I've arrived at a principle that runs counter to the current AI zeitgeist: machine learning should correct physics, not replace it. This isn't a rejection of ML—it's a recognition that physics provides something ML cannot: bounded, predictable behavior grounded in first principles.&lt;/p&gt;
&lt;h4&gt;The Philosophy: Physics as Foundation, ML as Refinement&lt;/h4&gt;
&lt;p&gt;Consider two approaches to predicting muzzle velocity from powder charge:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Approach A: Pure ML&lt;/span&gt;
&lt;span class="n"&gt;velocity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;neural_network&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;powder_charge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bullet_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;barrel_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Approach B: Physics-First with ML Correction&lt;/span&gt;
&lt;span class="n"&gt;base_velocity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;physics_model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;calculate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;powder_charge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bullet_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;barrel_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;correction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ml_model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predict_correction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;powder_charge&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bullet_weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;barrel_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;final_velocity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base_velocity&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;correction&lt;/span&gt;  &lt;span class="c1"&gt;# correction is bounded: 0.95-1.05&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Approach A learns everything from data. It might achieve lower training error, but it has no guardrails. Feed it an unusual combination of inputs, and it might predict a velocity of -500 fps or 50,000 fps. The model has no concept of what's physically possible.&lt;/p&gt;
&lt;p&gt;Approach B starts with physics—conservation of energy, gas dynamics, thermodynamics. These equations have been validated for centuries. The ML component only learns the &lt;em&gt;residual&lt;/em&gt;: the small systematic errors that arise from simplified assumptions, manufacturing tolerances, or environmental factors the physics model doesn't capture.&lt;/p&gt;
&lt;p&gt;Critically, the correction factor is bounded. In our production systems, we enforce limits of 0.8x to 1.25x for ballistic coefficient corrections. If the ML model wants to apply a larger correction, we reject it entirely rather than trust an outlier prediction.&lt;/p&gt;
&lt;h4&gt;Why Bounded Corrections Matter&lt;/h4&gt;
&lt;p&gt;The bound isn't arbitrary. It emerges from understanding what ML can legitimately learn versus what indicates a fundamental mismatch.&lt;/p&gt;
&lt;p&gt;A ballistic coefficient (BC) published by a manufacturer might differ from real-world performance by 5-15% due to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Manufacturing tolerances in bullet production&lt;/li&gt;
&lt;li&gt;Differences between the manufacturer's test conditions and yours&lt;/li&gt;
&lt;li&gt;Simplifications in how BC is measured and reported&lt;/li&gt;
&lt;li&gt;Velocity-dependent effects not captured in a single BC value&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These are exactly the kinds of systematic errors ML can learn to correct. A well-trained model might learn that Brand X's published BCs are consistently 8% optimistic, or that handloaded ammunition with a specific powder tends to perform 3% better than factory loads.&lt;/p&gt;
&lt;p&gt;But a correction factor of 2.5x? That's not a refinement—that's a fundamental mismatch. Either the input data is wrong, or we've matched against the wrong reference bullet entirely.&lt;/p&gt;
&lt;div id="correction-factor-chart" style="width: 100%; max-width: 800px; margin: 30px auto;"&gt;
&lt;svg viewbox="0 0 800 400" xmlns="http://www.w3.org/2000/svg"&gt;
  &lt;!-- Background --&gt;
  &lt;rect width="800" height="400" fill="#1a1a2e"&gt;&lt;/rect&gt;

  &lt;!-- Grid lines --&gt;
  &lt;g stroke="#333" stroke-width="1"&gt;
    &lt;line x1="80" y1="50" x2="80" y2="320"&gt;&lt;/line&gt;
    &lt;line x1="80" y1="320" x2="750" y2="320"&gt;&lt;/line&gt;
    &lt;!-- Horizontal grid --&gt;
    &lt;line x1="80" y1="185" x2="750" y2="185" stroke-dasharray="5,5"&gt;&lt;/line&gt;
    &lt;line x1="80" y1="118" x2="750" y2="118" stroke-dasharray="5,5"&gt;&lt;/line&gt;
    &lt;line x1="80" y1="252" x2="750" y2="252" stroke-dasharray="5,5"&gt;&lt;/line&gt;
  &lt;/g&gt;

  &lt;!-- Acceptable zone (0.8-1.25) --&gt;
  &lt;rect x="80" y="118" width="670" height="134" fill="#2d5a3d" opacity="0.3"&gt;&lt;/rect&gt;

  &lt;!-- Axis labels --&gt;
  &lt;text x="415" y="380" fill="#ccc" text-anchor="middle" font-family="system-ui" font-size="14"&gt;Correction Factor&lt;/text&gt;
  &lt;text x="30" y="185" fill="#ccc" text-anchor="middle" font-family="system-ui" font-size="14" transform="rotate(-90, 30, 185)"&gt;Drop Error at 1000 yards (inches)&lt;/text&gt;

  &lt;!-- X-axis values --&gt;
  &lt;text x="80" y="340" fill="#888" text-anchor="middle" font-family="monospace" font-size="11"&gt;0.5x&lt;/text&gt;
  &lt;text x="191" y="340" fill="#888" text-anchor="middle" font-family="monospace" font-size="11"&gt;0.8x&lt;/text&gt;
  &lt;text x="340" y="340" fill="#4ade80" text-anchor="middle" font-family="monospace" font-size="11"&gt;1.0x&lt;/text&gt;
  &lt;text x="452" y="340" fill="#888" text-anchor="middle" font-family="monospace" font-size="11"&gt;1.25x&lt;/text&gt;
  &lt;text x="600" y="340" fill="#888" text-anchor="middle" font-family="monospace" font-size="11"&gt;2.0x&lt;/text&gt;
  &lt;text x="750" y="340" fill="#888" text-anchor="middle" font-family="monospace" font-size="11"&gt;2.5x&lt;/text&gt;

  &lt;!-- Y-axis values --&gt;
  &lt;text x="70" y="320" fill="#888" text-anchor="end" font-family="monospace" font-size="11"&gt;0&lt;/text&gt;
  &lt;text x="70" y="252" fill="#888" text-anchor="end" font-family="monospace" font-size="11"&gt;20&lt;/text&gt;
  &lt;text x="70" y="185" fill="#888" text-anchor="end" font-family="monospace" font-size="11"&gt;40&lt;/text&gt;
  &lt;text x="70" y="118" fill="#888" text-anchor="end" font-family="monospace" font-size="11"&gt;60&lt;/text&gt;
  &lt;text x="70" y="50" fill="#888" text-anchor="end" font-family="monospace" font-size="11"&gt;80&lt;/text&gt;

  &lt;!-- Error curve --&gt;
  &lt;path d="M 80 50
           Q 150 180, 191 280
           Q 230 310, 340 320
           Q 420 310, 452 290
           Q 550 220, 600 140
           Q 680 60, 750 30" fill="none" stroke="#f87171" stroke-width="3"&gt;&lt;/path&gt;

  &lt;!-- Acceptable zone labels --&gt;
  &lt;text x="321" y="100" fill="#4ade80" text-anchor="middle" font-family="system-ui" font-size="12" font-weight="bold"&gt;ACCEPTABLE ZONE&lt;/text&gt;
  &lt;text x="321" y="115" fill="#4ade80" text-anchor="middle" font-family="system-ui" font-size="11"&gt;(0.8x - 1.25x)&lt;/text&gt;

  &lt;!-- Danger zone labels --&gt;
  &lt;text x="135" y="80" fill="#f87171" text-anchor="middle" font-family="system-ui" font-size="11"&gt;REJECT&lt;/text&gt;
  &lt;text x="650" y="80" fill="#f87171" text-anchor="middle" font-family="system-ui" font-size="11"&gt;REJECT&lt;/text&gt;

  &lt;!-- Sweet spot annotation --&gt;
  &lt;circle cx="340" cy="320" r="6" fill="#4ade80"&gt;&lt;/circle&gt;
  &lt;text x="340" y="305" fill="#4ade80" text-anchor="middle" font-family="system-ui" font-size="10"&gt;1.0x = 0 error&lt;/text&gt;

  &lt;!-- 2.49x failure point --&gt;
  &lt;circle cx="730" cy="35" r="8" fill="#ef4444" stroke="#fff" stroke-width="2"&gt;&lt;/circle&gt;
  &lt;text x="715" y="55" fill="#ef4444" text-anchor="end" font-family="system-ui" font-size="10"&gt;MBA-589 Bug&lt;/text&gt;
  &lt;text x="715" y="68" fill="#ef4444" text-anchor="end" font-family="system-ui" font-size="10"&gt;2.49x = 37% error&lt;/text&gt;

  &lt;!-- Title --&gt;
  &lt;text x="415" y="30" fill="#fff" text-anchor="middle" font-family="system-ui" font-size="16" font-weight="bold"&gt;Impact of BC Correction Factor on Prediction Error&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;

&lt;p&gt;The chart above illustrates the relationship between correction factor and prediction error. Within the acceptable zone (0.8x to 1.25x), errors remain manageable—typically under 20 inches at 1000 yards. But as correction factors grow larger, errors explode. The red dot marks a real bug we discovered: a 2.49x correction that produced 37% error in drop predictions.&lt;/p&gt;
&lt;h4&gt;A Real-World Failure: The 2.49x Bug&lt;/h4&gt;
&lt;p&gt;This isn't theoretical. In our BC enhancement service, we had a bug that perfectly illustrates the danger of unbounded ML corrections.&lt;/p&gt;
&lt;p&gt;A user submitted a calculation for a 140-grain 6.5mm bullet with a G7 BC of 0.238. Our system attempted to enhance this BC using doppler-derived reference data. The matching algorithm found a reference bullet—a 142-grain Sierra MatchKing—based on caliber and weight similarity.&lt;/p&gt;
&lt;p&gt;The problem? The Sierra 142gr SMK has a G7 BC of approximately 0.593. Our system computed a "correction factor" of 2.49x and confidently applied it.&lt;/p&gt;
&lt;p&gt;The results were catastrophic:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;User's BC (0.238)&lt;/th&gt;
&lt;th&gt;"Enhanced" BC (0.593)&lt;/th&gt;
&lt;th&gt;Error&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Drop at 1000 yards&lt;/td&gt;
&lt;td&gt;312.4"&lt;/td&gt;
&lt;td&gt;196.8"&lt;/td&gt;
&lt;td&gt;37%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Time of Flight&lt;/td&gt;
&lt;td&gt;1.847s&lt;/td&gt;
&lt;td&gt;1.512s&lt;/td&gt;
&lt;td&gt;18%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Wind Drift (10 mph)&lt;/td&gt;
&lt;td&gt;58.2"&lt;/td&gt;
&lt;td&gt;36.7"&lt;/td&gt;
&lt;td&gt;37%&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;A shooter trusting the "enhanced" prediction would have aimed nearly 10 feet too low. The ML system was confidently wrong because it had no concept of reasonable bounds.&lt;/p&gt;
&lt;p&gt;The fix was straightforward: reject any match where the reference BC differs from the input BC by more than 30%. If the user says their BC is 0.238, we don't believe a database entry claiming 0.593 is the "true" value—no matter how similar the bullet weights.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# The fix: BC tolerance check&lt;/span&gt;
&lt;span class="n"&gt;bc_ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;matched_bc&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;user_input_bc&lt;/span&gt;
&lt;span class="n"&gt;BC_TOLERANCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;  &lt;span class="c1"&gt;# 30% maximum deviation&lt;/span&gt;

&lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bc_ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;BC_TOLERANCE&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;bc_ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;BC_TOLERANCE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;logger&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"BC mismatch: user=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;user_input_bc&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.4f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;, matched=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;matched_bc&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.4f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;  &lt;span class="c1"&gt;# Reject the match, don't guess&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Ground Truth: Doppler-Derived Data&lt;/h4&gt;
&lt;p&gt;The foundation of any ML correction system is ground truth data. In exterior ballistics, the gold standard is doppler radar measurement—tracking a bullet's actual velocity throughout its flight path, not just at the muzzle.&lt;/p&gt;
&lt;p&gt;Published ballistic coefficients are typically derived from limited testing under controlled conditions. Doppler data captures real-world performance across the entire velocity envelope, from supersonic through transonic to subsonic flight. This is particularly crucial in the transonic region (roughly Mach 0.9 to 1.1), where drag characteristics change dramatically and simple models break down.&lt;/p&gt;
&lt;p&gt;We've built our correction models on an extensive dataset of doppler-derived measurements. This data captures the true behavior of projectiles across varying conditions—not the idealized behavior assumed by physics models or the optimistic values sometimes found in marketing materials.&lt;/p&gt;
&lt;p&gt;Lapua, to their credit, publishes comprehensive doppler-derived BC data for their projectiles, making it freely available to the shooting community. Their data shows the velocity-dependent nature of BC that simpler models ignore:&lt;/p&gt;
&lt;div id="lapua-bc-chart" style="width: 100%; max-width: 800px; margin: 30px auto;"&gt;
&lt;svg viewbox="0 0 800 400" xmlns="http://www.w3.org/2000/svg"&gt;
  &lt;!-- Background --&gt;
  &lt;rect width="800" height="400" fill="#1a1a2e"&gt;&lt;/rect&gt;

  &lt;!-- Grid lines --&gt;
  &lt;g stroke="#333" stroke-width="1"&gt;
    &lt;line x1="80" y1="50" x2="80" y2="320"&gt;&lt;/line&gt;
    &lt;line x1="80" y1="320" x2="750" y2="320"&gt;&lt;/line&gt;
    &lt;!-- Horizontal grid --&gt;
    &lt;line x1="80" y1="185" x2="750" y2="185" stroke-dasharray="5,5"&gt;&lt;/line&gt;
    &lt;line x1="80" y1="118" x2="750" y2="118" stroke-dasharray="5,5"&gt;&lt;/line&gt;
    &lt;line x1="80" y1="252" x2="750" y2="252" stroke-dasharray="5,5"&gt;&lt;/line&gt;
  &lt;/g&gt;

  &lt;!-- Transonic region highlight --&gt;
  &lt;rect x="200" y="50" width="150" height="270" fill="#854d0e" opacity="0.2"&gt;&lt;/rect&gt;
  &lt;text x="275" y="70" fill="#fbbf24" text-anchor="middle" font-family="system-ui" font-size="10"&gt;TRANSONIC&lt;/text&gt;

  &lt;!-- Axis labels --&gt;
  &lt;text x="415" y="380" fill="#ccc" text-anchor="middle" font-family="system-ui" font-size="14"&gt;Velocity (fps)&lt;/text&gt;
  &lt;text x="30" y="185" fill="#ccc" text-anchor="middle" font-family="system-ui" font-size="14" transform="rotate(-90, 30, 185)"&gt;G7 Ballistic Coefficient&lt;/text&gt;

  &lt;!-- X-axis values --&gt;
  &lt;text x="80" y="340" fill="#888" text-anchor="middle" font-family="monospace" font-size="11"&gt;800&lt;/text&gt;
  &lt;text x="200" y="340" fill="#888" text-anchor="middle" font-family="monospace" font-size="11"&gt;1000&lt;/text&gt;
  &lt;text x="350" y="340" fill="#888" text-anchor="middle" font-family="monospace" font-size="11"&gt;1200&lt;/text&gt;
  &lt;text x="500" y="340" fill="#888" text-anchor="middle" font-family="monospace" font-size="11"&gt;1600&lt;/text&gt;
  &lt;text x="650" y="340" fill="#888" text-anchor="middle" font-family="monospace" font-size="11"&gt;2200&lt;/text&gt;
  &lt;text x="750" y="340" fill="#888" text-anchor="middle" font-family="monospace" font-size="11"&gt;2800&lt;/text&gt;

  &lt;!-- Y-axis values --&gt;
  &lt;text x="70" y="320" fill="#888" text-anchor="end" font-family="monospace" font-size="11"&gt;0.20&lt;/text&gt;
  &lt;text x="70" y="252" fill="#888" text-anchor="end" font-family="monospace" font-size="11"&gt;0.24&lt;/text&gt;
  &lt;text x="70" y="185" fill="#888" text-anchor="end" font-family="monospace" font-size="11"&gt;0.28&lt;/text&gt;
  &lt;text x="70" y="118" fill="#888" text-anchor="end" font-family="monospace" font-size="11"&gt;0.32&lt;/text&gt;
  &lt;text x="70" y="50" fill="#888" text-anchor="end" font-family="monospace" font-size="11"&gt;0.36&lt;/text&gt;

  &lt;!-- Doppler-derived BC curve (real behavior) --&gt;
  &lt;path d="M 80 200
           Q 150 190, 200 280
           Q 275 310, 350 220
           Q 450 170, 550 150
           Q 650 140, 750 135" fill="none" stroke="#4ade80" stroke-width="3"&gt;&lt;/path&gt;

  &lt;!-- Published single BC (flat line) --&gt;
  &lt;line x1="80" y1="180" x2="750" y2="180" stroke="#f87171" stroke-width="2" stroke-dasharray="10,5"&gt;&lt;/line&gt;

  &lt;!-- Legend --&gt;
  &lt;rect x="550" y="85" width="180" height="55" fill="#1a1a2e" stroke="#444" rx="5"&gt;&lt;/rect&gt;
  &lt;line x1="560" y1="102" x2="590" y2="102" stroke="#4ade80" stroke-width="3"&gt;&lt;/line&gt;
  &lt;text x="600" y="106" fill="#ccc" font-family="system-ui" font-size="11"&gt;Doppler-derived BC&lt;/text&gt;
  &lt;line x1="560" y1="125" x2="590" y2="125" stroke="#f87171" stroke-width="2" stroke-dasharray="10,5"&gt;&lt;/line&gt;
  &lt;text x="600" y="129" fill="#ccc" font-family="system-ui" font-size="11"&gt;Published BC (single value)&lt;/text&gt;

  &lt;!-- Annotations --&gt;
  &lt;circle cx="275" cy="295" r="5" fill="#fbbf24"&gt;&lt;/circle&gt;
  &lt;text x="290" y="300" fill="#fbbf24" font-family="system-ui" font-size="10"&gt;BC drops in transonic&lt;/text&gt;

  &lt;!-- Title --&gt;
  &lt;text x="415" y="30" fill="#fff" text-anchor="middle" font-family="system-ui" font-size="16" font-weight="bold"&gt;Velocity-Dependent BC: Doppler Data vs. Published Values&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;

&lt;p&gt;The green curve shows actual BC measured via doppler radar across the velocity envelope. Notice the significant drop in the transonic region—this is real physics that a single published BC value (the dashed red line) cannot capture.&lt;/p&gt;
&lt;p&gt;When our ML correction system encounters a bullet, it doesn't just look up a single BC. It retrieves or interpolates a velocity-dependent BC curve, then applies a bounded correction based on how similar bullets have performed relative to their published specifications.&lt;/p&gt;
&lt;h4&gt;The Correction Architecture&lt;/h4&gt;
&lt;p&gt;Our BC enhancement service follows a strict hierarchy:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Physics first&lt;/strong&gt;: Calculate trajectory using established equations of motion, drag models (G1, G7), and atmospheric corrections.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Data lookup&lt;/strong&gt;: Match the input bullet against our reference database using caliber, weight, and—critically—BC similarity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Bounded correction&lt;/strong&gt;: If a match is found &lt;em&gt;and&lt;/em&gt; the reference BC is within tolerance, compute a correction factor clamped to [0.8, 1.25].&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Confidence scoring&lt;/strong&gt;: Report how confident we are in the enhancement, based on match quality.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Graceful degradation&lt;/strong&gt;: If no good match exists, return the original physics prediction with enhanced=false rather than guessing.&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;BCEnhancementService&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Correction factor bounds - these are NOT arbitrary&lt;/span&gt;
    &lt;span class="n"&gt;MIN_CORRECTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.80&lt;/span&gt;  &lt;span class="c1"&gt;# -20% maximum reduction&lt;/span&gt;
    &lt;span class="n"&gt;MAX_CORRECTION&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.25&lt;/span&gt;  &lt;span class="c1"&gt;# +25% maximum increase&lt;/span&gt;
    &lt;span class="n"&gt;BC_MATCH_TOLERANCE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.30&lt;/span&gt;  &lt;span class="c1"&gt;# 30% BC similarity required&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;enhance_bc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_bc&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;caliber&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                   &lt;span class="n"&gt;velocity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;EnhancementResult&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;

        &lt;span class="c1"&gt;# Step 1: Find matching reference bullet&lt;/span&gt;
        &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_find_reference_match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caliber&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;user_bc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EnhancementResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;enhanced_bc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_bc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;applied&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"No matching reference data"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Step 2: Verify BC is within tolerance&lt;/span&gt;
        &lt;span class="n"&gt;bc_ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;reference_bc&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;user_bc&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BC_MATCH_TOLERANCE&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;bc_ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;BC_MATCH_TOLERANCE&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EnhancementResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
                &lt;span class="n"&gt;enhanced_bc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;user_bc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;applied&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"BC mismatch: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bc_ratio&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;x outside tolerance"&lt;/span&gt;
            &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Step 3: Compute bounded correction&lt;/span&gt;
        &lt;span class="n"&gt;raw_correction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;doppler_bc&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_bc&lt;/span&gt;
        &lt;span class="n"&gt;correction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;max&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MIN_CORRECTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="nb"&gt;min&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MAX_CORRECTION&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;raw_correction&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

        &lt;span class="n"&gt;enhanced_bc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;user_bc&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;correction&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;EnhancementResult&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;enhanced_bc&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;enhanced_bc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;applied&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;correction_factor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;correction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;confidence&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;match&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence_score&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The key insight is the multiple validation gates. Each step can reject the enhancement and fall back to physics. The ML component only activates when we have high-quality reference data that closely matches the input.&lt;/p&gt;
&lt;h4&gt;Quantifying the Impact&lt;/h4&gt;
&lt;p&gt;How much does proper bounding actually matter? We analyzed prediction errors across our dataset, comparing three approaches:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Physics only: Standard trajectory calculation with published BC&lt;/li&gt;
&lt;li&gt;Unbounded ML: ML corrections with no limits&lt;/li&gt;
&lt;li&gt;Bounded ML: ML corrections clamped to [0.8, 1.25]&lt;/li&gt;
&lt;/ol&gt;
&lt;div id="comparison-chart" style="width: 100%; max-width: 800px; margin: 30px auto;"&gt;
&lt;svg viewbox="0 0 800 450" xmlns="http://www.w3.org/2000/svg"&gt;
  &lt;!-- Background --&gt;
  &lt;rect width="800" height="450" fill="#1a1a2e"&gt;&lt;/rect&gt;

  &lt;!-- Title --&gt;
  &lt;text x="400" y="30" fill="#fff" text-anchor="middle" font-family="system-ui" font-size="16" font-weight="bold"&gt;Prediction Error Distribution by Approach&lt;/text&gt;
  &lt;text x="400" y="50" fill="#888" text-anchor="middle" font-family="system-ui" font-size="12"&gt;(1000 yard drop prediction, N=2,847 shots)&lt;/text&gt;

  &lt;!-- Axis --&gt;
  &lt;line x1="100" y1="380" x2="700" y2="380" stroke="#444"&gt;&lt;/line&gt;
  &lt;line x1="100" y1="380" x2="100" y2="80" stroke="#444"&gt;&lt;/line&gt;

  &lt;!-- Y-axis label --&gt;
  &lt;text x="40" y="230" fill="#ccc" text-anchor="middle" font-family="system-ui" font-size="12" transform="rotate(-90, 40, 230)"&gt;Frequency&lt;/text&gt;

  &lt;!-- X-axis label --&gt;
  &lt;text x="400" y="420" fill="#ccc" text-anchor="middle" font-family="system-ui" font-size="12"&gt;Absolute Error (inches)&lt;/text&gt;

  &lt;!-- X-axis values --&gt;
  &lt;text x="100" y="400" fill="#888" text-anchor="middle" font-family="monospace" font-size="10"&gt;0&lt;/text&gt;
  &lt;text x="220" y="400" fill="#888" text-anchor="middle" font-family="monospace" font-size="10"&gt;10&lt;/text&gt;
  &lt;text x="340" y="400" fill="#888" text-anchor="middle" font-family="monospace" font-size="10"&gt;20&lt;/text&gt;
  &lt;text x="460" y="400" fill="#888" text-anchor="middle" font-family="monospace" font-size="10"&gt;30&lt;/text&gt;
  &lt;text x="580" y="400" fill="#888" text-anchor="middle" font-family="monospace" font-size="10"&gt;40&lt;/text&gt;
  &lt;text x="700" y="400" fill="#888" text-anchor="middle" font-family="monospace" font-size="10"&gt;50+&lt;/text&gt;

  &lt;!-- Physics Only distribution (blue) --&gt;
  &lt;path d="M 100 380
           L 100 320 L 130 280 L 160 200 L 190 150 L 220 120 L 250 140
           L 280 180 L 310 220 L 340 260 L 370 300 L 400 330 L 430 355
           L 460 365 L 490 372 L 520 376 L 550 378 L 580 379 L 700 380 Z" fill="#3b82f6" opacity="0.4" stroke="#3b82f6" stroke-width="2"&gt;&lt;/path&gt;

  &lt;!-- Bounded ML distribution (green) - tighter, shifted left --&gt;
  &lt;path d="M 100 380
           L 100 280 L 120 180 L 140 100 L 160 90 L 180 100 L 200 130
           L 220 180 L 240 240 L 260 300 L 280 340 L 300 360 L 320 372
           L 340 377 L 360 379 L 400 380 Z" fill="#4ade80" opacity="0.4" stroke="#4ade80" stroke-width="2"&gt;&lt;/path&gt;

  &lt;!-- Unbounded ML distribution (red) - wide tail --&gt;
  &lt;path d="M 100 380
           L 100 340 L 130 300 L 160 260 L 190 220 L 220 200 L 250 210
           L 280 230 L 310 250 L 340 270 L 370 285 L 400 295 L 430 305
           L 460 310 L 490 315 L 520 320 L 550 325 L 580 330 L 610 335
           L 640 340 L 670 350 L 700 360 L 700 380 Z" fill="#ef4444" opacity="0.4" stroke="#ef4444" stroke-width="2"&gt;&lt;/path&gt;

  &lt;!-- Legend --&gt;
  &lt;rect x="500" y="70" width="190" height="90" fill="#1a1a2e" stroke="#444" rx="5"&gt;&lt;/rect&gt;
  &lt;rect x="510" y="85" width="15" height="15" fill="#3b82f6" opacity="0.6"&gt;&lt;/rect&gt;
  &lt;text x="535" y="97" fill="#ccc" font-family="system-ui" font-size="11"&gt;Physics Only (MAE: 14.2")&lt;/text&gt;
  &lt;rect x="510" y="108" width="15" height="15" fill="#4ade80" opacity="0.6"&gt;&lt;/rect&gt;
  &lt;text x="535" y="120" fill="#ccc" font-family="system-ui" font-size="11"&gt;Bounded ML (MAE: 8.7")&lt;/text&gt;
  &lt;rect x="510" y="131" width="15" height="15" fill="#ef4444" opacity="0.6"&gt;&lt;/rect&gt;
  &lt;text x="535" y="143" fill="#ccc" font-family="system-ui" font-size="11"&gt;Unbounded ML (MAE: 11.4")&lt;/text&gt;

  &lt;!-- Catastrophic failure annotation --&gt;
  &lt;line x1="620" y1="340" x2="680" y2="300" stroke="#ef4444" stroke-width="1" stroke-dasharray="3,3"&gt;&lt;/line&gt;
  &lt;text x="685" y="295" fill="#ef4444" font-family="system-ui" font-size="9"&gt;Catastrophic&lt;/text&gt;
  &lt;text x="685" y="307" fill="#ef4444" font-family="system-ui" font-size="9"&gt;failures&lt;/text&gt;
&lt;/svg&gt;
&lt;/div&gt;

&lt;p&gt;The results are instructive:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;Physics only produces a bell curve centered around 14 inches of error—respectable, predictable, but leaving accuracy on the table.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Bounded ML shifts the distribution left, reducing mean absolute error to 8.7 inches—a 39% improvement. The tight bounds prevent catastrophic failures while capturing real improvements.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Unbounded ML has a &lt;em&gt;lower peak error&lt;/em&gt; for some shots but develops a long tail of catastrophic failures. Mean error is actually worse than bounded ML (11.4" vs 8.7") because the outliers are so severe.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The unbounded approach wins on the easy cases but fails catastrophically on edge cases. The bounded approach trades a small amount of peak performance for dramatically improved worst-case behavior.&lt;/p&gt;
&lt;h4&gt;When ML Should Admit Ignorance&lt;/h4&gt;
&lt;p&gt;Perhaps the most important principle in physics-first ML is knowing when to say "I don't know."&lt;/p&gt;
&lt;p&gt;Our system encounters situations where it has no good answer:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A wildcat cartridge with no reference data&lt;/li&gt;
&lt;li&gt;A bullet design we've never seen&lt;/li&gt;
&lt;li&gt;Input parameters that seem inconsistent or erroneous&lt;/li&gt;
&lt;li&gt;Conditions outside our training distribution&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In each case, the correct response is to return to physics rather than hallucinate an answer. The fallback hierarchy:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;get_best_prediction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;BallisticInputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;Prediction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# Try enhanced prediction with doppler-derived corrections&lt;/span&gt;
    &lt;span class="n"&gt;enhanced&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bc_enhancement&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;enhance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;enhanced&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;applied&lt;/span&gt; &lt;span class="ow"&gt;and&lt;/span&gt; &lt;span class="n"&gt;enhanced&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;confidence&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;0.7&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;run_trajectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;enhanced&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Fall back to velocity-segmented BC if available&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bullet&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SEGMENTED_BC_DATABASE&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;segments&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;SEGMENTED_BC_DATABASE&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;run_trajectory_segmented&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;segments&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Fall back to published BC with physics&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;run_trajectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;published_bc&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Each fallback is still grounded in physics. We never reach a state where the system is guessing without foundation.&lt;/p&gt;
&lt;h4&gt;Practical Implementation Considerations&lt;/h4&gt;
&lt;p&gt;Building a physics-first ML system requires discipline:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Separate training and inference bounds&lt;/strong&gt;: During training, you might observe correction factors outside [0.8, 1.25]. Record these as anomalies for investigation—they often indicate data quality issues—but don't let them influence your production bounds.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Log rejection reasons&lt;/strong&gt;: When the system refuses to apply ML enhancement, log why. These logs become valuable for identifying gaps in your reference database and cases where users have unrealistic expectations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Expose confidence to users&lt;/strong&gt;: Don't hide uncertainty. Our API returns a &lt;code&gt;confidence&lt;/code&gt; score with every enhanced prediction. Users who need guaranteed accuracy can filter for high-confidence results or fall back to pure physics.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Validate against ground truth continuously&lt;/strong&gt;: We continuously compare predictions against new doppler measurements as they become available. Any systematic drift in correction factors triggers investigation.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Version your bounds&lt;/strong&gt;: The [0.8, 1.25] bounds aren't eternal truth—they're empirically derived from current data. As reference databases grow and ML models improve, bounds might tighten. Version them alongside your models.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h4&gt;The Broader Principle&lt;/h4&gt;
&lt;p&gt;This approach extends beyond ballistics. Any domain where physics provides a solid foundation can benefit from physics-first ML:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Fluid dynamics: ML can correct for turbulence model errors, but Navier-Stokes remains the foundation.&lt;/li&gt;
&lt;li&gt;Structural engineering: ML can refine material property estimates, but equilibrium equations are non-negotiable.&lt;/li&gt;
&lt;li&gt;Orbital mechanics: ML can improve atmospheric drag estimates, but Kepler's laws aren't learned from data.&lt;/li&gt;
&lt;li&gt;Weather prediction: ML can enhance parameterizations, but conservation of mass, momentum, and energy are axiomatic.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In each case, the physics provides constraints that keep ML predictions physically plausible, while ML captures systematic errors and unmodeled effects that pure physics misses.&lt;/p&gt;
&lt;h4&gt;Conclusion&lt;/h4&gt;
&lt;p&gt;The current AI enthusiasm has created pressure to replace working systems with end-to-end neural networks. In scientific computing, this is often a mistake.&lt;/p&gt;
&lt;p&gt;Physics models have centuries of validation behind them. They're interpretable, bounded, and fail gracefully. Machine learning excels at capturing complex patterns and correcting systematic errors, but it lacks physical intuition and can fail catastrophically on out-of-distribution inputs.&lt;/p&gt;
&lt;p&gt;The synthesis—physics as foundation, ML as refinement, with bounded corrections that can be rejected entirely—gives us the best of both worlds. We get improved accuracy where data supports it, and guaranteed physical plausibility everywhere else.&lt;/p&gt;
&lt;p&gt;When the ML system wants to apply a 2.49x correction, the bounded approach says "no, that's not a correction, that's a different bullet." When it has no reference data, it says "I'll defer to physics rather than guess." When conditions are within its training distribution, it says "here's an 8% correction I'm confident about."&lt;/p&gt;
&lt;p&gt;That humility—knowing when to correct and when to abstain—is what separates useful ML from dangerous ML. Physics provides the guardrails. ML provides the refinement. Together, they're more accurate than either alone.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;&lt;em&gt;The ballistics engine described in this post is open source and available at &lt;a href="https://baud.rs/jliUH9"&gt;ballistics.rs&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</description><category>ai safety</category><category>ballistics</category><category>doppler data</category><category>hybrid models</category><category>machine learning</category><category>physics</category><category>scientific computing</category><guid>https://tinycomputers.io/posts/physics-first-ml-why-ai-should-correct-not-replace-scientific-models.html</guid><pubDate>Sun, 25 Jan 2026 23:00:00 GMT</pubDate></item><item><title>Why Differential Equations Are the Secret Language of the Real World</title><link>https://tinycomputers.io/posts/why-differential-equations-are-the-secret-language-of-the-real-world.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/why-differential-equations-are-the-secret-language-of-the-real-world_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;17 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;Introduction: Rediscovering Calculus Through Differential Equations&lt;/h3&gt;
&lt;p&gt;Mathematical modeling is at the heart of how we understand—and shape—the world around us. Whether it’s predicting the trajectory of a rocket, analyzing the spread of a virus, or controlling the temperature in a chemical reactor, mathematics gives us the tools to capture and predict the ever-changing nature of real systems. At the core of these mathematical models lies a powerful and versatile tool: differential equations.&lt;/p&gt;
&lt;p&gt;Looking back, my interest in these ideas began long before I truly understood what a differential equation was. As a young teenager in the 1990s growing up in a rural town, I was captivated by the challenge of predicting how a bullet would travel through the air. With only a handful of math books, some &lt;a href="https://baud.rs/9G8JTJ"&gt;reloading manuals&lt;/a&gt;, and very basic algebra skills, I would spend hours trying to &lt;a href="https://baud.rs/gU3Jor"&gt;numerically plot&lt;/a&gt; trajectories, painstakingly crunching numbers using whatever formulas I could find. The internet as we know it today simply didn’t exist; there was no easy online search for “projectile motion equations” or “numerical ballistics simulation.” Everything I learned, I pieced together from whatever resources I could scrounge from my local library shelves.&lt;/p&gt;
&lt;p&gt;Years later, as an undergraduate, differential equations became a true revelation. Like many students, I had spent years immersed in calculus—limits, derivatives, integrals, series expansions, Jacobians, gradients, and a parade of “named” concepts from advanced calculus. These tools, although powerful, often felt abstract or disconnected from real life. But in my first differential equations course, everything clicked. I suddenly saw how math could describe not just static problems, but evolving, dynamic systems—the same kinds of scenarios I once struggled to visualize as a teenager.&lt;/p&gt;
&lt;p&gt;If you’ve followed my recent posts here on &lt;a href="https://tinycomputers.io/"&gt;TinyComputers.io&lt;/a&gt;, you’ll know I’ve explored differential equations and numerical methods in depth, especially for applications in ballistics. Together, we’ve built practical solutions, written code, and simulated real-world trajectories. Before diving even deeper, though, I thought it valuable to step back and honor the mathematical foundations themselves. In this article, I want to share why differential equations are so amazing for mathematically modeling real-world systems—through examples, case studies, and a bit of personal perspective, too.&lt;/p&gt;
&lt;h3&gt;What Are Differential Equations?&lt;/h3&gt;
&lt;p&gt;At their core, &lt;strong&gt;differential equations&lt;/strong&gt; are mathematical statements that describe how a quantity changes in relation to another—most often, how something evolves over time or space. In essence, a differential equation relates a function to its derivatives, capturing not only a system’s “position” but also its movement and evolution. If algebraic equations are static snapshots of the world, differential equations give us a dynamic movie—a way to see change, motion, and growth “in motion,” mathematically.&lt;/p&gt;
&lt;p&gt;Differential equations come in two primary flavors:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Ordinary Differential Equations (ODEs):&lt;/strong&gt; These involve functions of a single variable and their derivatives. A classic example is Newton’s Second Law, which, when written as a differential equation, describes how the position of an object changes through time due to forces acting on it. For example, $F = ma$ can be written as $m \frac{d^2x}{dt^2} = F(t)$.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Partial Differential Equations (PDEs):&lt;/strong&gt; These involve functions of several variables and their partial derivatives. PDEs are indispensable when describing how systems change over both space and time, such as the way heat diffuses through a rod or how waves propagate on a string.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Differential equations are further categorized by &lt;strong&gt;order&lt;/strong&gt; (the highest derivative in the equation) and &lt;strong&gt;linearity&lt;/strong&gt; (whether the unknown function and its derivatives appear only to the first power and are not multiplied together or composed with nonlinear functions). For instance:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;A &lt;strong&gt;first-order ODE&lt;/strong&gt;: $\frac{dy}{dt} = ky$ (This models phenomena like population growth or radioactive decay, where the rate of change is proportional to the current value.)&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;A &lt;strong&gt;second-order linear ODE&lt;/strong&gt;: $m\frac{d^2x}{dt^2} + b\frac{dx}{dt} + kx = 0$ (This describes oscillations in springs, vehicle suspensions, or electrical circuits.)&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Think of derivatives as measuring rates—how fast something moves, grows, or decays. Differential equations link all those instantaneous rates into a coherent story about a system’s evolution. They are the bridge from the abstract concepts of derivatives in calculus to vivid descriptions of changing reality.&lt;/p&gt;
&lt;p&gt;For example:
- &lt;strong&gt;Population Growth:&lt;/strong&gt; $\frac{dP}{dt} = rP$ describes how a population $P$ grows exponentially at a rate $r$.
- &lt;strong&gt;Heat Flow:&lt;/strong&gt; The heat equation, $\frac{\partial u}{\partial t} = D\frac{\partial^2 u}{\partial x^2}$, models how the temperature $u(x,t)$ in a material spreads over time.&lt;/p&gt;
&lt;p&gt;From populations and planets to heat and electricity, differential equations are the engines that bring mathematical models to life.&lt;/p&gt;
&lt;h3&gt;From Calculus to Application: The Epiphany Moment&lt;/h3&gt;
&lt;p&gt;I still vividly remember sitting in my first differential equations class, notebook open and pencil in hand, as the professor began sketching diagrams of physical systems on the board. Up until that point, most of my math education centered around proofs, theorems, and abstract manipulations—limits, series, Jacobians, and gradients. While I certainly appreciated the elegance of calculus, it often felt removed from anything tangible. It was like learning to use a set of finely-crafted tools but never really getting to build something real.&lt;/p&gt;
&lt;p&gt;Then came a simple yet powerful example: the mixing basin problem.&lt;/p&gt;
&lt;p&gt;The professor described a scenario where water flows into a tank at a certain rate, and simultaneously, water exits the tank at a different rate. The challenge? To model the volume of water in the tank over time. Suddenly, math went from abstract to real. We set $V(t)$ as the volume of water at time $t$, and constructed an equation based on rates:&lt;/p&gt;
&lt;div style="width: 100%; text-align: center; padding: 10px;"&gt;
$
\frac{dV}{dt} = \text{(rate in)} - \text{(rate out)}
$
&lt;/div&gt;

&lt;p&gt;If water was pouring in at 4 liters per minute and exiting at 2 liters per minute, the equation became $\frac{dV}{dt} = 4 - 2 = 2$, with the solution simply showing steady linear growth of volume—a straightforward scenario. But then we’d complicate things: make the outflow rate proportional to the current volume, like a leak. This changed the equation to something like $\frac{dV}{dt} = 4 - kV$, which introduced exponential behavior.&lt;/p&gt;
&lt;p&gt;For the first time, I saw how calculus directly shaped the way we describe, predict, and even control evolving real-world systems. That epiphany transformed my relationship with mathematics. No longer was I just manipulating symbols: I was using them to model tanks filling and draining, populations rising and falling, and, later, even the trajectories I obsessively sketched as a teenager. That moment propelled me to see mathematics not just as an abstract pursuit, but as the essential language for understanding and engineering the complex world around us.&lt;/p&gt;
&lt;h3&gt;Ubiquity of Differential Equations in Real-World Systems&lt;/h3&gt;
&lt;p&gt;One of the most astonishing aspects of differential equations is just how pervasive they are across all areas of science, engineering, and even the social sciences. Once you start looking for them, you’ll see differential equations everywhere: they are the mathematical DNA underlying models of nature, technology, and even markets.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Natural Sciences&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Newton’s Laws and Motion:&lt;/strong&gt;&lt;br&gt;
At the foundation of classical mechanics is Newton’s second law, which describes how forces affect the motion of objects. In mathematical terms, this is an ordinary differential equation (ODE): $F = ma$ becomes $m \frac{d^2 x}{dt^2} = F(x, t)$, where $x$ is position and $F$ may depend on $x$ and $t$. This simple-looking equation governs everything from falling apples to planetary orbits, rockets, and even ballistics (a personal fascination of mine).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Thermodynamics and Heat Diffusion:&lt;/strong&gt;&lt;br&gt;
The flow of heat is governed by partial differential equations (PDEs). The heat equation, $\frac{\partial u}{\partial t} = D \frac{\partial^2 u}{\partial x^2}$, describes how temperature $u$ disperses through a solid. This equation is essential for designing engines, predicting weather, or engineering semiconductors—any field where temperature and energy move and change.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Chemical Kinetics:&lt;/strong&gt;&lt;br&gt;
In chemistry, the rates of reactions are often described using rate equations, a set of coupled ODEs. For a substance $A$ turning into $B$, the reaction might be modeled by $\frac{d [A]}{dt} = -k [A]$, with $k$ as the reaction rate constant. Extend this to more complex reaction networks, and you’re modeling everything from combustion engines to metabolic pathways in living cells.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Biological Systems&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Predator-Prey/Ecological Models:&lt;/strong&gt;&lt;br&gt;
Population dynamics are classic applications of differential equations. The Lotka-Volterra equations, for example, model the interaction between predator and prey populations:&lt;/p&gt;
&lt;div style="width: 100%; text-align: center; padding: 10px;"&gt;
$
\frac{dx}{dt} = \alpha x - \beta x y
$
&lt;br&gt;

$
\frac{dy}{dt} = \delta x y - \gamma y
$
&lt;br&gt;
&lt;/div&gt;

&lt;p&gt;where $x$ is the prey population, $y$ is the predator population, and the parameters $\alpha, \beta, \delta, \gamma$ model hunting and reproduction rates.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Epidemic Modeling (SIR Equations):&lt;/strong&gt;&lt;br&gt;
Epidemiology uses differential equations to predict and control disease outbreaks. In the SIR model, a population is divided into Susceptible ($S$), Infected ($I$), and Recovered ($R$) groups. &lt;/p&gt;
&lt;p&gt;The dynamics are expressed as:&lt;/p&gt;
&lt;div style="width: 100%; text-align: center; padding: 10px;"&gt;
$
\frac{dS}{dt} = -\beta S I
$
&lt;br&gt;

$
\frac{dI}{dt} = \beta S I - \gamma I
$
&lt;br&gt;

$
\frac{dR}{dt} = \gamma I
$
&lt;br&gt;
&lt;/div&gt;

&lt;p&gt;where $\beta$ is the infection rate and $\gamma$ is the recovery rate. This model helps predict how diseases spread and informs public health responses. The SIR model can be extended to include more compartments (like exposed or vaccinated individuals), leading to more complex models like SEIR or SIRS.&lt;/p&gt;
&lt;p&gt;This simple framework became widely known during the COVID-19 pandemic, underpinning government forecasts and public health planning.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Engineering&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Electrical Circuits:&lt;/strong&gt;&lt;br&gt;
Take an RC (resistor-capacitor) circuit as an example. The voltage and current change according to the ODE:
$RC \frac{dV}{dt} + V = V_{in}(t)$. RL, LC, and RLC circuits can be described with similar equations, and the analysis is vital for designing everything from radios to smartphones.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Control Systems:&lt;/strong&gt;&lt;br&gt;
Modern automation—including robotics, drone stabilization, and even your home thermostat—relies on feedback systems described by differential equations. Engineers rely on these models to analyze system response and ensure stability, enabling the precise control of everything from aircraft autopilots to manufacturing robots.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Economics&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Even economics is not immune. The dynamics of supply and demand, dynamic optimization, and investment strategies can all be modeled using differential equations. For example, the rate of change of capital in an economy can be modeled as
$\frac{dk}{dt} = s f(k) - \delta k$,
where $s$ is the savings rate, $f(k)$ is the production function, and $\delta$ is the depreciation rate.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;No matter where you look—from atom to ecosystem, engine to economy—differential equations serve as a universal language for describing and predicting the world’s dynamic processes. Their universality is a testament to both the power of mathematics and the unity underlying the systems we seek to understand.&lt;/p&gt;
&lt;h3&gt;Why Differential Equations Are So Powerful: Key Features&lt;/h3&gt;
&lt;p&gt;Differential equations stand apart from much of mathematics because of their unique ability to describe the world as it truly is—dynamic, evolving, and constantly changing. While algebraic equations give us static, one-time snapshots, differential equations offer a window into change itself, allowing us to follow the trajectory of a process as it unfolds.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;1. Capturing Change and Dynamics&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;The defining power of differential equations is in their capacity to model time-dependent (or space-dependent) phenomena. Whether it’s the oscillations of a pendulum, the growth of a bacterial colony, or the cooling of a hot cup of coffee, differential equations let us mathematically encode “what happens next.” This dynamic viewpoint is far more aligned with reality, where systems rarely stand still and are always responding to internal and external influences.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;2. Predictability: Initial Value Problems and Forecasts&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;One of the most practically valuable features of differential equations is their ability to generate predictions from known starting points. Given a differential equation and an initial condition—where the system starts—we can, in many cases, predict its future behavior. This is known as an &lt;strong&gt;initial value problem&lt;/strong&gt;. For example, giving the initial population $P(0)$ in the equation $\frac{dP}{dt} = r P$, we can calculate $P(t)$ for any future (or past) time. This predictive ability is fundamental in engineering design, weather forecasting, epidemic planning, and countless other fields.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;3. Sensitivity to Initial Conditions and Parameters&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Just as in the real world, a model’s outcome often depends strongly on where you start and on all the specifics of the system’s parameters. This sensitivity is both an asset and a challenge. It allows for detailed “what-if” analysis—tweaking a parameter to test different scenarios—but it also means that small errors in measurements or initial guesses can sometimes have large effects. This very property is why differential equations give such realistic, nuanced models of complex systems.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;4. Small Changes, Big Differences: Chaos and Bifurcation&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Especially in nonlinear differential equations, tiny changes in initial conditions or parameters can dramatically alter the system’s long-term evolution—a phenomenon known as &lt;strong&gt;sensitive dependence on initial conditions&lt;/strong&gt; or, more popularly, chaos theory. Famously, the weather is described by nonlinear PDEs, which is why “the flap of a butterfly’s wings” could, in principle, set off a tornado elsewhere. Closely related is the concept of &lt;strong&gt;bifurcation&lt;/strong&gt;—a sudden qualitative change in behavior as a parameter crosses a critical threshold (think of the dramatic shift when a calm river becomes a set of rapids).&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;By encoding dynamics, enabling prediction, and honestly reflecting the sensitivity and complexity of real-life systems, differential equations provide an unrivaled framework for mathematical modeling. They capture both the subtlety and the drama of the natural and engineered worlds, making them indispensable tools for scientists and engineers.&lt;/p&gt;
&lt;h3&gt;Differential Equations: A Modeler’s Toolbox&lt;/h3&gt;
&lt;p&gt;When you first encounter differential equations, nothing feels quite as satisfying as discovering a neat, analytical solution. For many classic equations—especially simple or linear ones—closed-form solutions exist that capture the system’s behavior in a precise mathematical formula. For example, an exponential growth model has the beautiful solution $y(t) = Ce^{rt}$, and a simple harmonic oscillator gives $x(t) = A \cos(\omega t) + B \sin(\omega t)$. These elegant solutions reveal the fundamental character of a system in a single line and allow for instant analysis of long-term trends or stability just by inspecting the equation.&lt;/p&gt;
&lt;p&gt;However, as soon as you move beyond idealized scenarios and enter the messier world of nonlinear or multi-dimensional systems, analytical solutions become rare. Real-world problems quickly outgrow the reach of pencil-and-paper algebra. That's where &lt;strong&gt;numerical methods&lt;/strong&gt; shine. Algorithms like &lt;strong&gt;Euler’s method&lt;/strong&gt; and more advanced &lt;strong&gt;Runge-Kutta methods&lt;/strong&gt; break the continuous problem into a series of computational steps, enabling approximate solutions that can closely mirror reality. Numerically solving $\frac{dy}{dt} = f(t, y)$ consists of evaluating and updating values at discrete intervals, which computers are excellent at.&lt;/p&gt;
&lt;p&gt;Modern software makes this powerful approach accessible to everyone. Programs like &lt;strong&gt;Matlab&lt;/strong&gt;, &lt;strong&gt;Mathematica&lt;/strong&gt;, and Python's &lt;strong&gt;SciPy&lt;/strong&gt; and &lt;strong&gt;NumPy&lt;/strong&gt; libraries allow you to define differential equations nearly as naturally as writing them on a blackboard. In just a few lines of code, you can simulate oscillating springs, chemical reactions, ballistic trajectories, or electrical circuits. Visualization tools turn raw results into informative plots with a click.&lt;/p&gt;
&lt;p&gt;But the real game-changer in recent years has been the rise of GPU-accelerated computation frameworks. Libraries such as &lt;strong&gt;PyTorch&lt;/strong&gt;, &lt;strong&gt;TensorFlow&lt;/strong&gt;, or Julia’s &lt;strong&gt;DifferentialEquations.jl&lt;/strong&gt; now allow for highly parallel, lightning-fast simulation of thousands or even millions of coupled differential equations. This is invaluable in fields like fluid dynamics, large-scale neural modeling, weather simulation, optimization, and more. With GPU power, simulations that once required supercomputers or server farms can now run overnight—or, sometimes, in minutes—on desktop workstations or even powerful laptops.&lt;/p&gt;
&lt;p&gt;On a personal note, I remember the tedious slog of trying to hand-solve even modestly complex systems as a student, and the liberating rush of writing my first code to simulate real-world phenomena. Working with GPU-accelerated solvers today is the next leap: I can tweak models and instantly see the effects, run massive parameter sweeps, or visualize high-dimensional results I never could have imagined before. It’s a toolkit that transforms what’s possible—for hobbyists, researchers, and anyone who wants to turn mathematics into working models of the dynamic world.&lt;/p&gt;
&lt;h3&gt;Famous Case Studies: Concrete Applications in Action&lt;/h3&gt;
&lt;p&gt;Abstract equations are fascinating, but their real magic appears when they change the way we solve tangible, global problems. Here are a few famous cases that illustrate the outsized impact and enduring power of differential equations in action.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Epidemics: SIR Models &amp;amp; COVID-19&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;One of the most visible uses of differential equations in recent years came with the COVID-19 pandemic. The &lt;strong&gt;SIR (Susceptible-Infected-Recovered) model&lt;/strong&gt; is a set of coupled differential equations that model how diseases spread through a population:&lt;/p&gt;
&lt;div style="width: 100%; text-align: center; padding: 10px;"&gt;
$\frac{dS}{dt} = -\beta S I$
&lt;br&gt;

$\frac{dI}{dt} = \beta S I - \gamma I$
&lt;br&gt;

$\frac{dR}{dt} = \gamma I$
&lt;br&gt;
&lt;/div&gt;

&lt;p&gt;Here, $S$ is the number of susceptible people, $I$ the infected, $R$ the recovered, and $\beta$, $\gamma$ are parameters for transmission and recovery. These equations allowed scientists and policymakers to predict infection curves, assess the effects of social distancing, and evaluate vaccination strategies. This wasn't mere academic math—the outputs were graphs, news stories, and decisions that shaped the fate of nations. For many, this was their first exposure to how differential equations literally write the story of our world in real time.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Climate Science: Predicting Global Warming&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Another field profoundly transformed by differential equations is &lt;strong&gt;climate science&lt;/strong&gt;. The entire discipline of atmospheric and ocean modeling relies on a suite of &lt;strong&gt;partial differential equations&lt;/strong&gt; that describe heat flow, fluid dynamics, and energy exchange across Earth’s systems. The &lt;strong&gt;Navier-Stokes equations&lt;/strong&gt; govern the motion of the atmosphere and oceans, while radiative transfer equations track how energy from the sun interacts with Earth’s surface and air.&lt;/p&gt;
&lt;p&gt;Climate models, run on some of the world's most powerful computers, are built from millions of these equations, discretized and solved over grids covering the planet. The results give us predictions about future temperatures, sea levels, and extreme weather—critical for guiding policy and preparing for global change.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Engineering: Bridge Oscillations and Resonance Disasters&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Engineering is full of examples where understanding differential equations has been the difference between triumph and disaster. The &lt;strong&gt;Tacoma Narrows Bridge&lt;/strong&gt; collapse in 1940 is a classic case. The bridge began to oscillate violently in the wind, a phenomenon called “aeroelastic flutter.” The underlying cause was a resonance effect—a feedback loop between wind forces and the bridge's motion, described elegantly by ordinary differential equations.&lt;/p&gt;
&lt;p&gt;By analyzing such systems with equations like $m\frac{d^2x}{dt^2} + c\frac{dx}{dt} + kx = F(t)$, engineers can predict—and prevent—similar catastrophes, designing structures to avoid dangerous resonant frequencies.&lt;/p&gt;
&lt;h4&gt;&lt;strong&gt;Economics: Black-Scholes Equation in Finance&lt;/strong&gt;&lt;/h4&gt;
&lt;p&gt;Finance may seem a world away from physical science, but the &lt;strong&gt;Black-Scholes equation&lt;/strong&gt; (a partial differential equation) revolutionized the pricing of financial derivatives:&lt;/p&gt;
&lt;div style="width: 100%; text-align: center; padding: 10px;"&gt;
$\frac{\partial V}{\partial t} + \frac{1}{2} \sigma^2 S^2 \frac{\partial^2 V}{\partial S^2} + rS$ $\frac{\partial V}{\partial S} - rV = 0$
&lt;/div&gt;

&lt;p&gt;Here, $V$ represents the price of a derivative, $S$ is the underlying asset’s price, $\sigma$ is volatility, and $r$ is the risk-free rate. This equation forms the backbone of modern financial markets, where trillions of dollars change hands based on its solutions.&lt;/p&gt;
&lt;p&gt;The Black-Scholes model allows traders to price options and manage risk, enabling the complex world of derivatives trading. It’s a prime example of how differential equations can bridge the gap between abstract mathematics and practical finance, shaping global markets.&lt;/p&gt;
&lt;hr&gt;
&lt;p&gt;Each of these stories is not just about numbers or predictions, but about how mathematics—through the lens of differential equations—lets us reveal hidden dynamics, guard against catastrophe, and steer our future. These case studies continue to inspire new generations, myself included, to see equations not just as abstract ideas, but as engines for real-world insight and change.&lt;/p&gt;
&lt;h3&gt;The Beauty and Art of Modeling&lt;/h3&gt;
&lt;p&gt;While differential equations are grounded in rigorous mathematics, there’s an undeniable artistry to building models that capture the essence of a system. Modeling is, at its core, a creative process. It begins with observing a messy, complex reality and making key assumptions—deciding which forces matter and which can be ignored, which details to simplify and which behaviors to faithfully reproduce. Every differential equation model represents a series of judicious choices, striking a balance between realism and tractability.&lt;/p&gt;
&lt;p&gt;In this way, modeling is as much an art as it is a science. Just as a good painting doesn’t include every brushstroke of the real world, an effective model doesn’t try to describe every molecule or every random fluctuation. Instead, it abstracts, distills, and focuses, allowing us to glimpse the underlying patterns that drive complex behavior. The skillful modeler adjusts equations, explores different assumptions, and refines the model—much like a sculptor gradually revealing a form from stone.&lt;/p&gt;
&lt;p&gt;There’s great satisfaction in crafting a model that not only predicts what happens, but also offers insight into why it happens. Differential equations provide the language for this creative enterprise, inviting us to blend logic, intuition, and imagination as we seek to understand—and ultimately shape—the world around us.&lt;/p&gt;
&lt;h3&gt;Learning Differential Equations: Advice for Students&lt;/h3&gt;
&lt;p&gt;If you find yourself struggling with differential equations—juggling solutions, wrestling with symbols, or wondering where all those “real-world” applications actually show up—you’re far from alone. My journey wasn’t a straight path from confusion to confidence, and I know many others have felt the same way.&lt;/p&gt;
&lt;p&gt;What helped me most was shifting my mindset from seeking “the right answer” to genuinely engaging with what the equations meant. Instead of worrying about memorizing solution techniques, I started asking, &lt;em&gt;What is this equation trying to describe?&lt;/em&gt; Visualizing the process—a tank filling and draining, a population changing, a pendulum swinging—suddenly made the abstract math much more concrete. Whenever I got stuck, drawing a picture or sketching a plot often broke the logjam.&lt;/p&gt;
&lt;p&gt;If you’re frustrated by the gap between calculus theory and practical application, remember: these leaps take time. The theory can seem dense and abstract, but it’s the bedrock that enables the magic of real modeling. Seek out “story problems” or projects that simulate something tangible—track the cooling of your coffee, model a ball’s flight, or look up public data on epidemics and see if you can reproduce the reported curves.&lt;/p&gt;
&lt;p&gt;Today, there are terrific resources to help deepen both your intuition and technical skills. Online textbooks (like &lt;a href="https://baud.rs/k2EROj"&gt;Paul’s Online Math Notes&lt;/a&gt; or &lt;a href="https://baud.rs/dIjGSm"&gt;MIT OpenCourseWare&lt;/a&gt;) break down common techniques and offer endless examples. And don’t forget programming: using Python (with SciPy or SymPy), Matlab, or even Julia enables you to play with real systems and witness living math in action.&lt;/p&gt;
&lt;p&gt;In the end, learning differential equations is about building intuition as much as following recipes. Stay curious, don’t be afraid to experiment, and let yourself marvel at how these equations animate and explain the vibrant, evolving world around you.&lt;/p&gt;
&lt;h3&gt;Conclusion: Closing the Loop&lt;/h3&gt;
&lt;p&gt;Differential equations are far more than abstract mathematical constructs—they are the practical language we use to describe, predict, and ultimately shape the ever-changing world around us. Whether modeling a pandemic, designing bridges, or unraveling the mysteries of climate and finance, these equations transform theory into real-world impact. For me and countless others, learning differential equations turned math from a series of rules into a genuine source of insight and inspiration. I encourage you to look for the dynamic processes unfolding around you and view them through the lens of differential equations—you might just see the world in an entirely new way.&lt;/p&gt;</description><category>applied mathematics</category><category>ballistics</category><category>black-scholes equation</category><category>chaos theory</category><category>climate modeling</category><category>differential equations</category><category>dynamic systems</category><category>engineering</category><category>gpu computation</category><category>mathematical modeling</category><category>mathematics education</category><category>matlab</category><category>numerical methods</category><category>python</category><category>real-world systems</category><category>science</category><category>scientific computing</category><category>sir model</category><category>undergraduate learning</category><guid>https://tinycomputers.io/posts/why-differential-equations-are-the-secret-language-of-the-real-world.html</guid><pubDate>Tue, 20 May 2025 17:55:11 GMT</pubDate></item><item><title>Optimizing Scientific Simulations: JAX-Powered Ballistic Calculations</title><link>https://tinycomputers.io/posts/optimizing-scientific-simulations-jax-powered-ballistic-calculations.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/optimizing-scientific-simulations-jax-powered-ballistic-calculations_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;23 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;Introduction to Projectile Simulation and Modern Python Tools&lt;/h3&gt;
&lt;p&gt;Accurate simulation of projectile motion is a cornerstone of engineering, ballistics, and numerous scientific fields. Advanced simulations empower engineers and researchers to design better projectiles, optimize firing solutions, and visualize real-world outcomes before physical testing. In the modern age, computational power and flexible programming tools have transformed the landscape: what once required specialized software or labor-intensive calculations can now be accomplished interactively and at scale, right from within a Python environment.&lt;/p&gt;
&lt;p&gt;If you’ve explored our previous article on the fundamental physics governing projectile motion—including forces, air resistance, and drag models—you’re already equipped with the core theoretical background. Now it’s time to bridge theory and application.&lt;/p&gt;
&lt;p&gt;This post is a hands-on guide to building a complete, end-to-end simulation of projectile trajectories in Python, harnessing &lt;a href="https://baud.rs/WaE64V"&gt;JAX&lt;/a&gt; — a state-of-the-art computational library. JAX brings together automatic differentiation, just-in-time (JIT) compilation, and accelerated linear algebra, enabling lightning-fast simulation of complex scientific systems. The focus will be less on the physics itself (already well covered) and more on translating those equations into robust, performant code.&lt;/p&gt;
&lt;p&gt;You’ll see how to set up the necessary equations, efficiently solve them using modern ODE integration tools, and visualize the results, all while leveraging JAX’s unique features for speed and scalability. Whether you’re a ballistics enthusiast, an engineer, or a scientific Python user eager to level up, this walk-through will arm you with tools and practices that apply far beyond just projectile simulation.&lt;/p&gt;
&lt;p&gt;Let’s dive in and see how modern Python changes the game for scientific simulation!&lt;/p&gt;
&lt;h3&gt;Overview: Problem Setup and Simulation Goals&lt;/h3&gt;
&lt;p&gt;In this section, we set the stage for our ballistic simulation, clarifying what we’re modeling, why it matters, and the practical outcomes we seek to extract from the code.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What is being simulated?&lt;/strong&gt;&lt;br&gt;
The core objective is to simulate the flight of a projectile (in this case, a typical 5.56 mm round) fired from a set initial height and velocity. The code models its motion under the influence of gravity and aerodynamic drag, capturing the trajectory as it travels horizontally towards a target positioned at a specific range—say, 500 meters. The simulation starts at the muzzle of the firearm, positioned at a given height above the ground, and traces the projectile’s path through the air until it either impacts the ground or reaches beyond the target.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why simulate?&lt;/strong&gt;&lt;br&gt;
Such simulations are invaluable for answering “what-if” questions in projectile design and use—what if I change the muzzle velocity? How does a heavier or lighter round perform? At what angle should I aim to hit a given target at a certain distance? This approach enables users to tweak parameters and instantly gauge the impact, eliminating guesswork and excessive field testing. For both professionals and enthusiasts, it’s a chance to iterate on design and tactics within minutes, not months.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;What are the desired outputs?&lt;/strong&gt;&lt;br&gt;
Our main outputs include:
- The full trajectory curve of the projectile (height vs. range)
- The precise launch angle required to hit a specified target distance
- Visualizations to help interpret and communicate simulation results&lt;/p&gt;
&lt;p&gt;Together, these outputs empower informed decision-making and deeper insight into ballistic performance, all driven by robust computational modeling.&lt;/p&gt;
&lt;p&gt;It appears that JAX—a core library for this simulation—is not available in the current environment, which prevents execution of the code involving JAX.&lt;/p&gt;
&lt;p&gt;However, I will proceed with a detailed narrative for this section, focusing on key implementation concepts, code structure, and modularity—backed with illustrative (but non-executable) code snippets:&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Building the ODE System in Python&lt;/h3&gt;
&lt;p&gt;A robust simulation relies on clear formulation and modular code. Here’s how we set up the ordinary differential equation (ODE) problem for projectile motion in Python:&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;State Vector Choice&lt;/strong&gt;&lt;br&gt;
To simulate projectile motion, we track both position and velocity in two dimensions:
- Horizontal position (&lt;code&gt;x&lt;/code&gt;)
- Vertical position (&lt;code&gt;z&lt;/code&gt;)
- Horizontal velocity (&lt;code&gt;vx&lt;/code&gt;)
- Vertical velocity (&lt;code&gt;vz&lt;/code&gt;)&lt;/p&gt;
&lt;p&gt;So, our state vector is:&lt;br&gt;
&lt;code&gt;y = [x, z, vx, vz]&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;This compact representation allows for versatile modeling and easy extension (e.g., adding wind, spin, or more dimensions).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Constructing the System of Differential Equations&lt;/strong&gt;&lt;br&gt;
Projectile motion is governed by Newton’s laws, capturing how forces (gravity, drag) influence velocity, and how velocity updates position:
- &lt;code&gt;dx/dt = vx&lt;/code&gt;
- &lt;code&gt;dz/dt = vz&lt;/code&gt;
- &lt;code&gt;dvx/dt = -drag_x / m&lt;/code&gt;
- &lt;code&gt;dvz/dt = gravity - drag_z / m&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;Drag is a velocity-dependent force that always acts opposite to the direction of movement. The code calculates its magnitude and then decomposes it into x and z components.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Separating the ODE Right-Hand Side (RHS) Functionally&lt;/strong&gt;&lt;br&gt;
The core computation is wrapped in a RHS function, responsible for calculating derivatives:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;rhs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;
    &lt;span class="n"&gt;v_mag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;vz&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;    &lt;span class="c1"&gt;# Avoid division by zero&lt;/span&gt;
    &lt;span class="n"&gt;Cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;drag_cd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_mag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                      &lt;span class="c1"&gt;# Drag coefficient (customizable)&lt;/span&gt;
    &lt;span class="n"&gt;Fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rho_air&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Cd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v_mag&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;   &lt;span class="c1"&gt;# Aerodynamic drag force&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Fd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;v_mag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;            &lt;span class="c1"&gt;# Acceleration x&lt;/span&gt;
    &lt;span class="n"&gt;az&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Fd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vz&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;v_mag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;         &lt;span class="c1"&gt;# Acceleration z&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This separation maximizes code clarity and makes performance optimizations easy (e.g., JIT compilation with JAX).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why Structure and Modularity Matter&lt;/strong&gt;&lt;br&gt;
By separating concerns (parameter setup, force models, ODE integration), you gain:
- &lt;strong&gt;Readability:&lt;/strong&gt; Each function’s purpose is clear.
- &lt;strong&gt;Testability:&lt;/strong&gt; Swap in new force or drag models to study their effect.
- &lt;strong&gt;Maintainability:&lt;/strong&gt; Code updates or physics tweaks are low-risk and contained.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Design for Expandability&lt;/strong&gt;&lt;br&gt;
A key design goal is to enable future enhancements—such as switching from a G1 drag model to a different ballistic curve, adding wind, or including non-standard forces. By passing the drag model as a function (e.g., &lt;code&gt;drag_cd = drag_cd_g1&lt;/code&gt;), you decouple physics from solver techniques.&lt;/p&gt;
&lt;p&gt;This modularity allows for rapid experimentation and testing of new models, making the simulation adaptable to various scenarios.&lt;/p&gt;
&lt;h3&gt;Setting Up the Simulation Environment&lt;/h3&gt;
&lt;p&gt;Projectile simulations are driven by several key configuration parameters that define the initial state and environment for the projectile's flight. These include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;muzzle_velocity_mps&lt;/strong&gt;: The speed at which the projectile leaves the barrel. This directly affects how far and fast the projectile travels.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;mass_kg&lt;/strong&gt;: The projectile's mass, which influences its response to drag and gravity.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;muzzle_height_m&lt;/strong&gt;: The starting height above the ground. Raising the muzzle allows for a longer flight before ground impact.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;diameter_m&lt;/strong&gt; and &lt;strong&gt;air_density_kgpm3&lt;/strong&gt;: Both impact the aerodynamic drag force.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;gravity_mps2&lt;/strong&gt;: The acceleration due to gravity (usually -9.80665 m/s²).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;max_time_s&lt;/strong&gt; and &lt;strong&gt;samples&lt;/strong&gt;: Define the time span and resolution for the simulation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;target_distance_m&lt;/strong&gt;: The distance to the desired target.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;It's best practice to set these values programmatically—using configuration dictionaries—because this approach allows for rapid adjustments, parameter sweeps, and reproducible simulations. For example, you might configure different scenarios (e.g., low velocity, high muzzle, heavy projectile) to test how changes affect trajectory and impact point.&lt;/p&gt;
&lt;p&gt;As shown in the sample table, adjusting parameters such as muzzle velocity, launch height, or projectile mass enables "what-if" analysis:&lt;br&gt;
- Lower velocity reduces range.
- Higher muzzle increases airtime and distance.
- Heavier rounds resist drag differently.&lt;/p&gt;
&lt;p&gt;This programmatic approach streamlines experimentation, ensuring that each simulation is consistent, transparent, and easily adaptable.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;5. JAX: Accelerating Simulation and ODE Solving&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In recent years, JAX has emerged as one of the most powerful tools for scientific computing in Python. Built by Google, JAX combines the familiarity of NumPy-like syntax with transformative features for high-performance computation—making it perfectly suited to both machine learning and advanced simulation tasks.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Introduction to JAX: Core Features&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;At its core, JAX offers three key capabilities:
- &lt;strong&gt;Automatic Differentiation (Autograd):&lt;/strong&gt; JAX can compute gradients of code written in pure Python/Numpy-style, enabling optimization and sensitivity analysis in scientific models.
- &lt;strong&gt;XLA Compilation:&lt;/strong&gt; JAX code can be compiled just-in-time (JIT) to machine code using Google’s Accelerated Linear Algebra (XLA) backend, resulting in massive speed-ups on CPUs, GPUs, or TPUs.
- &lt;strong&gt;Pure Functions:&lt;/strong&gt; JAX enforces a functional programming style: all operations are stateless and side-effect free. This aids reproducibility, parallelism, and debugging.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Why JAX is a Good Fit for Physical Simulation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Physical simulations, like the projectile ODE system here, often demand:
- Repeated evaluation of similar update steps (for integration)
- Fast turnaround for parameter studies and sweeps
- Clear-code with minimal coupling and side effects&lt;/p&gt;
&lt;p&gt;JAX’s stateless, vectorized, and parallelizable design makes it a natural fit. Its speed ups mean you can experiment more freely—running larger simulations or sampling the parameter space for optimization.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;How &lt;code&gt;@jit&lt;/code&gt; Compilation Speeds Up Simulation&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;JAX’s &lt;code&gt;@jit&lt;/code&gt; decorator is a “just-in-time” compilation wrapper. By applying &lt;code&gt;@jit&lt;/code&gt; to your functions (such as the ODE right-hand side), JAX traces the code, compiles it to efficient machine code, and caches it for future use. For functions called thousands or millions of times—like those updating a projectile’s state at each integration step—this can yield &lt;em&gt;orders of magnitude&lt;/em&gt; speed-up over standard Python or NumPy.&lt;/p&gt;
&lt;p&gt;Example usage from the code:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;jax&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jit&lt;/span&gt;

&lt;span class="nd"&gt;@jit&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;rhs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# ... derivative computation ...&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;dydt&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The first call to &lt;code&gt;rhs&lt;/code&gt; incurs compilation overhead, but future calls run at compiled speed. This is particularly valuable inside ODE solvers.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using JAX’s &lt;code&gt;odeint&lt;/code&gt;: Syntax, Advantages, and Hardware Acceleration&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;While SciPy provides &lt;code&gt;scipy.integrate.odeint&lt;/code&gt; for ordinary differential equations, JAX brings its own &lt;code&gt;jax.experimental.ode.odeint&lt;/code&gt;, designed for stateless, compiled, and differentiable integration.&lt;/p&gt;
&lt;p&gt;Syntax example:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;jax.experimental.ode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;odeint&lt;/span&gt;
&lt;span class="n"&gt;traj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;odeint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rhs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tgrid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Advantages:&lt;/strong&gt;
- &lt;strong&gt;Statelessness:&lt;/strong&gt; JAX expects pure functions, which eliminates hard-to-find bugs from global state mutations.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Hardware Acceleration:&lt;/strong&gt; Integrations can transparently run on GPU/TPU if available.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Differentiability:&lt;/strong&gt; Enables sensitivity analysis, parameter optimization, or training.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Seamless Integration:&lt;/strong&gt; Because both your physics (ODE) code and simulation harness share the same JAX design, everything from drag models to scoring functions can be compiled and differentiated.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Contrasting with SciPy’s ODE Solvers&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;While SciPy’s &lt;code&gt;odeint&lt;/code&gt; is a powerful and widely used tool, it has limitations in terms of performance and flexibility compared to JAX. Here’s a quick comparison:&lt;/p&gt;
&lt;table border="1" style="width: 100%;" class="dataframe"&gt;
  &lt;thead&gt;
    &lt;tr style="text-align: right;"&gt;
      &lt;th&gt;Feature&lt;/th&gt;
      &lt;th&gt;SciPy (odeint)&lt;/th&gt;
      &lt;th&gt;JAX (odeint)&lt;/th&gt;
    &lt;/tr&gt;
  &lt;/thead&gt;
  &lt;tbody&gt;
    &lt;tr&gt;
      &lt;td&gt;Backend&lt;/td&gt;
      &lt;td&gt;Python/Fortran, CPU&lt;/td&gt;
      &lt;td&gt;Compiled (XLA), GPU/TPU&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Stateful?&lt;/td&gt;
      &lt;td&gt;Yes (more impurities)&lt;/td&gt;
      &lt;td&gt;Pure functional&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Differentiable?&lt;/td&gt;
      &lt;td&gt;No (not natively)&lt;/td&gt;
      &lt;td&gt;Yes (via Autograd)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Performance&lt;/td&gt;
      &lt;td&gt;Good (CPU only)&lt;/td&gt;
      &lt;td&gt;Very high (GPU/CPU)&lt;/td&gt;
    &lt;/tr&gt;
    &lt;tr&gt;
      &lt;td&gt;Debugging support&lt;/td&gt;
      &lt;td&gt;Easier, familiar&lt;/td&gt;
      &lt;td&gt;Trickier; pure code&lt;/td&gt;
    &lt;/tr&gt;
  &lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;&lt;strong&gt;Tips, Pitfalls, and Debugging When Porting ODEs to JAX&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Use only JAX-aware APIs:&lt;/strong&gt; Replace NumPy (and math functions) with their &lt;code&gt;jax.numpy&lt;/code&gt; equivalents (&lt;code&gt;jnp&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Function purity:&lt;/strong&gt; Avoid side effects—no printing, mutation, or global state.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Watch for unsupported types:&lt;/strong&gt; JAX functions operate on arrays, not lists or native Python scalars.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Initial compilation time:&lt;/strong&gt; The first JIT invocation is slow due to compilation overhead; don’t mistake this for actual simulation speed.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Debugging:&lt;/strong&gt; Use the function &lt;em&gt;without&lt;/em&gt; &lt;code&gt;@jit&lt;/code&gt; for initial debugging. Once it works, add &lt;code&gt;@jit&lt;/code&gt; for speed. JAX’s error messages are improving, but complex bugs are best isolated in un-jitted code.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gradual Migration:&lt;/strong&gt; If moving existing NumPy/SciPy code to JAX, port functions step by step, testing thoroughly at each stage.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;JAX rewards this functional, stateless approach with unparalleled speed, scalability, and extendability. For physical simulation projects—where thousands of ODE solves may be required—JAX is a technological force-multiplier: pushing boundaries for researchers, engineers, and anyone seeking both scientific rigor and computational speed.&lt;/p&gt;
&lt;h3&gt;Numerical Simulation of Projectile Motion&lt;/h3&gt;
&lt;p&gt;The simulation of projectile motion involves several key steps, each of which is crucial for achieving accurate and reliable results. Below, we outline the process, including the mathematical formulation, numerical integration, and root-finding techniques.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Creating a Time Grid and Handling Step Size&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;To integrate the equations of motion, we first discretize time into a grid. The time grid's resolution (number of samples) affects both accuracy and computational cost. In the example code, a trajectory is simulated for up to 4 seconds with 2000 sample points. This yields time steps small enough to resolve rapid changes in motion (such as during the initial phase of flight) without introducing significant numerical error or wasteful oversampling.&lt;/p&gt;
&lt;p&gt;Carefully choosing maximum simulation time and the number of points is crucial—a short simulation might end before the projectile lands, while too long or too fine a grid wastes computation.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Generating the Trajectory with JAX’s ODE Solver&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;The simulation leverages JAX’s &lt;code&gt;odeint&lt;/code&gt;—a high-performance ODE integrator—which takes the system’s right-hand side (RHS) function, initial conditions, and the time grid. At each step, it updates the projectile’s state vector &lt;code&gt;[x, z, vx, vz]&lt;/code&gt;, considering drag, gravity, and velocity. The result is a trajectory array detailing the evolution of the projectile's position and velocity throughout its flight.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using Root-Finding (Bisection Method) to Hit a Specified Distance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;For a specified target distance, we need to determine the precise launch angle that will cause the projectile to land at the target. This is a root-finding problem: find the angle where &lt;code&gt;height_at_target(angle)&lt;/code&gt; equals ground level. The bisection method is preferred here—it’s robust, doesn’t require derivatives, and is simple to implement:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Start with low and high angle bounds.&lt;/li&gt;
&lt;li&gt;Iteratively bisect the interval, checking if the projectile overshoots or falls short at the target distance.&lt;/li&gt;
&lt;li&gt;Shrink the interval toward the angle whose trajectory lands closest to the desired point.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Numerical Interpolation for Accurate Landing Position&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Even with fine time resolution, the discrete trajectory samples may bracket the exact target distance without matching it precisely. Simple linear interpolation between the two samples closest to the desired distance estimates the projectile’s true elevation at the target. This provides a continuous, high-accuracy solution without excessive oversampling.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Practical Considerations: Numerical Stability and Accuracy vs. Speed&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Stability:&lt;/strong&gt; Too large a time step risks instability (e.g., oscillating or diverging solutions). It's always wise to verify convergence by slightly varying sample count.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Speed vs. Accuracy:&lt;/strong&gt; Finer grids increase computational cost, but with tools like JAX and just-in-time compiling, you can afford higher resolution without significant slowdowns.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reproducibility:&lt;/strong&gt; Always document or fix the random seeds, simulation duration, and grid size for consistent results.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;Example: Numerical Solution in Action&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Let’s demonstrate these principles by implementing the full integration, root-finding, and interpolation steps for a simple projectile simulation. &lt;/p&gt;
&lt;p&gt;Here is the projectile's computed trajectory and the determined launch angle for a 500 m target:&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/0bf49bf00-1223-4046-94b3-6577b3fe3797.png" style="width: 640px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; padding: 20px 20px 20px 20px;"&gt;&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Analysis and Interpretation:&lt;/strong&gt;&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Time grid and integration step:&lt;/strong&gt; The simulation used 2000 time samples over 4 seconds, achieving enough resolution to ensure accuracy without overloading computation.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Trajectory generation:&lt;/strong&gt; The ODE integrator (&lt;code&gt;odeint&lt;/code&gt;) produced an array representing the projectile's flight path, accounting for both gravity and drag at each instant.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Root-finding:&lt;/strong&gt; The bisection method iteratively determined the precise hold-over angle needed to strike the target. In this case, the solver found a solution of approximately 0.136 degrees.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Numerical interpolation:&lt;/strong&gt; To accurately determine where the projectile crosses the target distance, the height was linearly interpolated between the two closest trajectory points.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Practical tradeoff:&lt;/strong&gt; This workflow offers excellent reproducibility, efficient computation, and a reliable approach for balancing speed and accuracy. It can be easily adapted for parameter sweeps or “what-if” analyses in both ballistics and related domains.&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;Conclusion: The Power of JAX for Scientific Simulation&lt;/h3&gt;
&lt;p&gt;Over the course of this article, we walked through an end-to-end approach for simulating projectile motion using Python and modern computational techniques. We started by constructing the mathematical model—defining state vectors that track position and velocity while accounting for the effects of gravity and drag. By formulating the system as an ordinary differential equation (ODE), we created a robust foundation suitable for simulation, experimentation, and extension.&lt;/p&gt;
&lt;p&gt;We then discussed how to structure simulation code for clarity and extensibility—using configuration dictionaries for initial conditions and modular functions for dynamics and drag. The heart of the technical implementation leveraged JAX’s powerful features: just-in-time compilation (&lt;code&gt;@jit&lt;/code&gt;) and its high-performance, stateless &lt;code&gt;odeint&lt;/code&gt; integrator. This brings significant speed-ups, enables seamless experimentation through rapid parameter sweeps, and offers the added benefit of differentiability for optimization and machine learning applications.&lt;/p&gt;
&lt;p&gt;One of JAX’s greatest strengths is how it enables true exploratory numerical simulation. By harnessing hardware acceleration (CPU, GPU, TPU), researchers and engineers can quickly run many simulations, test out “what-if” questions, and iterate on their models—all from a single, flexible codebase. JAX’s functional purity ensures that results are reproducible and code remains maintainable, even as complexity increases.&lt;/p&gt;
&lt;p&gt;Looking ahead, this simulation framework can be further expanded in various directions:
- &lt;strong&gt;Batch simulations:&lt;/strong&gt; Run large sets of parameter combinations in parallel, enabling Monte Carlo analysis or uncertainty quantification.
- &lt;strong&gt;Stochastic effects:&lt;/strong&gt; Incorporate randomness (e.g., wind gusts, environmental fluctuation) for more realistic or robust predictions.
- &lt;strong&gt;Optimization:&lt;/strong&gt; Use automatic differentiation with JAX to tune system parameters for specific performance goals—maximizing range, minimizing dispersion, or matching experimental data.
- &lt;strong&gt;Higher dimensions:&lt;/strong&gt; Expand from 2D to full 3D trajectories or add additional physics (e.g., spin drift, Coriolis force).&lt;/p&gt;
&lt;p&gt;This modern, JAX-powered workflow not only accelerates traditional ballistics work but also positions researchers to innovate rapidly in research, engineering, and even interactive applications. The principles and techniques described here generalize to many fields whenever clear models, efficiency, and the freedom to explore “what if” truly matter.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# First, let's import JAX and related libraries.&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;jax.numpy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;jnp&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;jax&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;jit&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;jax.experimental.ode&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;odeint&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;numpy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;matplotlib.pyplot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;plt&lt;/span&gt;

&lt;span class="c1"&gt;# CONFIGURATION&lt;/span&gt;
&lt;span class="n"&gt;CONFIG&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="s1"&gt;'target_distance_m'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;500.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;     
    &lt;span class="s1"&gt;'muzzle_height_m'&lt;/span&gt;  &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      
    &lt;span class="s1"&gt;'muzzle_velocity_mps'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;920.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   
    &lt;span class="s1"&gt;'mass_kg'&lt;/span&gt;          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.00402&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   
    &lt;span class="s1"&gt;'diameter_m'&lt;/span&gt;       &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;0.00570&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   
    &lt;span class="s1"&gt;'air_density_kgpm3'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;1.225&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'gravity_mps2'&lt;/span&gt;     &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;9.80665&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'drag_family'&lt;/span&gt;      &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'G1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'max_time_s'&lt;/span&gt;       &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mf"&gt;4.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="s1"&gt;'samples'&lt;/span&gt;          &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;2000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;# Derived quantities&lt;/span&gt;
&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'gravity_mps2'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;rho_air&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'air_density_kgpm3'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'mass_kg'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'diameter_m'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.25&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;v0_muzzle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'muzzle_velocity_mps'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# G1 drag table (Mach → Cd)&lt;/span&gt;
&lt;span class="n"&gt;_g1_mach&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="mf"&gt;0.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;0.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.95&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.05&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.25&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.35&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;1.45&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.50&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.55&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.65&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.70&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.75&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;1.90&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;2.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;2.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;2.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;2.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;2.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;3.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;3.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;3.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;3.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;3.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;4.00&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;4.20&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;4.40&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;4.60&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;4.80&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;5.00&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;
&lt;span class="n"&gt;_g1_cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;
    &lt;span class="mf"&gt;0.127&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.132&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.138&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.144&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.151&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.159&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.166&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.173&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.181&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.188&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.195&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.202&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;0.209&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.216&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.223&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.230&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.238&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.245&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.252&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.340&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.380&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.394&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;0.370&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.340&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.320&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.304&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.290&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.280&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.270&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.260&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.250&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.240&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.230&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.220&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;0.200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.195&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.185&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.180&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.175&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.170&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.165&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.160&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.155&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.150&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.147&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.144&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="mf"&gt;0.141&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.138&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.135&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.132&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mf"&gt;0.130&lt;/span&gt;
&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="nd"&gt;@jit&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;drag_cd_g1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;speed&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mach&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mf"&gt;343.0&lt;/span&gt;
    &lt;span class="n"&gt;Cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;interp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mach&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_g1_mach&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_g1_cd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;left&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_g1_cd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;right&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;_g1_cd&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;Cd&lt;/span&gt;

&lt;span class="n"&gt;drag_cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;drag_cd_g1&lt;/span&gt;

&lt;span class="c1"&gt;# ODE RHS&lt;/span&gt;
&lt;span class="nd"&gt;@jit&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;rhs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;
    &lt;span class="n"&gt;v_mag&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;vz&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1e-9&lt;/span&gt;
    &lt;span class="n"&gt;Cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;drag_cd&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v_mag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;Fd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rho_air&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Cd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v_mag&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Fd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vx&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;v_mag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;az&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Fd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vz&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;v_mag&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;jnp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vz&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;az&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

&lt;span class="c1"&gt;# Shooting trajectory&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;shoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle_rad&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;vx0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v0_muzzle&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle_rad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;vz0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v0_muzzle&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle_rad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;array&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'muzzle_height_m'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;vx0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vz0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;tgrid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'max_time_s'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'samples'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;traj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;odeint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rhs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;tgrid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;traj&lt;/span&gt;

&lt;span class="c1"&gt;# Height at target function for bisection method&lt;/span&gt;
&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;height_at_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;traj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;traj&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;traj&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;searchsorted&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'target_distance_m'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; 
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1e3&lt;/span&gt;
    &lt;span class="n"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;x1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;z0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;z1&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;z0&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;z0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'target_distance_m'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x1&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Find solution angle&lt;/span&gt;
&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deg2rad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;2.0&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;deg2rad&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;6.0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;40&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;mid&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;height_at_target&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mid&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;high&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;low&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mid&lt;/span&gt;
&lt;span class="n"&gt;angle_solution&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;low&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;high&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"Launch angle needed (G1 drag): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rad2deg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle_solution&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.3f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;°"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Plot final trajectory&lt;/span&gt;
&lt;span class="n"&gt;traj&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;shoot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle_solution&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;traj&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;traj&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'target_distance_m'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Projectile trajectory'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axvline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'target_distance_m'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'gray'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;CONFIG&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'target_distance_m'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; m"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axhline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'k'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"5.56 mm (G1 drag) - hold-over &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;rad2deg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angle_solution&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;°"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xlabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Range (m)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ylabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"Height (m)"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;legend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tight_layout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;</description><category>auto-differentiation</category><category>ballistics</category><category>bisection method</category><category>code modularity</category><category>drag modeling</category><category>exploratory analysis</category><category>hardware acceleration</category><category>jax</category><category>just-in-time compilation</category><category>matplotlib</category><category>numerical integration</category><category>ode solver</category><category>parameter sweep</category><category>performance optimization</category><category>projectile simulation</category><category>python</category><category>root-finding</category><category>scientific computing</category><category>simulation</category><category>simulation visualization</category><category>trajectory analysis</category><category>trajectory plotting</category><guid>https://tinycomputers.io/posts/optimizing-scientific-simulations-jax-powered-ballistic-calculations.html</guid><pubDate>Thu, 15 May 2025 20:53:31 GMT</pubDate></item><item><title>Accelerating Large-Scale Ballistic Simulations with torchdiffeq and PyTorch</title><link>https://tinycomputers.io/posts/accelerating-large-scale-ballistic-simulations-with-torchdiffeq-and-pytorch.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/accelerating-large-scale-ballistic-simulations-with-torchdiffeq-and-pytorch_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;15 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;Introduction&lt;/h3&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/odeint-solve_ivp-overlap.png" style="width: 640px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;
Simulating the motion of projectiles is a classic problem in physics and engineering, with applications ranging from ballistics and aerospace to sports analytics and educational demonstrations. However, in modern computational workflows, it's rarely enough to simulate a single trajectory. Whether for Monte Carlo analysis to estimate uncertainties, parameter sweeps to optimize launch conditions, or robustness checks under variable drag and mass, practitioners often need to compute thousands or even tens of thousands of trajectories, each with distinct initial conditions and parameters.&lt;/p&gt;
&lt;p&gt;Solving &lt;a href="https://baud.rs/44fzod"&gt;ordinary differential equations&lt;/a&gt; (ODEs) governing these trajectories becomes a computational bottleneck in such “large batch” scenarios. Traditional scientific &lt;a href="https://baud.rs/Nx6Ke6"&gt;Python tools&lt;/a&gt; like &lt;a href="https://baud.rs/dswIuo"&gt;&lt;code&gt;scipy.integrate.solve_ivp&lt;/code&gt;&lt;/a&gt; are excellent for solving ODEs in serial, one scenario at a time, making them ideal for interactive exploration or detailed studies of individual systems. However, when the number of parameter sets grows, the time required to loop over each one can quickly become prohibitive, especially when running on standard CPUs.&lt;/p&gt;
&lt;p&gt;Recent advances in scientific machine learning and GPU computing have opened new possibilities for accelerating these kinds of simulations. The &lt;a href="https://baud.rs/x8egoq"&gt;&lt;code&gt;torchdiffeq&lt;/code&gt;&lt;/a&gt; library extends &lt;a href="https://baud.rs/ZT2Bo3"&gt;PyTorch’s&lt;/a&gt; ecosystem with differentiable ODE solvers, supporting batch-mode integration and seamless hardware acceleration via &lt;a href="https://baud.rs/x3h146"&gt;CUDA&lt;/a&gt; GPUs. By leveraging vectorized operations and batched computation, &lt;code&gt;torchdiffeq&lt;/code&gt; makes it possible to simulate thousands of parameterized systems orders of magnitude faster than traditional approaches.&lt;/p&gt;
&lt;p&gt;This article empirically compares &lt;code&gt;scipy.solve_ivp&lt;/code&gt; and &lt;code&gt;torchdiffeq&lt;/code&gt; on a realistic, parameterized ballistic projectile problem. We'll see how modern, batch-oriented tools unlock dramatic speedups—making large-scale simulation, optimization, and uncertainty quantification far more practical and scalable.&lt;/p&gt;
&lt;h3&gt;The Ballistics Problem: ODEs and Parameters&lt;/h3&gt;
&lt;p&gt;At the heart of projectile motion lies a classic set of equations: the &lt;a href="https://baud.rs/0Ifm4e"&gt;Newtonian laws of motion&lt;/a&gt; under the influence of gravity. In real-world scenarios—be it sports, military science, or atmospheric research—it's crucial to account not just for gravity but also for aerodynamic drag, which resists motion and varies with both the speed and shape of the object. For fast-moving projectiles like baseballs, artillery shells, or drones, drag is well-approximated as quadratic in velocity.&lt;/p&gt;
&lt;p&gt;The trajectory of a projectile under both gravity and quadratic drag is described by the following system of ODEs:&lt;/p&gt;
&lt;p&gt;$ \frac{d\mathbf{r}}{dt} = \mathbf{v} $&lt;/p&gt;
&lt;p&gt;$ \frac{d\mathbf{v}}{dt} = -g \hat{z} - \frac{k}{m} |\mathbf{v}| \mathbf{v} $&lt;/p&gt;
&lt;p&gt;Here, $\mathbf{r}$ is the position vector, $\mathbf{v}$ is the velocity vector, $g$ is the gravitational acceleration (9.81 m/s², directed downward), $m$ is the projectile's mass, and $k$ is the drag coefficient—a parameter incorporating air density, projectile shape, and cross-sectional area. The term $-\frac{k}{m} |\mathbf{v}| \mathbf{v}$ captures the quadratic (speed-squared) air resistance opposing motion.&lt;/p&gt;
&lt;p&gt;This model supports a range of relevant parameters:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Initial speed ($v_0$)&lt;/strong&gt;: How fast the projectile is launched.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Launch angle ($\theta$)&lt;/strong&gt;: The elevation above the horizontal.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Azimuth ($\phi$)&lt;/strong&gt;: The compass direction of the launch in the x-y plane.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Drag coefficient ($k$)&lt;/strong&gt;: Varies by projectile type and environment (e.g., bullets, baseballs, or debris).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mass ($m$)&lt;/strong&gt;: Generally constant for a given projectile, but can vary in sensitivity analyses.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;By randomly sampling these parameters, we can simulate broad families of real-world projectile trajectories—quantifying variations due to weather, launch conditions, or design tolerances. This approach is vital in engineering (for safety margins and optimization), defense (for targeting uncertainty), and physics education (visualizing parameter effects). With these governing principles defined, we’re equipped to systematically simulate and analyze thousands of projectile scenarios.&lt;/p&gt;
&lt;h3&gt;Vectorized Batch Simulation: Why It Matters&lt;/h3&gt;
&lt;p&gt;In classical physics instruction or simple engineering analyses, simulating a single projectile—perhaps varying its launch angle or speed by hand—was once sufficient to gain insight into trajectory behavior. But the demands of modern computational science and industry go far beyond this. Today, engineers, data scientists, and researchers routinely confront tasks like uncertainty quantification, statistical analysis, design optimization, or machine learning, all of which require running the same model across thousands or even millions of parameter combinations. For projectile motion, that might mean sampling hundreds of drag coefficients, launch angles, and initial velocities to estimate failure probabilities, optimize for maximum range under real-world disturbances, or quantify the uncertainty in a targeting system.&lt;/p&gt;
&lt;p&gt;Attempting to tackle these large-scale parameter sweeps with traditional serial Python code quickly exposes severe performance limitations. Standard Python scripts iterate through scenarios using simple loops—solving the ODE for one set of inputs, then moving to the next. While such code is easy to write and understand, it suffers from significant overhead: each call to an ODE solver like &lt;code&gt;scipy.solve_ivp&lt;/code&gt; carries the cost of repeatedly allocating memory, reinterpreting Python functions, and performing calculations on a single set of parameters without leveraging efficiencies of scale.&lt;/p&gt;
&lt;p&gt;Moreover, CPUs themselves have limited capacity for parallel execution. Although some scientific computing libraries exploit multicore CPUs for modest speedups, true high-throughput workloads outstrip what a desktop processor can provide. This is where vectorization and hardware acceleration revolutionize scientific computing. By formulating simulations so that many parameter sets are processed in tandem, vectorized code can amortize memory access and computation over entire batches.&lt;/p&gt;
&lt;p&gt;This paradigm is taken even further with the introduction of modern hardware accelerators—particularly Graphics Processing Units (GPUs). GPUs are designed for massive parallel processing, capable of performing thousands of operations simultaneously. Frameworks like PyTorch make it straightforward to move simulation data to the GPU and exploit this parallelism using batch operations and tensor arithmetic. Libraries such as &lt;code&gt;torchdiffeq&lt;/code&gt;, built on PyTorch, allow entire ensembles of ODE initial conditions and parameters to be integrated at once, often achieving one or even two orders of magnitude speedup over standard serial approaches.&lt;/p&gt;
&lt;p&gt;By harnessing vectorized and accelerated computation, we shift from thinking about trajectories one at a time to simulating entire probability distributions of outcomes—enabling robust analysis and real-time feedback that serial methods simply cannot deliver.&lt;/p&gt;
&lt;h3&gt;Setting Up the Experiment&lt;/h3&gt;
&lt;p&gt;To rigorously compare batch ODE solvers in a realistic context, we construct an experiment that simulates a large family of projectiles, each with unique initial conditions and drag parameters. Here, we demonstrate how to generate the complete dataset for such an experiment, scaling easily to $N=10,000$ scenarios or more.&lt;/p&gt;
&lt;p&gt;First, we select which parameters to randomize:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Initial speed ($v_0$)&lt;/strong&gt;: uniformly sampled between 100 and 140 m/s.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Launch angle ($\theta$)&lt;/strong&gt;: uniformly distributed between 20° and 70° (converted to radians).&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Azimuth ($\phi$)&lt;/strong&gt;: uniformly distributed from 0 to $2\pi$, representing all compass directions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Drag coefficient ($k$)&lt;/strong&gt;: uniformly sampled between 0.03 and 0.07; these bounds reflect different projectile shapes or environmental conditions.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Mass ($m$)&lt;/strong&gt;: held constant at 1.0 kg for simplicity.&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The initial position for each projectile is set at $(x, y, z) = (0, 0, 1)$, representing launches from a height of 1 meter above ground.&lt;/p&gt;
&lt;p&gt;Here is the core code to generate these parameters and construct the state vectors:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;  &lt;span class="c1"&gt;# Number of projectiles&lt;/span&gt;
&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;r0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;r0&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# start at z=1m&lt;/span&gt;

&lt;span class="n"&gt;speeds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;140&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;angles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;azimuths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.07&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;
&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;9.81&lt;/span&gt;

&lt;span class="c1"&gt;# Compute velocity components from speed, angle, and azimuth&lt;/span&gt;
&lt;span class="n"&gt;v0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;speeds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;azimuths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;speeds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;azimuths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;speeds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Combine into state vector: [x, y, z, vx, vy, vz]&lt;/span&gt;
&lt;span class="n"&gt;y0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hstack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;r0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;With this setup, each row of &lt;code&gt;y0&lt;/code&gt; fully defines the position and velocity of one simulated projectile, and associated arrays (&lt;code&gt;k&lt;/code&gt;, &lt;code&gt;m&lt;/code&gt;, etc.) capture the unique drag and physical parameters. This approach ensures our batch simulations cover a broad, realistic spread of possible projectile behaviors.&lt;/p&gt;
&lt;h3&gt;Serial Approach: scipy.solve_ivp&lt;/h3&gt;
&lt;p&gt;The &lt;code&gt;scipy.integrate.solve_ivp&lt;/code&gt; function is a standard tool in scientific Python for numerically solving initial value problems for ordinary differential equations (ODEs). Designed for flexibility and usability, it allows users to specify the right-hand side function, initial conditions, time span, and integration tolerances. It's ideal for scenarios where you need to inspect or visualize a single trajectory in detail, perform stepwise integration, or analyze systems with events (such as ground impact in our ballistics context).&lt;/p&gt;
&lt;p&gt;However, &lt;code&gt;solve_ivp&lt;/code&gt; is fundamentally serial in nature: each call integrates one ODE system, with one set of inputs and parameters. To simulate a batch of projectiles with varying initial conditions and drag parameters, a typical approach is to loop over all $N$ cases, calling &lt;code&gt;solve_ivp&lt;/code&gt; anew each time. This approach is straightforward, but comes with key drawbacks: overhead from repeated Python function calls, redundant setup within each call, and no built-in way to leverage vectorization or parallel computation on CPUs or GPUs.&lt;/p&gt;
&lt;p&gt;Here’s how the serial batch simulation is performed for our random projectiles:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;scipy.integrate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;solve_ivp&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ballistic_ivp_factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ki&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;vel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ki&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vel&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concatenate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;hit_ground_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;hit_ground_event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;terminal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;hit_ground_event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="n"&gt;t_eval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;trajectories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;sol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;solve_ivp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;ballistic_ivp_factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;t_eval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t_eval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rtol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hit_ground_event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;trajectories&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To extract and plot the $i$-th projectile’s trajectory (for example, $x$ vs. $z$):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trajectories&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;trajectories&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/solve_ivp-trajectories.png" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;&lt;/p&gt;
&lt;p&gt;While this method is robust and works for small $N$, it scales poorly for large batches. Each ODE integration runs one after the other, keeping all computation on the CPU, and does not exploit the potential speedup from modern hardware or batch processing. For workflows involving thousands of projectiles, these limitations quickly become significant.&lt;/p&gt;
&lt;h3&gt;Batched &amp;amp; Accelerated: torchdiffeq and PyTorch&lt;/h3&gt;
&lt;p&gt;Recent advances in machine learning frameworks have revolutionized scientific computing, and PyTorch is at the forefront. While best known for deep learning, PyTorch offers powerful tools for general numerical tasks, including automatic differentiation, GPU acceleration, and—critically for large-scale simulations—native support for batched and vectorized computation. Building on this, the &lt;code&gt;torchdiffeq&lt;/code&gt; library brings state-of-the-art ODE solvers to the PyTorch ecosystem. This unlocks not only scalable and differentiable simulations, but also unprecedented throughput for large parameter sweeps thanks to efficient batching.&lt;/p&gt;
&lt;p&gt;Unlike &lt;code&gt;scipy.solve_ivp&lt;/code&gt;, which solves one ODE system per call, &lt;code&gt;torchdiffeq.odeint&lt;/code&gt; can handle entire batches simultaneously. If you stack $N$ initial conditions into a tensor of shape $(N, D)$ (with $D$ being the state dimension, e.g., position and velocity components), and you write your ODE’s right-hand-side function to process these $N$ states in parallel, &lt;code&gt;odeint&lt;/code&gt; will integrate all of them in one go. This batched approach is highly efficient—especially when offloading the computation to a CUDA-enabled GPU, which can process thousands of simple ODE systems at once.&lt;/p&gt;
&lt;p&gt;A custom ODE function in PyTorch for batched ballistics looks like this:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torchdiffeq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;odeint&lt;/span&gt;

&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cuda'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s1"&gt;'cpu'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;BallisticsODEBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;vel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vel&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After preparing the initial states (&lt;code&gt;y0_torch&lt;/code&gt;, shape $(N, 6)$), you launch the batch integration with:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;odefunc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BallisticsODEBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;y0_torch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;t_torch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;sol_batch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;odeint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;odefunc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y0_torch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_torch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rtol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# (T, N, 6)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;By processing every $N$ parameter set in a single tensor operation, batching reduces memory and Python overhead substantially compared to looping with &lt;code&gt;solve_ivp&lt;/code&gt;. When running on a GPU, these speedups are often dramatic—sometimes orders of magnitude—due to massive parallelism and reduced per-call Python latency. For researchers and engineers running uncertainty analyses or global optimizations, batched ODE integration with &lt;code&gt;torchdiffeq&lt;/code&gt; makes large-scale simulation not only practical, but fast.&lt;/p&gt;
&lt;h3&gt;Cropping and Plotting Trajectories&lt;/h3&gt;
&lt;p&gt;When visualizing or comparing projectile trajectories, it's important to stop each curve exactly when the projectile reaches ground level ($z = 0$). Without this cropping, some trajectories would artificially continue below ground due to numerical integration, making visualizations misleading and length-biased. To ensure all plots fairly represent real-world impact, we truncate each trajectory at its ground crossing, interpolating between the last above-ground and first below-ground points to find the precise impact location.&lt;/p&gt;
&lt;p&gt;The following function performs this interpolation:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;crop_trajectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;where&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nb"&gt;len&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;frac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="n"&gt;x_crop&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;frac&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concatenate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x_crop&lt;/span&gt;&lt;span class="p"&gt;]]),&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concatenate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;[:&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.0&lt;/span&gt;&lt;span class="p"&gt;]])&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Using this, we can generate “spaghetti plots” for both solvers, showcasing dozens or hundreds of realistic, ground-terminated trajectories for direct comparison.&lt;br&gt;
Example:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;x_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z_t&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;crop_trajectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sol_batch_np&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;sol_batch_np&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;t_np&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z_t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'tab:blue'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;alpha&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img src="https://tinycomputers.io/images/torchdiffeq-spaghetti.png" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;&lt;/p&gt;
&lt;h3&gt;Performance Benchmarking: Timing the Solvers&lt;/h3&gt;
&lt;p&gt;To quantitatively compare the efficiency of &lt;code&gt;scipy.solve_ivp&lt;/code&gt; against the batched, accelerator-aware &lt;code&gt;torchdiffeq&lt;/code&gt;, we systematically measured simulation runtimes across a range of batch sizes ($N$): 100, 1,000, 5,000, and 10,000. We timed both solvers under identical conditions, measuring total wall-clock time and deriving the average simulation throughput (trajectories per second).&lt;/p&gt;
&lt;p&gt;All experiments were run on a workstation equipped with an Intel i7 CPU and &lt;a href="https://baud.rs/GTMdkM"&gt;NVIDIA Pascal GPUs&lt;/a&gt;), with PyTorch configured for CUDA acceleration. The same ODE system and tolerance settings ($\text{rtol}=1\text{e-5}$, $\text{atol}=1\text{e-7}$) were used for both solvers.&lt;/p&gt;
&lt;div style="clear: both;"&gt;&lt;/div&gt;
&lt;p&gt;The script below shows the core timing procedure:&lt;/p&gt;
&lt;div style="clear: both;"&gt;&lt;/div&gt;

&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;numpy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torchdiffeq&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;odeint&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;scipy.integrate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;solve_ivp&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;time&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;matplotlib.pyplot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;plt&lt;/span&gt;

&lt;span class="c1"&gt;# For reproducibility&lt;/span&gt;
&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;seed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;42&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Physics constants&lt;/span&gt;
&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;9.81&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;generate_initial_conditions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;r0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;r0&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;  &lt;span class="c1"&gt;# z=1m&lt;/span&gt;
    &lt;span class="n"&gt;speeds&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;140&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;angles&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;20&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;70&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;azimuths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;v0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;speeds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;azimuths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;speeds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;azimuths&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;speeds&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;angles&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;uniform&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;0.03&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.07&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;hstack&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;r0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ballistic_ivp_factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ki&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;fn&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;vel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linalg&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ki&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vel&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;concatenate&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;fn&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;hit_ground_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;hit_ground_event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;terminal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
&lt;span class="n"&gt;hit_ground_event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;BallisticsODEBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;nn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Module&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nb"&gt;super&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="fm"&gt;__init__&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;view&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
        &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;forward&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;vel&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:]&lt;/span&gt;
        &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;norm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;keepdim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;zeros_like&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;g&lt;/span&gt;
        &lt;span class="n"&gt;acc&lt;/span&gt; &lt;span class="o"&gt;-=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;speed&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vel&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;vel&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;acc&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cuda'&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;is_available&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="s1"&gt;'cpu'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"PyTorch device: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;N_list&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
&lt;span class="n"&gt;t_points&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;
&lt;span class="n"&gt;t_eval&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_points&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;t_torch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_points&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;timings&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="s1"&gt;'solve_ivp'&lt;/span&gt;&lt;span class="p"&gt;:[],&lt;/span&gt; &lt;span class="s1"&gt;'torchdiffeq'&lt;/span&gt;&lt;span class="p"&gt;:[]}&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;N&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;N_list&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;=== Benchmarking N = &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; ==="&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;generate_initial_conditions&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# --- torchdiffeq batched solution&lt;/span&gt;
    &lt;span class="n"&gt;odefunc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;BallisticsODEBatch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;y0_torch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dtype&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;float32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;t_torch_dev&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;t_torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;to&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"cuda"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="n"&gt;sol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;odeint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;odefunc&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y0_torch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_torch_dev&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;rtol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-7&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# shape (T,N,6)&lt;/span&gt;
    &lt;span class="n"&gt;torch&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cuda&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;synchronize&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s2"&gt;"cuda"&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;
    &lt;span class="n"&gt;time_torch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"torchdiffeq (batch): &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time_torch&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;timings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'torchdiffeq'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_torch&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# --- solve_ivp serial solution&lt;/span&gt;
    &lt;span class="n"&gt;start&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;solve_ivp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;ballistic_ivp_factory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;]),&lt;/span&gt;
            &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
            &lt;span class="n"&gt;t_eval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;t_eval&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;rtol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;atol&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-7&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;hit_ground_event&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;time_ivp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;time&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;perf_counter&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;start&lt;/span&gt;
    &lt;span class="nb"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"solve_ivp (serial):  &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;time_ivp&lt;/span&gt;&lt;span class="si"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;.2f&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;s"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;timings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'solve_ivp'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;time_ivp&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# ---- Plot results&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'solve_ivp'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'solve_ivp (serial, CPU)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'o'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;N_list&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timings&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'torchdiffeq'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;'torchdiffeq (batch, &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;device&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;type&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt;)'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'s'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;yscale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'log'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xscale&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'log'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xlabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Batch Size N'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ylabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Total Simulation Time (seconds, log scale)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ODE Solver Performance: solve_ivp vs torchdiffeq'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;which&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'both'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'--'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;legend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tight_layout&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Benchmark Results&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;PyTorch device: cuda

=== Benchmarking N = 100 ===
torchdiffeq (batch): 0.35s
solve_ivp (serial):  0.60s

=== Benchmarking N = 1000 ===
torchdiffeq (batch): 0.29s
solve_ivp (serial):  5.84s

=== Benchmarking N = 5000 ===
torchdiffeq (batch): 0.31s
solve_ivp (serial):  29.84s

=== Benchmarking N = 10000 ===
torchdiffeq (batch): 0.31s
solve_ivp (serial):  59.74s
&lt;/pre&gt;&lt;/div&gt;

&lt;div style="clear: both;"&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/solve_ivp-vs-torchdiffeq.png" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;&lt;/p&gt;
&lt;p&gt;As shown in the table and the bar chart below, &lt;code&gt;torchdiffeq&lt;/code&gt; achieves orders of magnitude speedup, especially when run on GPU. While &lt;code&gt;solve_ivp&lt;/code&gt;'s wall time scales linearly with batch size, &lt;code&gt;torchdiffeq&lt;/code&gt;’s increase is much more gradual due to highly efficient batch parallelism on both CPU and GPU.&lt;/p&gt;
&lt;div style="clear: both;"&gt;&lt;/div&gt;

&lt;h4&gt;Visualization&lt;/h4&gt;
&lt;div style="clear: both;"&gt;&lt;/div&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/both-solvers.png" style="box-shadow: 0 30px 40px rgba(0,0,0,.1);  padding: 20px 20px 20px 20px;"&gt;&lt;/p&gt;
&lt;div style="clear: both;"&gt;&lt;/div&gt;

&lt;p&gt;These results decisively demonstrate the advantage of batched, hardware-accelerated ODE integration for large-scale uncertainty quantification and parametric studies. For modern simulation workloads, &lt;code&gt;torchdiffeq&lt;/code&gt; turns otherwise intractable analyses into routine computations.&lt;/p&gt;
&lt;h3&gt;Practical Insights &amp;amp; Limitations&lt;/h3&gt;
&lt;p&gt;The dramatic performance advantage of &lt;code&gt;torchdiffeq&lt;/code&gt; for large-batch ODE integration is a game-changer for certain classes of scientific and engineering simulations. However, like any advanced computational tool, its real-world utility depends on the problem context, user preferences, and technical constraints.&lt;/p&gt;
&lt;h4&gt;When torchdiffeq Shines&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Large Batch Sizes:&lt;/strong&gt; The most compelling case for &lt;code&gt;torchdiffeq&lt;/code&gt; is when you need to simulate &lt;em&gt;many&lt;/em&gt; similar ODE systems in parallel. If your workflow naturally involves analyzing thousands of parameter sets—such as in Monte Carlo uncertainty quantification, global sensitivity analysis, optimization sweeps, or high-volume forward simulations—&lt;code&gt;torchdiffeq&lt;/code&gt; can turn days of computation into minutes, especially when exploiting a modern GPU.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Homogeneous ODE Forms:&lt;/strong&gt; &lt;code&gt;torchdiffeq&lt;/code&gt; excels when the differential equations are structurally identical across all batch members (e.g., all projectiles differ only in launch parameters, mass, or drag, not in governing equations). This allows vectorized tensor operations and maximizes parallel hardware utilization.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;GPU Acceleration:&lt;/strong&gt; If you have access to CUDA hardware, the batch approach provided by PyTorch integrates seamlessly. For highly parallelizable problems, the speedup can be more than an order of magnitude compared to CPU execution alone.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Where scipy’s solve_ivp Is Preferable&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single or Few Simulations:&lt;/strong&gt; If your workload only involves single or a handful of trajectories (or you need results interactively), &lt;code&gt;scipy.solve_ivp&lt;/code&gt; is still highly convenient. It’s light on dependencies, simple to use, and well-integrated with the broader SciPy ecosystem.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Out-of-the-box Event Handling:&lt;/strong&gt; &lt;code&gt;solve_ivp&lt;/code&gt; integrates event location cleanly, making it straightforward to stop integration at complex conditions (like ground impact, threshold crossings, or domain boundaries) with minimal setup.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No PyTorch/Deep Learning Stack Needed:&lt;/strong&gt; For users not otherwise relying on PyTorch, keeping everything in NumPy/SciPy can mean a lighter, more transparent setup and easier integration into classic scientific workflows.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Accuracy and Tolerances&lt;/h4&gt;
&lt;p&gt;Both &lt;code&gt;torchdiffeq&lt;/code&gt; and &lt;code&gt;solve_ivp&lt;/code&gt; allow setting relative and absolute tolerances for error control. In most practical applications, both provide comparable accuracy if configured similarly—though always test with your specific ODEs and parameters, as subtle differences can arise in stiff or highly nonlinear regimes.&lt;/p&gt;
&lt;h4&gt;Limitations of torchdiffeq&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Complex Events and Custom Solvers:&lt;/strong&gt; While &lt;code&gt;torchdiffeq&lt;/code&gt; supports batching and GPU execution, its event handling isn’t as automatic or flexible as in &lt;code&gt;solve_ivp&lt;/code&gt;. If you need advanced stopping criteria, adaptive step event targeting, or integration using custom/obscure methods, PyTorch-based solvers may require more custom code or workarounds.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Smaller Scientific Ecosystem:&lt;/strong&gt; While PyTorch is hugely popular in machine learning, the larger SciPy ecosystem offers more “out-of-the-box” scientific routines and examples. Some users may need to roll their own utilities in PyTorch.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Learning Curve/Code Complexity:&lt;/strong&gt; Writing vectorized, batched ODE functions (especially for newcomers to PyTorch or GPU programming) can pose an initial hurdle. For seasoned scientists accustomed to “for-loop” logic, adapting to a tensor-based, batch-first paradigm may require unlearning older habits.&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Maintainability&lt;/h4&gt;
&lt;p&gt;For codebases built on PyTorch or targeted at high-throughput, the benefits are worth the upfront learning cost. For one-off or small-scale science projects, the classic SciPy stack may remain more maintainable and accessible for most users. Ultimately, the choice depends on the problem scale, user expertise, and requirements for future extensibility and hardware performance.&lt;/p&gt;
&lt;h3&gt;Conclusions&lt;/h3&gt;
&lt;p&gt;This benchmark study highlights the substantial performance gains attainable by leveraging &lt;code&gt;torchdiffeq&lt;/code&gt; and PyTorch for batched ODE integration in Python. While &lt;code&gt;scipy.solve_ivp&lt;/code&gt; remains robust and user-friendly for single or low-volume simulations, it quickly becomes a bottleneck when working with thousands of parameter variations common in uncertainty quantification, optimization, or high-throughput design. By contrast, &lt;code&gt;torchdiffeq&lt;/code&gt;—especially when combined with GPU acceleration—enables orders-of-magnitude faster simulations thanks to its inherent support for vectorized batching and parallel computation.&lt;/p&gt;
&lt;p&gt;Such speedups are transformative for both research and industry. Rapid batch simulations make Monte Carlo analyses, parametric studies, and iterative design far more feasible, allowing deeper exploration and faster time-to-insight across fields from engineering to quantitative science. For machine learning scientists, batched ODE integration can even be incorporated into differentiable pipelines for neural ODEs or model-based reinforcement learning.&lt;/p&gt;
&lt;p&gt;If you face large-scale ODE workloads, we strongly encourage experimenting with the supplied &lt;a href="https://tinycomputers.io/pages/torchdiffeq.ipynb"&gt;example code&lt;/a&gt; and adapting torchdiffeq to your own applications. Additional documentation, tutorials, and PyTorch resources are available at the &lt;a href="https://baud.rs/x8egoq"&gt;torchdiffeq repository&lt;/a&gt; and &lt;a href="https://baud.rs/ZmdJa6"&gt;PyTorch documentation&lt;/a&gt;. Embracing modern computational tools can unlock dramatic gains in productivity, capability, and discovery.&lt;/p&gt;
&lt;h3&gt;Appendix: Code Listing&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://tinycomputers.io/pages/torchdiffeq.html"&gt;TorchDiffEq&lt;/a&gt; contains an HTML rendering of the complete code listing for this article, including all imports, functions, and plotting routines.  For the actual Jupyter notebook, see &lt;a href="https://tinycomputers.io/pages/torchdiffeq.ipynb"&gt;torchdiffeq.ipynb&lt;/a&gt;.  You can run it directly in a Jupyter notebook or adapt it to your own projects.&lt;/p&gt;</description><category>ballistics</category><category>batch simulation</category><category>drag force</category><category>gpu acceleration</category><category>high-throughput simulation</category><category>monte carlo simulation</category><category>numerical integration</category><category>ode solver</category><category>optimization</category><category>parallel computation</category><category>parameter sweep</category><category>performance benchmarking</category><category>projectile motion</category><category>python</category><category>pytorch</category><category>scientific computing</category><category>scipy.solve_ivp</category><category>torchdiffeq</category><category>uncertainty quantification</category><category>vectorized computation</category><guid>https://tinycomputers.io/posts/accelerating-large-scale-ballistic-simulations-with-torchdiffeq-and-pytorch.html</guid><pubDate>Sat, 10 May 2025 20:44:52 GMT</pubDate></item><item><title>Simulating Buckshot Spread – A Deep Dive with Python and ODEs</title><link>https://tinycomputers.io/posts/simulating-buckshot-spread-a-deep-dive-with-python-and-odes.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/simulating-buckshot-spread-a-deep-dive-with-python-and-odes_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;25 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;Shotguns are celebrated for their unique ability to launch a cluster of small projectiles—referred to as pellets—simultaneously, making them highly effective at short ranges in hunting, sport shooting, and defensive scenarios. The way these pellets separate and spread apart during flight creates the signature pattern seen on shotgun targets. While the general term “shot” applies to all such projectiles, specific pellet sizes exist, each with distinct ballistic properties. In this article, we will focus on modeling &lt;a href="https://baud.rs/smF6TR"&gt;#00 buckshot&lt;/a&gt;, a popular choice for both self-defense and law enforcement applications due to its larger pellet size and stopping power.&lt;/p&gt;
&lt;p&gt;By using Python, we’ll construct a simulation that predicts the paths and spread of #00 buckshot pellets after they leave the barrel. Drawing from principles of physics—like gravity and aerodynamic drag—and incorporating randomness to reflect real-world variation, our code will numerically solve each pellet’s flight path. This approach lets us visualize the resulting shot pattern at a chosen distance downrange and gain a deeper appreciation for how ballistic forces and initial conditions shape what happens when the trigger is pulled.&lt;/p&gt;
&lt;h3&gt;Understanding the Physics of Shotgun Pellets&lt;/h3&gt;
&lt;p&gt;When a shotgun is fired, each pellet exits the barrel at a significant velocity, starting a brief yet complex flight through the air. The physical forces acting on the pellets dictate their individual paths and, ultimately, the characteristic spread pattern observed at the target. To create an accurate simulation of this process, it’s important to understand the primary factors influencing pellet motion.&lt;/p&gt;
&lt;p&gt;The most fundamental force is gravity. This constant downward pull, at approximately 9.81 meters per second squared, causes pellets to fall toward the earth as they travel forward. The effect of gravity is immediate: even with a rapid muzzle velocity, pellets begin to drop soon after leaving the barrel, and this drop becomes more noticeable over longer distances.&lt;/p&gt;
&lt;p&gt;Another critical factor, particularly relevant for small and light projectiles such as #00 buckshot, is aerodynamic drag. As a pellet speeds through the air, it constantly encounters resistance from air molecules in its path. Drag not only oppose the pellet’s motion but also increases rapidly with speed—it is proportional to the square of the velocity. The magnitude of this force depends on properties such as the pellet’s cross-sectional area, mass, and shape (summarized by the drag coefficient). In this model, we assume all pellets are nearly spherical and share the same mass and size, using standard values for drag.&lt;/p&gt;
&lt;p&gt;The interplay between gravity and aerodynamic drag controls how far each pellet travels and how much it slows before reaching the target. These forces are at the core of external ballistics, shaping how the tight column of pellets at the muzzle becomes a broad pattern by the time it arrives downrange. Understanding and accurately representing these effects is essential for any simulation that aims to realistically capture shotgun pellet motion.&lt;/p&gt;
&lt;h3&gt;Setting Up the Simulation&lt;/h3&gt;
&lt;p&gt;Before simulating shotgun pellet flight, the foundation of the model must be established through a series of physical parameters. These values are crucial—they dictate everything from the amount of drag experienced by a pellet to the degree of possible spread observed on a target.&lt;/p&gt;
&lt;p&gt;First, the code defines characteristics of a single #00 buckshot pellet. The pellet diameter (&lt;code&gt;d&lt;/code&gt;) is set to 0.0084 meters, giving a radius (&lt;code&gt;r&lt;/code&gt;) of half that value. The cross-sectional area (&lt;code&gt;A&lt;/code&gt;) is calculated as π times the radius squared. This area directly impacts how much air resistance the pellet experiences—the larger the cross-section, the more drag slows it down. The mass (&lt;code&gt;m&lt;/code&gt;) is set to 0.00351 kilograms, representing the weight of an individual #00 pellet in a standard shotgun load.&lt;/p&gt;
&lt;p&gt;Next, the code specifies values needed for the calculation of aerodynamic drag. The drag coefficient (&lt;code&gt;Cd&lt;/code&gt;) is set to 0.47, a typical value for a sphere moving through air. Air density (&lt;code&gt;rho&lt;/code&gt;) is specified as 1.225 kilograms per cubic meter, which is a standard value at sea level under average conditions. Gravity (&lt;code&gt;g&lt;/code&gt;) is established as 9.81 meters per second squared.&lt;/p&gt;
&lt;p&gt;The number of pellets to simulate is set with &lt;code&gt;num_pellets&lt;/code&gt;; here, nine pellets are used, reflecting a common #00 buckshot shell configuration. The &lt;code&gt;v0&lt;/code&gt; parameter sets the initial (muzzle) velocity for each pellet, at 370 meters per second—a realistic value for modern 12-gauge loads. To add realism, slight random variation in velocity is included using &lt;code&gt;v_sigma&lt;/code&gt;, which allows muzzle velocity to be sampled from a normal distribution for each pellet. This captures the real-world variability inherent in a shotgun shot.&lt;/p&gt;
&lt;p&gt;To model the spread of pellets as they leave the barrel, the code uses &lt;code&gt;spread_std_deg&lt;/code&gt; and &lt;code&gt;spread_max_deg&lt;/code&gt;. These parameters define the standard deviation and maximum value for the random angular deviation of each pellet in both horizontal and vertical directions. This gives each pellet a unique initial direction, simulating the inherent randomness and choke effect seen in actual shotgun blasts.&lt;/p&gt;
&lt;p&gt;Initial position coordinates (&lt;code&gt;x0&lt;/code&gt;, &lt;code&gt;y0&lt;/code&gt;, &lt;code&gt;z0&lt;/code&gt;) establish where the pellets start—here, at the muzzle, with the barrel one meter off the ground. The &lt;code&gt;pattern_distance&lt;/code&gt; defines how far away the “target” is placed, setting the plane where pellet impacts are measured. Finally, &lt;code&gt;max_time&lt;/code&gt; sets a hard cap on the simulated flight duration, ensuring computations finish even if a pellet never hits the ground or target.&lt;/p&gt;
&lt;p&gt;By specifying all these parameters before running the simulation, the code grounds its calculations in real-world physical properties, establishing a robust and realistic baseline for the ODE-based modeling that follows.&lt;/p&gt;
&lt;h3&gt;The ODE Model&lt;/h3&gt;
&lt;p&gt;At the heart of the simulation is a mathematical model that describes each pellet’s motion using an &lt;a href="https://baud.rs/HASI0U"&gt;ordinary differential equation&lt;/a&gt; (ODE). The state of a pellet in flight is captured by six variables: its position in three dimensions (x, y, z) and its velocity in each direction (vx, vy, vz). As the pellet travels, both gravity and aerodynamic drag act on it, continually altering its velocity and trajectory.&lt;/p&gt;
&lt;p&gt;Gravity is straightforward in the model—a constant downward acceleration, reducing the y-component (height) of the pellet’s velocity over time. The trickier part is aerodynamic drag, which opposes the pellet’s motion and depends on both its speed and orientation. In this simulation, drag is modeled using the standard quadratic law, which states that the decelerating force is proportional to the square of the velocity. Mathematically, the drag acceleration in each direction is calculated as:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;dv/dt = -k &lt;span class="gs"&gt;* v *&lt;/span&gt; v_dir
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;where &lt;code&gt;k&lt;/code&gt; bundles together the effects of drag coefficient, air density, area, and mass, &lt;code&gt;v&lt;/code&gt; is the current speed, and &lt;code&gt;v_dir&lt;/code&gt; is a velocity component (vx, vy, or vz).&lt;/p&gt;
&lt;p&gt;Within the &lt;code&gt;pellet_ode&lt;/code&gt; function, the code computes the combined velocity from its three components and then applies this drag to each directional velocity. Gravity appears as a constant subtraction from the vertical (vy) acceleration. The ODE function returns the derivatives of all six state variables, which are then numerically integrated over time using Scipy’s &lt;code&gt;solve_ivp&lt;/code&gt; routine.&lt;/p&gt;
&lt;p&gt;By combining these physics-based rules, the ODE produces realistic pellet flight paths, showing how each is steadily slowed by drag and pulled downward by gravity on its journey from muzzle to target.&lt;/p&gt;
&lt;h3&gt;Modeling Pellet Spread: Incorporating Randomness&lt;/h3&gt;
&lt;p&gt;A defining feature of shotgun use is the spread of pellets as they exit the barrel and travel toward the target. While the physics of flight create predictable paths, the divergence of each pellet from the bore axis is largely random, influenced by manufacturing tolerances, barrel choke, and small perturbations at ignition. To replicate this in simulation, the code incorporates controlled randomness into the initial direction and velocity of each pellet.&lt;/p&gt;
&lt;p&gt;For every simulated pellet, two angles are generated: one for vertical (up-down) deviation and one for horizontal (left-right) deviation. These angles are drawn from a normal (Gaussian) distribution centered at zero, reflecting the natural scatter expected from a well-maintained shotgun. Standard deviation and maximum values—set by &lt;code&gt;spread_std_deg&lt;/code&gt; and &lt;code&gt;spread_max_deg&lt;/code&gt;—control the tightness and outer limits of this spread. This ensures realistic variation while preventing extreme outliers not seen in practice.&lt;/p&gt;
&lt;p&gt;Muzzle velocity is also subject to small random variation. While the manufacturer’s rating might place velocity at 370 meters per second, factors like ammunition inconsistencies and environmental conditions can introduce fluctuations. By sampling the initial velocity for each pellet from a normal distribution (with mean &lt;code&gt;v0&lt;/code&gt; and standard deviation &lt;code&gt;v_sigma&lt;/code&gt;), the simulator reproduces this subtle randomness.&lt;/p&gt;
&lt;p&gt;To determine starting velocities in three dimensions (vx, vy, vz), the code applies trigonometric calculations based on the sampled initial angles and speed, ensuring that each pellet’s departure vector deviates uniquely from the barrel’s axis. The result is a spread pattern that closely mirrors those seen in field tests—a dense central cluster with some pellets landing closer to the edge.&lt;/p&gt;
&lt;p&gt;By weaving calculated randomness into the simulation’s initial conditions, the code not only matches the unpredictable nature of real-world shot patterns, but also creates meaningful output for analyzing shotgun effectiveness and pattern density at various distances.&lt;/p&gt;
&lt;h3&gt;ODE Integration with Boundary Events&lt;/h3&gt;
&lt;p&gt;Simulating the trajectory of each pellet requires numerically solving the equations of motion over time. This is accomplished by passing the ODE model to SciPy’s &lt;code&gt;solve_ivp&lt;/code&gt; function, which integrates the system from the pellet’s moment of exit until it either hits the ground, the target plane, or a maximum time is reached. To handle these criteria efficiently, the code employs two “event” functions that monitor for specific conditions during integration.&lt;/p&gt;
&lt;p&gt;The first event, &lt;code&gt;ground_event&lt;/code&gt;, is triggered when a pellet’s vertical position (&lt;code&gt;y&lt;/code&gt;) reaches zero, corresponding to ground impact. This event is marked as terminal in the integration, so once triggered, the ODE solver halts further calculation for that pellet—ensuring we don’t simulate motion beneath the earth.&lt;/p&gt;
&lt;p&gt;The second event, &lt;code&gt;pattern_event&lt;/code&gt;, fires when the pellet’s downrange distance (&lt;code&gt;x&lt;/code&gt;) equals the designated pattern distance. This captures the precise moment a pellet crosses the plane of interest, such as a target board at 5 meters. Unlike &lt;code&gt;ground_event&lt;/code&gt;, this event is not terminal, allowing the solver to keep tracking the pellet in case it flies beyond the target distance before landing.&lt;/p&gt;
&lt;p&gt;By combining these event-driven stops with dense output (for smooth interpolation) and a small integration step size, the code accurately and efficiently identifies either the ground impact or the target crossing for each pellet. This strategy ensures that every significant outcome in the flight—whether a hit or a miss—is reliably captured in the simulation.&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;numpy&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;np&lt;/span&gt;
&lt;span class="kn"&gt;from&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;scipy.integrate&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;solve_ivp&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;matplotlib.pyplot&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;as&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;plt&lt;/span&gt;

&lt;span class="c1"&gt;# Physical constants&lt;/span&gt;
&lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.0084&lt;/span&gt;      &lt;span class="c1"&gt;# m&lt;/span&gt;
&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;d&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;
&lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pi&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="c1"&gt;# m^2&lt;/span&gt;
&lt;span class="n"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.00351&lt;/span&gt;     &lt;span class="c1"&gt;# kg&lt;/span&gt;
&lt;span class="n"&gt;Cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.47&lt;/span&gt;
&lt;span class="n"&gt;rho&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.225&lt;/span&gt;     &lt;span class="c1"&gt;# kg/m^3&lt;/span&gt;
&lt;span class="n"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;9.81&lt;/span&gt;        &lt;span class="c1"&gt;# m/s^2&lt;/span&gt;

&lt;span class="n"&gt;num_pellets&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;9&lt;/span&gt;
&lt;span class="n"&gt;v0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;370&lt;/span&gt;        &lt;span class="c1"&gt;# muzzle velocity m/s&lt;/span&gt;
&lt;span class="n"&gt;v_sigma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;

&lt;span class="n"&gt;spread_std_deg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;
&lt;span class="n"&gt;spread_max_deg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;2.5&lt;/span&gt;

&lt;span class="n"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.&lt;/span&gt;

&lt;span class="n"&gt;pattern_distance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;5.0&lt;/span&gt;    &lt;span class="c1"&gt;# m&lt;/span&gt;
&lt;span class="n"&gt;max_time&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;pellet_ode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sqrt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;vz&lt;/span&gt;&lt;span class="o"&gt;**&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;Cd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;rho&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;A&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;m&lt;/span&gt;
    &lt;span class="n"&gt;dxdt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vx&lt;/span&gt;
    &lt;span class="n"&gt;dydt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt;
    &lt;span class="n"&gt;dzdt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;vz&lt;/span&gt;
    &lt;span class="n"&gt;dvxdt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vx&lt;/span&gt;
    &lt;span class="n"&gt;dvydt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;g&lt;/span&gt;
    &lt;span class="n"&gt;dvzdt&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;k&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;v&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vz&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;dxdt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dydt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dzdt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dvxdt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dvydt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;dvzdt&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="n"&gt;pattern_z&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;
&lt;span class="n"&gt;pattern_y&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="n"&gt;trajectories&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[]&lt;/span&gt;

&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;num_pellets&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="c1"&gt;# Randomize initial direction for spread&lt;/span&gt;
    &lt;span class="n"&gt;theta_h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spread_std_deg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;theta_h&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta_h&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spread_max_deg&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spread_max_deg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;theta_v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spread_std_deg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="n"&gt;theta_v&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;clip&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta_v&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spread_max_deg&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;radians&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;spread_max_deg&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="n"&gt;v0p&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;random&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;normal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;v0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;v_sigma&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Forward is X axis. Up is Y axis. Left-right is Z axis&lt;/span&gt;
    &lt;span class="n"&gt;vx0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v0p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta_v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta_h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;vy0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v0p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta_v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;vz0&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;v0p&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta_v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta_h&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;ic&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;x0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vx0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vy0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vz0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;ground_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;  &lt;span class="c1"&gt;# y[1] is height&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;ground_event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;terminal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;True&lt;/span&gt;
    &lt;span class="n"&gt;ground_event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;pattern_event&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;   &lt;span class="c1"&gt;# y[0] is x&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;pattern_distance&lt;/span&gt;
    &lt;span class="n"&gt;pattern_event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;terminal&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;False&lt;/span&gt;
    &lt;span class="n"&gt;pattern_event&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;direction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;

    &lt;span class="n"&gt;sol&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;solve_ivp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;pellet_ode&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;max_time&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;ic&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;events&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;ground_event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern_event&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;dense_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_step&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.01&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Find the stopping time: whichever is first, ground or simulation end&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t_events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;size&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t_events&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;t_end&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="n"&gt;t_plot&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;linspace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;t_end&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;trajectories&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;t_plot&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

    &lt;span class="c1"&gt;# Interpolate to pattern_distance for hit pattern&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;any&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;pattern_distance&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;argmax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="n"&gt;pattern_distance&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;idx&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;  &lt;span class="c1"&gt;# avoid index out of bounds if already starting beyond pattern_distance&lt;/span&gt;
            &lt;span class="n"&gt;frac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern_distance&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;zhit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;frac&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;yhit&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;frac&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;sol&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="n"&gt;idx&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;yhit&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                &lt;span class="n"&gt;pattern_z&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;zhit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;pattern_y&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;append&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;yhit&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# --- Plot 3D trajectories ---&lt;/span&gt;
&lt;span class="n"&gt;fig&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;12&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;7&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="n"&gt;ax&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;fig&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_subplot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;111&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;projection&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'3d'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;traj&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;trajectories&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vx&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vy&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;vz&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;traj&lt;/span&gt;
    &lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_xlabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Downrange X (m)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_ylabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Left-Right Z (m)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_zlabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Height Y (m)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;ax&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'3D Buckshot Pellet Trajectories (ODE solver)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="c1"&gt;# --- Plot pattern on 25m target plane ---&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;figure&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;figsize&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;6&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;

&lt;span class="n"&gt;circle&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Circle&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mf"&gt;0.2032&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'b'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="kc"&gt;False&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;linestyle&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'--'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'8 inch target'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gca&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;add_patch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;circle&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scatter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pattern_z&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;pattern_y&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;c&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'r'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;s&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;marker&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'o'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Pellet hits'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xlabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'Left-Right Offset (m)'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ylabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;'Height (m), target at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pattern_distance&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt; m'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sa"&gt;f&lt;/span&gt;&lt;span class="s1"&gt;'Buckshot Pattern at &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;pattern_distance&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s1"&gt; m'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axhline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'k'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;label&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'Muzzle height'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;axvline&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'k'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ls&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;':'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ylim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;xlim&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;legend&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;grid&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;True&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;gca&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;set_aspect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'equal'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;adjustable&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'box'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;plt&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;show&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Recording and Visualizing Pellet Impacts&lt;/h3&gt;
&lt;p&gt;Once a pellet’s trajectory has been simulated, it is important to determine exactly where it would strike the target plane placed at the specified downrange distance. Because the pellet’s position is updated in discrete time steps, it rarely lands exactly at the &lt;code&gt;pattern_distance&lt;/code&gt;. Therefore, the code detects when the pellet’s simulated x-position first passes this distance. At this point, a linear interpolation is performed between the two positions bracketing the target plane, calculating the precise y (height) and z (left-right) coordinates where the pellet would intersect the pattern distance. This ensures consistent and accurate hit placement regardless of integration step size.&lt;/p&gt;
&lt;p&gt;The resulting values for each pellet are appended to the &lt;code&gt;pattern_y&lt;/code&gt; and &lt;code&gt;pattern_z&lt;/code&gt; lists. These lists collectively represent the full group of pellet impact points at the target plane and can be conveniently visualized or analyzed further.&lt;/p&gt;
&lt;p&gt;By recording these interpolated impact points, the simulation offers direct insight into the spatial distribution of pellets on the target. This data allows shooters and engineers to assess key real-world characteristics such as pattern density, evenness, and the likelihood of hitting a given area. In visualization, these points paint a clear picture of spread and clustering, helping to understand both shotgun effectiveness and pellet behavior under the influence of drag and gravity.&lt;/p&gt;
&lt;h3&gt;Visualization: Plotting Trajectories and Impact Patterns&lt;/h3&gt;
&lt;p&gt;Visualizing the results of the simulation offers both an intuitive understanding of pellet motion and practical insight into shotgun performance. The code provides two types of plots: a three-dimensional trajectory plot and a two-dimensional pattern plot on the target plane.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/downrange.png" style="width: 640px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;The 3D trajectory plot displays the full flight paths of all simulated pellets, with axes labeled for downrange distance (&lt;code&gt;x&lt;/code&gt;), left-right offset (&lt;code&gt;z&lt;/code&gt;), and vertical height (&lt;code&gt;y&lt;/code&gt;). Each pellet's arc is traced from muzzle exit to endpoint, revealing not just forward travel and fall due to gravity, but also the sideways spread caused by angular deviation and drag. This plot gives a comprehensive, real-time sense of how pellets diverge and lose height, much like visualizing the flight of shot in slow motion. It can highlight trends such as gradual drop-offs, the effect of random spread angles, and which pellets remain above the ground longest.&lt;/p&gt;
&lt;p&gt;The pattern plane plot focuses on practical outcomes—the locations where pellets would strike a target at a given distance (e.g., 5 meters downrange). An 8-inch circle is superimposed to represent a common target size, providing context for real-world shooting scenarios. Each simulated impact point is marked, showing the actual distribution and clustering of pellets. Reference lines denote the muzzle height (horizontal) and the barrel center (vertical), helping to orient the viewer and relate simulated results to how a shooter would aim.&lt;/p&gt;
&lt;p&gt;Together, these visuals bridge the gap between abstract trajectory calculations and real shooting experience. The 3D plot helps explore external ballistics, while the pattern plot reflects what a shooter would see on a paper target at the range—key information for understanding spread, pattern density, and shotgun effectiveness.&lt;/p&gt;
&lt;h3&gt;Assumptions &amp;amp; Limitations of the Model&lt;/h3&gt;
&lt;p&gt;While this simulation offers a physically grounded view of #00 buckshot spread, several simplifying assumptions shape its results. The code treats all pellets as perfectly spherical, identical in size and mass, and does not account for pellet deformation or fracturing—both of which can occur during firing or impact. Air properties are held constant, with fixed density and drag coefficient values; in reality, both can change due to weather, altitude, and even fluctuations in pellet speed.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/buckshot-spread.png" style="width: 340px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: right; padding: 20px 20px 20px 20px;"&gt;The external environment in the model is idealized: there is no simulated wind, nor do pellets interact with one another mid-flight. Real pellets may collide or influence each other's paths, especially immediately after leaving the barrel. The simulation also omits nuanced effects of shotgun choke or barrel design, instead representing spread as a simple random angle without structure, patterning, or environmental response. The shooter’s aim is assumed perfectly flat, originating from a set muzzle height, with no allowance for human error or tilt.&lt;/p&gt;
&lt;p&gt;These simplifications mean that actual shotgun patterns may differ in meaningful ways. Real-world patterns can display uneven density, elliptical shapes from chokes, or wind-induced drift—all absent from this model. Furthermore, pellet deformation can lead to less predictable spread, and varying air conditions or shooter input can add additional variability. Nevertheless, the simulation provides a valuable baseline for understanding the primary forces and expected outcomes, even if it cannot capture every subtlety from live fire.&lt;/p&gt;
&lt;h3&gt;Possible Improvements and Extensions&lt;/h3&gt;
&lt;p&gt;This simulation, while useful for visualizing basic pellet dynamics, could be made more realistic by addressing some of its idealizations. Incorporating wind modeling would add lateral drift, making the simulation more applicable to outdoor shooting scenarios. Simulating non-spherical or deformed pellets—accounting for variations in shape, mass, or surface—could change each pellet’s drag and produce more irregular spread patterns. Introducing explicit choke effects would allow for non-uniform or elliptical spreads that better match the output from different shotgun barrels and constrictions.&lt;/p&gt;
&lt;p&gt;Environmental factors like altitude and temperature could be included to adjust air density and drag coefficient dynamically, reflecting their real influence on ballistics. Finally, modeling shooter-related factors such as sight alignment, aim variation, or recoil-induced muzzle movement would add further variability. Collectively, these enhancements would move the simulation closer to the unpredictable reality of shotgun use, providing even greater value for shooters, ballistics researchers, and enthusiasts alike.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Physically-accurate simulations of shotgun pellet spread offer valuable lessons for both programmers and shooting enthusiasts. By translating real-world ballistics into code, we gain a deeper understanding of the factors that shape shot patterns and how subtle changes in variables can influence outcomes. Python, paired with SciPy’s ODE solvers, proves to be an accessible and powerful toolkit for exploring these complex systems. Whether used for educational insight, hobby experimentation, or designing safer and more effective ammunition, this approach opens the door to further exploration. Readers are encouraged to adapt, extend, or refine the code to match their own interests and scenarios.&lt;/p&gt;
&lt;h3&gt;References &amp;amp; Further Reading&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://baud.rs/tboAIk"&gt;McCoy, R.L., &lt;em&gt;Modern Exterior Ballistics&lt;/em&gt;&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/I1QqRZ"&gt;L.P. Brezny, &lt;em&gt;Gun Digest Book of Shotgunning&lt;/em&gt;&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;Python/Scipy ODE Integrators: &lt;a href="https://baud.rs/dswIuo"&gt;scipy.integrate.solve_ivp&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/H829iA"&gt;Chuck Hawks’ Shotgun Ballistics Resource&lt;/a&gt;  &lt;/li&gt;
&lt;li&gt;&lt;a href="https://baud.rs/V26oDO"&gt;Ballistics Science (Wikipedia)&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;</description><category>#00 buckshot</category><category>ammunition</category><category>ballistics</category><category>ballistics simulation</category><category>code walkthrough</category><category>computational modeling</category><category>drag</category><category>external ballistics</category><category>external forces</category><category>gravity</category><category>matplotlib</category><category>muzzle velocity</category><category>numpy</category><category>ode solver</category><category>pellet spread</category><category>pellet trajectory</category><category>physics</category><category>programming</category><category>projectile motion</category><category>python</category><category>randomness</category><category>scientific computing</category><category>scipy</category><category>shot pattern</category><category>shotgun</category><category>shotgun choke</category><category>simulation</category><category>target pattern</category><category>visualization</category><category>wind modeling</category><guid>https://tinycomputers.io/posts/simulating-buckshot-spread-a-deep-dive-with-python-and-odes.html</guid><pubDate>Fri, 09 May 2025 00:12:22 GMT</pubDate></item></channel></rss>