<?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 physics)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/physics.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Original site content © 2022–2026 Tiny Machines Workshop, LLC, except where otherwise noted. Some rights reserved.</copyright><lastBuildDate>Sat, 29 Aug 2026 23:17:12 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>The Bullet Doesn't Go Where You Point It</title><link>https://tinycomputers.io/posts/the-bullet-doesnt-go-where-you-point-it.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/the-bullet-doesnt-go-where-you-point-it_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;20 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h3&gt;The bugs were never in the physics&lt;/h3&gt;
&lt;p&gt;Here is the thing I did not expect when I started, and which I suspect generalizes far past ballistics: in something like ninety thousand lines of Rust, almost none of the bugs worth remembering have been physics bugs. The equations are in textbooks. They have been in textbooks for decades, they were checked by people better at this than me, and when I have implemented one incorrectly the error was usually so large that it announced itself immediately.&lt;/p&gt;
&lt;p&gt;The bugs are in the &lt;em&gt;conventions&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;Consider a wind direction. A number of degrees — obviously. Except: is that the direction the wind is blowing &lt;em&gt;toward&lt;/em&gt;, or the direction it is coming &lt;em&gt;from&lt;/em&gt;? Meteorology says from. Is zero degrees a headwind, a tailwind, or due north? Is it measured relative to the direction you are shooting, or is it a compass bearing that has to be resolved against your shot azimuth before it means anything? Every one of those is a defensible choice, several of them are in active use by real tools, and each pairing produces a number that looks completely reasonable and is wrong by up to a hundred percent of the wind effect. The engine's answer is wind-&lt;em&gt;from&lt;/em&gt;, zero degrees is a headwind — and that sentence is now welded into the help text, the API docs, and a shared constant, because the one time the convention drifted between two subcommands, both of them looked right in isolation.&lt;/p&gt;
&lt;p&gt;Units are the same disease. Inches or millimeters for the height of the scope above the bore. Feet per second or meters per second. Grains — a unit of mass equal to one seven-thousandth of a pound, used for bullets and gunpowder and essentially nothing else on Earth. Minutes of angle or milliradians for the correction you dial into your scope, where a minute of angle is &lt;em&gt;approximately&lt;/em&gt; an inch at a hundred yards, and the approximation has cost people their afternoons. None of this is intellectually difficult. All of it is a place where a plausible number comes out and nobody catches it, which is precisely the definition of the expensive kind of bug.&lt;/p&gt;
&lt;h3&gt;Testing physics you cannot check by hand&lt;/h3&gt;
&lt;p&gt;Which raises the real problem: how do you test this? For a normal library you assert that the function returns the right answer. Here, nobody knows the right answer. The whole point of the engine is to compute a number I cannot compute another way — and if I could hand-verify the output, I would not have needed to write it.&lt;/p&gt;
&lt;p&gt;The usual answer is a pile of regression tests, and there is one: roughly fourteen hundred of them. But a regression test only tells you that today's answer matches the answer you wrote down on the day you wrote the test, which may have been wrong. What I have found much more valuable is a set of ten fuzzing harnesses that do not look for crashes so much as for &lt;em&gt;lies&lt;/em&gt; — properties that must hold for any input at all, checked against millions of inputs nobody thought to write down.&lt;/p&gt;
&lt;p&gt;Some of them have an actual oracle. Turn the drag off — set the ballistic coefficient absurdly high so air resistance becomes negligible — and a projectile is just a parabola, with a closed-form solution any physics undergraduate can produce: the drop at distance &lt;em&gt;x&lt;/em&gt; is exactly ½g(x/v)². So one harness generates arbitrary velocities, runs the full production solver, and demands that its answer match the exact analytic parabola. If the integrator, the step size, or the coordinate handling is subtly wrong, no amount of physics-modeling opinion can hide it, because for that one degenerate case the truth is known.&lt;/p&gt;
&lt;p&gt;Others check symmetry. Switch off spin drift, Magnus, and Coriolis so that the only thing pushing the bullet sideways is wind, then mirror the wind: the drift must mirror too, exactly. Any asymmetry left over is a bug, because there is nothing left in the model that could physically prefer one side. Others check monotonicity — a claim like "at a fixed distance, a bullet with a higher ballistic coefficient must not drop &lt;em&gt;more&lt;/em&gt;." No exact answer required; only a direction of change, which is often the strongest statement you can actually make about a system this tangled.&lt;/p&gt;
&lt;p&gt;And my favorite links the &lt;em&gt;previous released version of the engine&lt;/em&gt; into the same binary as the current one and runs both. Not to demand identical output — the numbers are supposed to change when the physics improves — but to compare their qualitative responses, so that if a refactor quietly inverts how drop responds to a change in ballistic coefficient, the difference surfaces against the version that shipped rather than against my expectations. Which, being the person who wrote the refactor, are the least trustworthy oracle in the building. This trick has one delightful practical wrinkle: linking two versions of the same Rust crate into one executable works fine right up until both of them export identical &lt;code&gt;#[no_mangle]&lt;/code&gt; C symbols for the FFI layer and the linker gives up. The fix is a feature flag that turns the C ABI off on one of the two edges — a small ugly accommodation for a technique that has more than paid for it.&lt;/p&gt;
&lt;h3&gt;Thirteen platforms, because I am like this&lt;/h3&gt;
&lt;p&gt;One more testing note disguised as a shipping note. Each release goes out as native binaries for thirteen platforms — Linux on x86-64, Arm64, RISC-V and 64-bit MIPS; macOS on Intel and Apple Silicon; Windows; and FreeBSD, OpenBSD and NetBSD each on both x86-64 and Arm64 — built by &lt;a href="https://tinycomputers.io/posts/multi-operating-system-and-multi-architecture-build-orchestration-system.html"&gt;a small fleet of single-board computers&lt;/a&gt; that boots a real BSD guest for the length of one compile and then puts it away again. There is no business case for most of that; there is not, so far as I know, a single OpenBSD/Arm64 user waiting on a ballistics calculator.&lt;/p&gt;
&lt;p&gt;But it earns its place in this particular story, because portability is a bug detector. An engine that produces the same numbers on big-endian 64-bit MIPS as it does on Apple Silicon is an engine carrying fewer accidental assumptions about how its floats are laid out and how its integers are ordered. Thirteen platforms is thirteen independent opportunities to be told I assumed something I had not noticed assuming.&lt;/p&gt;
&lt;h3&gt;Solving it backwards&lt;/h3&gt;
&lt;p&gt;Everything so far is the &lt;em&gt;forward&lt;/em&gt; problem: here is the load, tell me where it hits. That is the well-posed direction, and honestly, it is a solved problem — there are good calculators, mine included, and they mostly agree.&lt;/p&gt;
&lt;p&gt;It is also not the question anybody actually has. Nobody at a loading bench wonders where their known load will land. They wonder the other way around: &lt;em&gt;I want it to land here — what gets me there?&lt;/em&gt; Which powder, and how many grains of it, produces the velocity I want? What barrel twist rate will stabilize this bullet? How long should I seat this cartridge? What is this bullet's ballistic coefficient really, derived from its measured dimensions, rather than the optimistic number on the box?&lt;/p&gt;
&lt;p&gt;Those are inverse problems, and inverse problems are where it stops being a matter of running the textbook forward. They generally have no closed form. You solve them by searching — running the forward model over and over inside a root-find or an optimizer, hunting for the input that produces the output that was asked for — and the search is only as trustworthy as the model inside its loop, which is the entire ballgame. A backward solver built on a mediocre forward model does not fail loudly. It produces a confident, specific, plausible number that is wrong, which is worse than useless in a domain where people act on the answer.&lt;/p&gt;
&lt;p&gt;So that is where the real work went: the drag model those searches lean on is calibrated against Doppler radar measurements of how actual bullets actually decelerate — including through the transonic region, where the classical scale-a-reference-curve assumption is at its weakest — rather than assuming every projectile behaves like a reference shape from a century ago.&lt;/p&gt;
&lt;h3&gt;The split, stated plainly&lt;/h3&gt;
&lt;p&gt;I would rather just tell you the arrangement than have you discover it.&lt;/p&gt;
&lt;p&gt;The engine is free and open: the library, the CLI, the WebAssembly build, the Python and Ruby bindings, the whole forward solver, the fuzzing harnesses, all of it. Compute trajectories, run dispersions, true your velocity against real measured drops, build hold tables, ship it inside something of your own. No account, no key, no telemetry. That is &lt;a href="https://ballistics.rs/"&gt;ballistics.rs&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The four inverse solvers are the one piece behind a subscription, at &lt;a href="https://ballisticsinsight.com/"&gt;Ballistics Insight&lt;/a&gt;. They run as a service — in a browser, or from the same CLI once you have logged in:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;ballistics login
ballistics recommend-powder --cartridge "308 Winchester" \
    --bullet-weight 175 --desired-velocity 2600
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;They are the paid piece because they are the part with real ongoing cost behind them — the measured drag data the searches depend on, and the compute to run them — and because a project like this is more likely to still exist in five years if some part of it pays for itself. If you shoot, they may be worth it to you. If you are here for the numerical methods and the fuzzing and the pile of BSD binaries, that is all free and I am glad you came.&lt;/p&gt;
&lt;p&gt;One honest note if you do use the powder solver: what it returns is a &lt;em&gt;starting&lt;/em&gt; point, not a recipe. It is where to begin working a load up, carefully, against published load data, over a chronograph, in the slow and boring and safe way. The physics is good. The physics is not your reloading manual and will not pretend to be.&lt;/p&gt;
&lt;p&gt;The bullet still does not go where you point it. But it goes somewhere entirely predictable, and I find that a much more interesting fact than it first appears.&lt;/p&gt;</description><category>ballistics</category><category>cli</category><category>cross-compilation</category><category>external ballistics</category><category>fuzzing</category><category>numerical methods</category><category>ode solver</category><category>physics</category><category>property testing</category><category>rust</category><category>wasm</category><guid>https://tinycomputers.io/posts/the-bullet-doesnt-go-where-you-point-it.html</guid><pubDate>Sat, 01 Aug 2026 17:00: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 and 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>Transfer Learning for Predictive Custom Drag Modeling: Automated Generation of Drag Coefficient Curves Using Multi-Modal AI</title><link>https://tinycomputers.io/posts/transfer-learning-for-predictive-custom-drag-modeling-automated-generation-of-drag-coefficient-curves-using-multi-modal-ai.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/transfer-learning-for-predictive-custom-drag-modeling-automated-generation-of-drag-coefficient-curves-using-multi-modal-ai_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;TL;DR&lt;/h3&gt;
&lt;p&gt;We built a neural network that predicts full drag coefficient curves (41 Mach points from 0.5 to 4.5) for rifle bullets using only basic specifications like weight, caliber, and ballistic coefficient. The system achieves 3.15% mean absolute error and has been serving predictions in production since September 2025. This post walks through the technical implementation details, architecture decisions, and lessons learned building a real-world ML system for ballistic physics.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Read the full whitepaper: &lt;a href="https://tinycomputers.io/data/cdm_transfer_learning.pdf"&gt;Transfer Learning for Predictive Custom Drag Modeling&lt;/a&gt; (17 pages)&lt;/p&gt;
&lt;/blockquote&gt;
&lt;hr&gt;
&lt;h3&gt;The Problem: Drag Curves Are Scarce, But Critical&lt;/h3&gt;
&lt;p&gt;If you've ever built a ballistic calculator, you know the challenge: accurate drag modeling is everything. Standard drag models (G1, G7, G8) work okay for "average" bullets, but modern precision shooting demands better. Custom Drag Models (CDMs), full drag coefficient curves measured with doppler radar, are the gold standard. They capture the unique aerodynamic signature of each bullet design.&lt;/p&gt;
&lt;p&gt;The catch? Getting a CDM requires:
- Access to a doppler radar range (≈$500K+ equipment)
- Firing 50-100 rounds at various velocities
- Expert analysis to process the raw data
- Cost: $5,000-$15,000 per bullet&lt;/p&gt;
&lt;p&gt;For manufacturers like Hornady and Lapua, this is routine. For smaller manufacturers or custom bullet makers? Not happening. We had 641 bullets with real radar-measured CDMs and thousands of bullets with only basic specs. Could we use machine learning to bridge the gap?&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;The Vision: Transfer Learning from Radar Data&lt;/h3&gt;
&lt;p&gt;The core insight: bullets with similar physical characteristics have similar drag curves. A 168gr .308 boattail match bullet from Manufacturer A will drag similarly to one from Manufacturer B. We could train a neural network on our 641 radar-measured bullets and use transfer learning to predict CDMs for bullets we've never measured.&lt;/p&gt;
&lt;p&gt;But we faced an immediate data problem: 641 samples isn't much for deep learning. Enter synthetic data augmentation.&lt;/p&gt;
&lt;h3&gt;Part 1: Automating Data Extraction with Claude Vision&lt;/h3&gt;
&lt;p&gt;Applied Ballistics publishes ballistic data for 704+ bullets as JPEG images. Manual data entry would take 1,408 hours (704 bullets × 2 hours each). We needed automation.&lt;/p&gt;
&lt;h4&gt;The Vision Processing Pipeline&lt;/h4&gt;
&lt;p&gt;We built an extraction pipeline using Claude 3.5 Sonnet's vision capabilities:&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;anthropic&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;base64&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;pathlib&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;Path&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;extract_bullet_data&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Extract bullet specifications from AB datasheet JPEG."""&lt;/span&gt;
    &lt;span class="n"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;anthropic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Anthropic&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;api_key&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;os&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;environ&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s2"&gt;"ANTHROPIC_API_KEY"&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="c1"&gt;# Load and encode image&lt;/span&gt;
    &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;image_path&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s2"&gt;"rb"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;image_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;base64&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;standard_b64encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;read&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"utf-8"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Vision extraction prompt&lt;/span&gt;
    &lt;span class="n"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;client&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;create&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"claude-3-5-sonnet-20241022"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;max_tokens&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1024&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[{&lt;/span&gt;
            &lt;span class="s2"&gt;"role"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"user"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s2"&gt;"content"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
                        &lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"base64"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s2"&gt;"media_type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"image/jpeg"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                        &lt;span class="s2"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;image_data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;},&lt;/span&gt;
                &lt;span class="p"&gt;{&lt;/span&gt;
                    &lt;span class="s2"&gt;"type"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                    &lt;span class="s2"&gt;"text"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"""Extract the following from this Applied Ballistics bullet datasheet:&lt;/span&gt;
&lt;span class="s2"&gt;                    - Caliber (inches, decimal format)&lt;/span&gt;
&lt;span class="s2"&gt;                    - Bullet weight (grains)&lt;/span&gt;
&lt;span class="s2"&gt;                    - G1 Ballistic Coefficient&lt;/span&gt;
&lt;span class="s2"&gt;                    - G7 Ballistic Coefficient&lt;/span&gt;
&lt;span class="s2"&gt;                    - Bullet length (inches, if visible)&lt;/span&gt;
&lt;span class="s2"&gt;                    - Ogive radius (calibers, if visible)&lt;/span&gt;

&lt;span class="s2"&gt;                    Return as JSON with keys: caliber, weight_gr, bc_g1, bc_g7, length_in, ogive_radius_cal"""&lt;/span&gt;
                &lt;span class="p"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="p"&gt;}]&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Parse response&lt;/span&gt;
    &lt;span class="n"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;json&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;loads&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;message&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;content&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;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="c1"&gt;# Physics validation&lt;/span&gt;
    &lt;span class="n"&gt;validate_bullet_physics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&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;data&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;validate_bullet_physics&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Sanity checks for extracted data."""&lt;/span&gt;
    &lt;span class="n"&gt;caliber&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'weight_gr'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

    &lt;span class="c1"&gt;# Caliber bounds&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mf"&gt;0.172&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;caliber&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.50&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;"Invalid caliber: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;caliber&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;

    &lt;span class="c1"&gt;# Weight-to-caliber ratio (sectional density proxy)&lt;/span&gt;
    &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weight&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;caliber&lt;/span&gt;  &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mf"&gt;0.5&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;ratio&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;2.0&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;"Implausible weight for caliber: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;weight&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;gr @ &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;caliber&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;in"&lt;/span&gt;

    &lt;span class="c1"&gt;# BC sanity&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'bc_g1'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;1.2&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;"Invalid G1 BC: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'bc_g1'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
    &lt;span class="k"&gt;assert&lt;/span&gt; &lt;span class="mf"&gt;0.1&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'bc_g7'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;0.9&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;"Invalid G7 BC: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;data&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'bc_g7'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="Vision Processing Pipeline" src="https://tinycomputers.io/images/vision_pipeline.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 2: Claude Vision extraction pipeline - from JPEG datasheets to structured bullet specifications&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Results:
- 704/704 successful extractions (100% success rate)
- 2.3 seconds per bullet (average)
- 27 minutes total vs. 1,408 hours manual
- 99.97% time savings&lt;/p&gt;
&lt;p&gt;We validated against a manually-verified subset of 50 bullets:
- 100% match on caliber
- 98% match on weight (±0.5 grain tolerance)
- 96% match on BC values (±0.002 tolerance)&lt;/p&gt;
&lt;p&gt;The vision model occasionally struggled with hand-drawn or low-quality scans, but the physics validation caught these errors before they corrupted our dataset.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Part 2: Generating Synthetic CDM Curves&lt;/h3&gt;
&lt;p&gt;Now we had 704 bullets with BC values but no full CDM curves. We needed to synthesize them.&lt;/p&gt;
&lt;h4&gt;The BC-to-CDM Transformation Algorithm&lt;/h4&gt;
&lt;p&gt;The relationship between ballistic coefficient and drag coefficient is straightforward:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;BC = m / (C_d × d²)

Rearranging:
C_d(M) = m / (BC(M) × d²)
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;But BC values are typically single scalars, not curves. We developed a 5-step hybrid algorithm combining standard drag model references with BC-derived corrections:&lt;/p&gt;
&lt;h5&gt;Step 1: Base Reference Curve&lt;/h5&gt;
&lt;p&gt;Start with the G7 standard drag curve as a baseline (better for modern boattail bullets than G1):&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_g7_reference_curve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;mach_points&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;ndarray&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;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""G7 standard drag curve from McCoy (1999)."""&lt;/span&gt;
    &lt;span class="c1"&gt;# Precomputed G7 curve at 41 Mach points&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;interpolate_standard_curve&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;"G7"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mach_points&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h5&gt;Step 2: BC-Based Scaling&lt;/h5&gt;
&lt;p&gt;Scale the reference curve using extracted BC values:&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;scale_by_bc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_base&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;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;bc_actual&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;bc_reference&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.221&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;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Scale drag curve to match actual BC.&lt;/span&gt;

&lt;span class="sd"&gt;    BC_G7_ref = 0.221 (G7 standard projectile)&lt;/span&gt;
&lt;span class="sd"&gt;    """&lt;/span&gt;
    &lt;span class="n"&gt;scaling_factor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bc_reference&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;bc_actual&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cd_base&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;scaling_factor&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h5&gt;Step 3: Multi-Regime Interpolation&lt;/h5&gt;
&lt;p&gt;When both G1 and G7 BCs are available, blend them based on Mach regime:&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;blend_drag_models&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;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cd_g1&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;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cd_g7&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;ndarray&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;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Blend G1 and G7 curves based on flight regime.&lt;/span&gt;

&lt;span class="sd"&gt;    - Supersonic (M &amp;gt; 1.2): Use G1 (better for shock wave region)&lt;/span&gt;
&lt;span class="sd"&gt;    - Transonic (0.8 &amp;lt; M &amp;lt; 1.2): Cubic spline interpolation&lt;/span&gt;
&lt;span class="sd"&gt;    - Subsonic (M &amp;lt; 0.8): Use G7 (better for low-speed)&lt;/span&gt;
&lt;span class="sd"&gt;    """&lt;/span&gt;
    &lt;span class="n"&gt;cd_blended&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;mach&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="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nb"&gt;enumerate&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="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Supersonic: G1 better captures shock effects&lt;/span&gt;
            &lt;span class="n"&gt;cd_blended&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;cd_g1&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="k"&gt;elif&lt;/span&gt; &lt;span class="n"&gt;M&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Subsonic: G7 better for boattail bullets&lt;/span&gt;
            &lt;span class="n"&gt;cd_blended&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;cd_g7&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="k"&gt;else&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Transonic: smooth interpolation&lt;/span&gt;
            &lt;span class="n"&gt;t&lt;/span&gt; &lt;span class="o"&gt;=&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;0.8&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.4&lt;/span&gt;  &lt;span class="c1"&gt;# Normalize to [0, 1]&lt;/span&gt;
            &lt;span class="n"&gt;cd_blended&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;cubic_interpolate&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_g7&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;cd_g1&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&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_blended&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h5&gt;Step 4: Transonic Peak Generation&lt;/h5&gt;
&lt;p&gt;Model the transonic drag spike using a Gaussian kernel:&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;add_transonic_peak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_base&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;ndarray&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;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                       &lt;span class="n"&gt;bc_g1&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;bc_g7&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;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Add realistic transonic drag spike.&lt;/span&gt;

&lt;span class="sd"&gt;    Peak amplitude calibrated from BC ratio (G1 worse than G7 in transonic).&lt;/span&gt;
&lt;span class="sd"&gt;    """&lt;/span&gt;
    &lt;span class="c1"&gt;# Estimate peak amplitude from BC discrepancy&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;bc_g1&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;bc_g7&lt;/span&gt;
    &lt;span class="n"&gt;peak_amplitude&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bc_ratio&lt;/span&gt; &lt;span class="o"&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="c1"&gt;# Empirically tuned&lt;/span&gt;

    &lt;span class="c1"&gt;# Gaussian centered at critical Mach&lt;/span&gt;
    &lt;span class="n"&gt;M_crit&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;sigma&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.15&lt;/span&gt;

    &lt;span class="n"&gt;transonic_spike&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;peak_amplitude&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;exp&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;mach&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;M_crit&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="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;sigma&lt;/span&gt;  &lt;span class="mi"&gt;2&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_base&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;transonic_spike&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h5&gt;Step 5: Monotonicity Enforcement&lt;/h5&gt;
&lt;p&gt;Apply Savitzky-Golay smoothing to prevent unphysical oscillations:&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.signal&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;savgol_filter&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;enforce_smoothness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_curve&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;ndarray&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_length&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&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;polyorder&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&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;np&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ndarray&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Smooth drag curve while preserving transonic peak.&lt;/span&gt;

&lt;span class="sd"&gt;    Savitzky-Golay filter preserves peak shape better than moving average.&lt;/span&gt;
&lt;span class="sd"&gt;    """&lt;/span&gt;
    &lt;span class="c1"&gt;# Must have odd window length&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;window_length&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;window_length&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;return&lt;/span&gt; &lt;span class="n"&gt;savgol_filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_curve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;window_length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;polyorder&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'nearest'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Validation Against Ground Truth&lt;/h4&gt;
&lt;p&gt;We validated synthetic curves against 127 bullets where both BC values and full CDM curves were available:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;th&gt;Notes&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Mean Absolute Error&lt;/td&gt;
&lt;td&gt;3.2%&lt;/td&gt;
&lt;td&gt;Across all Mach points&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transonic Error&lt;/td&gt;
&lt;td&gt;4.8%&lt;/td&gt;
&lt;td&gt;Mach 0.8-1.2 (most challenging)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Supersonic Error&lt;/td&gt;
&lt;td&gt;2.1%&lt;/td&gt;
&lt;td&gt;Mach 1.5-3.0 (best performance)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Shape Correlation&lt;/td&gt;
&lt;td&gt;r = 0.984&lt;/td&gt;
&lt;td&gt;Pearson correlation&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The synthetic curves satisfied all physics constraints:
- Monotonic decrease in supersonic regime
- Realistic transonic peaks (1.3-2.0× baseline)
- Smooth transitions between regimes&lt;/p&gt;
&lt;p&gt;&lt;img alt="Physics Validation" src="https://tinycomputers.io/images/physics_validation.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 3: Validation of synthetic CDM curves against ground truth radar measurements&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Total training data: 1,345 bullets (704 synthetic + 641 real), 2.1x data augmentation.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Part 3: Architecture Exploration&lt;/h3&gt;
&lt;p&gt;With data ready, we explored four neural architectures:&lt;/p&gt;
&lt;h4&gt;1. Multi-Layer Perceptron (Baseline)&lt;/h4&gt;
&lt;p&gt;Simple feedforward network:&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;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;torch.nn&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;nn&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;CDMPredictor&lt;/span&gt;&lt;span class="p"&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="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""MLP for CDM prediction: 13 features → 41 Cd values."""&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;dropout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;float&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;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;network&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;Sequential&lt;/span&gt;&lt;span class="p"&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;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&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;ReLU&lt;/span&gt;&lt;span class="p"&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;Dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dropout&lt;/span&gt;&lt;span class="p"&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;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&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;ReLU&lt;/span&gt;&lt;span class="p"&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;Dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dropout&lt;/span&gt;&lt;span class="p"&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;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&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;ReLU&lt;/span&gt;&lt;span class="p"&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;Dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dropout&lt;/span&gt;&lt;span class="p"&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;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&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;ReLU&lt;/span&gt;&lt;span class="p"&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;Dropout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dropout&lt;/span&gt;&lt;span class="p"&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;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# Output: 41 Mach points&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;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;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&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;network&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;/pre&gt;&lt;/div&gt;

&lt;p&gt;Input Features (13 total):&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="n"&gt;features&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
    &lt;span class="s1"&gt;'caliber'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;           &lt;span class="c1"&gt;# inches&lt;/span&gt;
    &lt;span class="s1"&gt;'weight_gr'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;         &lt;span class="c1"&gt;# grains&lt;/span&gt;
    &lt;span class="s1"&gt;'bc_g1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# G1 ballistic coefficient&lt;/span&gt;
    &lt;span class="s1"&gt;'bc_g7'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;            &lt;span class="c1"&gt;# G7 ballistic coefficient&lt;/span&gt;
    &lt;span class="s1"&gt;'length_in'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;# bullet length (imputed if missing)&lt;/span&gt;
    &lt;span class="s1"&gt;'ogive_radius_cal'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# ogive radius in calibers&lt;/span&gt;
    &lt;span class="s1"&gt;'meplat_diam_in'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# meplat diameter&lt;/span&gt;
    &lt;span class="s1"&gt;'boat_tail_angle'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# boattail angle (degrees)&lt;/span&gt;
    &lt;span class="s1"&gt;'bearing_length'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# bearing surface length&lt;/span&gt;
    &lt;span class="s1"&gt;'sectional_density'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;# weight / caliber²&lt;/span&gt;
    &lt;span class="s1"&gt;'form_factor_g1'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# i / BC_G1&lt;/span&gt;
    &lt;span class="s1"&gt;'form_factor_g7'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;   &lt;span class="c1"&gt;# i / BC_G7&lt;/span&gt;
    &lt;span class="s1"&gt;'length_to_diameter'&lt;/span&gt; &lt;span class="c1"&gt;# L/D ratio&lt;/span&gt;
&lt;span class="p"&gt;]&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="Network Architecture" src="https://tinycomputers.io/images/network_architecture.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 4: MLP architecture - 13 input features through 4 hidden layers to 41 output Mach points&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;2. Physics-Informed Neural Network (PINN)&lt;/h4&gt;
&lt;p&gt;Added physics loss term enforcing drag model constraints:&lt;/p&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;PINN_CDMPredictor&lt;/span&gt;&lt;span class="p"&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="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Physics-Informed NN with drag equation constraints."""&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="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="c1"&gt;# Same architecture as MLP&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;network&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;build_mlp_network&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;physics_loss&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;cd_pred&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;Tensor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;features&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;Tensor&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;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="o"&gt;-&amp;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="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;"""Enforce physics constraints on predictions.&lt;/span&gt;

&lt;span class="sd"&gt;        Constraints:&lt;/span&gt;
&lt;span class="sd"&gt;        1. Drag increases with Mach in subsonic&lt;/span&gt;
&lt;span class="sd"&gt;        2. Transonic peak exists near M=1&lt;/span&gt;
&lt;span class="sd"&gt;        3. Monotonic decrease in supersonic&lt;/span&gt;
&lt;span class="sd"&gt;        """&lt;/span&gt;
        &lt;span class="c1"&gt;# Constraint 1: Subsonic gradient&lt;/span&gt;
        &lt;span class="n"&gt;subsonic_mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mach&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;
        &lt;span class="n"&gt;subsonic_cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cd_pred&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;subsonic_mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;subsonic_grad&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;diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;subsonic_cd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;subsonic_violation&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;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;subsonic_grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Penalize decreases&lt;/span&gt;

        &lt;span class="c1"&gt;# Constraint 2: Transonic peak&lt;/span&gt;
        &lt;span class="n"&gt;transonic_mask&lt;/span&gt; &lt;span class="o"&gt;=&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;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;transonic_cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cd_pred&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;transonic_mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;peak_violation&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;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;transonic_cd&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Must exceed 1.1&lt;/span&gt;

        &lt;span class="c1"&gt;# Constraint 3: Supersonic monotonicity&lt;/span&gt;
        &lt;span class="n"&gt;supersonic_mask&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;mach&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mf"&gt;1.5&lt;/span&gt;
        &lt;span class="n"&gt;supersonic_cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cd_pred&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;supersonic_mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;supersonic_grad&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;diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;supersonic_cd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;supersonic_violation&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;relu&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;supersonic_grad&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Penalize increases&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;subsonic_violation&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;peak_violation&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;supersonic_violation&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;total_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cd_true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;features&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;lambda_physics&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Combined data + physics loss."""&lt;/span&gt;
    &lt;span class="n"&gt;data_loss&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;MSELoss&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;&lt;span class="n"&gt;cd_pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cd_true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;physics_loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;physics_loss&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;features&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;data_loss&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;lambda_physics&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;physics_loss&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Result: Over-regularization. Physics loss was too strict, preventing the model from learning subtle variations. Performance degraded to 4.86% MAE.&lt;/p&gt;
&lt;h4&gt;3. Transformer Architecture&lt;/h4&gt;
&lt;p&gt;Treated the 41 Mach points as a sequence:&lt;/p&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;TransformerCDM&lt;/span&gt;&lt;span class="p"&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="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Transformer encoder for sequence-to-sequence CDM prediction."""&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;d_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;128&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;nhead&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_layers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&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;feature_embedding&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;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;d_model&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;encoder_layer&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;TransformerEncoderLayer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;d_model&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;d_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;nhead&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;nhead&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;dim_feedforward&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;512&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;dropout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;0.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;transformer&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;TransformerEncoder&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;encoder_layer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;num_layers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;num_layers&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;output_head&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;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;d_model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;41&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;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;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# x: [batch, 13]&lt;/span&gt;
        &lt;span class="n"&gt;embedded&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;feature_embedding&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="c1"&gt;# [batch, d_model]&lt;/span&gt;
        &lt;span class="n"&gt;embedded&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;embedded&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;unsqueeze&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;expand&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;41&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="c1"&gt;# [batch, 41, d_model]&lt;/span&gt;

        &lt;span class="n"&gt;transformed&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;transformer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;embedded&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# [batch, 41, d_model]&lt;/span&gt;

        &lt;span class="n"&gt;cd_pred&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;output_head&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;transformed&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="p"&gt;:,&lt;/span&gt; &lt;span class="p"&gt;:])&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;squeeze&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="c1"&gt;# [batch, 41]&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;cd_pred&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Result: Mismatch between architecture and problem. CDM prediction isn't a sequence modeling task; Mach points are independent given bullet features. Performance: 6.05% MAE.&lt;/p&gt;
&lt;h4&gt;4. Neural ODE&lt;/h4&gt;
&lt;p&gt;Attempted to model drag as a continuous ODE:&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;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="k"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;DragODE&lt;/span&gt;&lt;span class="p"&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="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Neural ODE for continuous drag modeling."""&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;hidden_dim&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;64&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;net&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;Sequential&lt;/span&gt;&lt;span class="p"&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;Linear&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="mi"&gt;13&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hidden_dim&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;  &lt;span class="c1"&gt;# Mach + features&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;Tanh&lt;/span&gt;&lt;span class="p"&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;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hidden_dim&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;hidden_dim&lt;/span&gt;&lt;span class="p"&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;Tanh&lt;/span&gt;&lt;span class="p"&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;Linear&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;hidden_dim&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="c1"&gt;# dCd/dM&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;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;state&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="c1"&gt;# t: current Mach number&lt;/span&gt;
        &lt;span class="c1"&gt;# state: [Cd, features...]&lt;/span&gt;
        &lt;span class="k"&gt;return&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;net&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;cat&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;state&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="k"&gt;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;predict_cdm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mach_points&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Integrate ODE to get Cd curve."""&lt;/span&gt;
    &lt;span class="n"&gt;initial_cd&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="mf"&gt;0.5&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;  &lt;span class="c1"&gt;# Initial guess&lt;/span&gt;
    &lt;span class="n"&gt;state&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;cat&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;initial_cd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

    &lt;span class="n"&gt;solution&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;ode_func&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;state&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mach_points&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;solution&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="c1"&gt;# Extract Cd values&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Result: Failed to converge due to dimension mismatch errors and extreme sensitivity to initial conditions. Abandoned after 2 days of debugging.&lt;/p&gt;
&lt;h4&gt;Architecture Comparison Results&lt;/h4&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Architecture&lt;/th&gt;
&lt;th&gt;MAE&lt;/th&gt;
&lt;th&gt;Smoothness&lt;/th&gt;
&lt;th&gt;Shape Correlation&lt;/th&gt;
&lt;th&gt;Status&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;MLP Baseline&lt;/td&gt;
&lt;td&gt;3.66%&lt;/td&gt;
&lt;td&gt;90.05%&lt;/td&gt;
&lt;td&gt;0.9380&lt;/td&gt;
&lt;td&gt;✅ Best&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Physics-Informed NN&lt;/td&gt;
&lt;td&gt;4.86%&lt;/td&gt;
&lt;td&gt;64.02%&lt;/td&gt;
&lt;td&gt;0.8234&lt;/td&gt;
&lt;td&gt;❌ Over-regularized&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Transformer&lt;/td&gt;
&lt;td&gt;6.05%&lt;/td&gt;
&lt;td&gt;56.83%&lt;/td&gt;
&lt;td&gt;0.7891&lt;/td&gt;
&lt;td&gt;❌ Poor fit&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Neural ODE&lt;/td&gt;
&lt;td&gt;---&lt;/td&gt;
&lt;td&gt;---&lt;/td&gt;
&lt;td&gt;---&lt;/td&gt;
&lt;td&gt;❌ Failed to converge&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;&lt;img alt="Architecture Comparison" src="https://tinycomputers.io/images/architecture_comparison.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 5: Performance comparison across four neural architectures - MLP baseline wins&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Key Insight: Simple MLP with dropout outperformed complex physics-constrained models. The training data already contained sufficient physics signal; explicit constraints hurt generalization.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Part 4: Production System Design&lt;/h3&gt;
&lt;p&gt;The POC model (3.66% MAE) validated the approach. Now we needed production hardening.&lt;/p&gt;
&lt;h4&gt;Training Pipeline Improvements&lt;/h4&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;pytorch_lightning&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;pl&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;torch.utils.data&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;DataLoader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;TensorDataset&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;ProductionCDMModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LightningModule&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Production-ready CDM predictor with monitoring."""&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;learning_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight_decay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-4&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;save_hyperparameters&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;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CDMPredictor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dropout&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;span class="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;learning_rate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;learning_rate&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;weight_decay&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;weight_decay&lt;/span&gt;

        &lt;span class="c1"&gt;# Metrics tracking&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;train_mae&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;val_mae&lt;/span&gt; &lt;span class="o"&gt;=&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;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;x&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="k"&gt;return&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;model&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;def&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;training_step&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;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cd_true&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;
        &lt;span class="n"&gt;cd_pred&lt;/span&gt; &lt;span class="o"&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;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Weighted MSE loss (emphasize transonic region)&lt;/span&gt;
        &lt;span class="n"&gt;weights&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;_get_mach_weights&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
        &lt;span class="n"&gt;loss&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_pred&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cd_true&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;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Metrics&lt;/span&gt;
        &lt;span class="n"&gt;mae&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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_pred&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cd_true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'train_loss'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loss&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'train_mae'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mae&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;loss&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;validation_step&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;batch&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_idx&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cd_true&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;batch&lt;/span&gt;
        &lt;span class="n"&gt;cd_pred&lt;/span&gt; &lt;span class="o"&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;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;loss&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;MSELoss&lt;/span&gt;&lt;span class="p"&gt;()(&lt;/span&gt;&lt;span class="n"&gt;cd_pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cd_true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;mae&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;abs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_pred&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;cd_true&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'val_loss'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;loss&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'val_mae'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mae&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Physics validation&lt;/span&gt;
        &lt;span class="n"&gt;smoothness&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;_calculate_smoothness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_pred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;transonic_quality&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;_check_transonic_peak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_pred&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'smoothness'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;smoothness&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;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'transonic_quality'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;transonic_quality&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;loss&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;configure_optimizers&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;optimizer&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;optim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;AdamW&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;parameters&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="n"&gt;lr&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;learning_rate&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;weight_decay&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;weight_decay&lt;/span&gt;
        &lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;scheduler&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;optim&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lr_scheduler&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ReduceLROnPlateau&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
            &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'min'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="n"&gt;factor&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="n"&gt;patience&lt;/span&gt;&lt;span class="o"&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;verbose&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s1"&gt;'optimizer'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;optimizer&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'lr_scheduler'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;scheduler&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'monitor'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'val_loss'&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;_get_mach_weights&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="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;"""Weight transonic region more heavily."""&lt;/span&gt;
        &lt;span class="n"&gt;weights&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;ones&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;41&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;transonic_indices&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;mach_points&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;mach_points&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;transonic_indices&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="c1"&gt;# 2x weight in transonic&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;weights&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&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;_calculate_smoothness&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;cd_pred&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;"""Measure curve smoothness (low = better)."""&lt;/span&gt;
        &lt;span class="n"&gt;second_derivative&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;diff&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_pred&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n&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;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="k"&gt;return&lt;/span&gt; &lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mf"&gt;1.0&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;second_derivative&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;abs&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&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;_check_transonic_peak&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;cd_pred&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;"""Verify transonic peak exists and is realistic."""&lt;/span&gt;
        &lt;span class="n"&gt;transonic_mask&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;mach_points&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.8&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&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;mach_points&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="mf"&gt;1.2&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;peak_cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cd_pred&lt;/span&gt;&lt;span class="p"&gt;[:,&lt;/span&gt; &lt;span class="n"&gt;transonic_mask&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max&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="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;
        &lt;span class="n"&gt;baseline_cd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cd_pred&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="c1"&gt;# Subsonic baseline&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;peak_cd&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;baseline_cd&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Should be &amp;gt; 1.0&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Training Configuration&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Data preparation&lt;/span&gt;
&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;X_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prepare_features&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# 1,039 → 831 / 104 / 104&lt;/span&gt;
&lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;prepare_targets&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

&lt;span class="n"&gt;train_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TensorDataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_train&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_train&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;val_dataset&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;TensorDataset&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;X_val&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y_val&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;train_loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DataLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;train_dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shuffle&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;num_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;val_loader&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;DataLoader&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;val_dataset&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;batch_size&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;32&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;shuffle&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;num_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Model training&lt;/span&gt;
&lt;span class="n"&gt;model&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ProductionCDMModel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;learning_rate&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;weight_decay&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mf"&gt;1e-4&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;trainer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;Trainer&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;max_epochs&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;callbacks&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
        &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;EarlyStopping&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'val_loss'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;patience&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'min'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="n"&gt;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ModelCheckpoint&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;monitor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'val_mae'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;mode&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'min'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;save_top_k&lt;/span&gt;&lt;span class="o"&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;pl&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;callbacks&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;LearningRateMonitor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;logging_interval&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'epoch'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="n"&gt;accelerator&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'gpu'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="n"&gt;devices&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;log_every_n_steps&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="n"&gt;trainer&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;fit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;train_loader&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;val_loader&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;img alt="Training Convergence" src="https://tinycomputers.io/images/training_convergence.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 6: Training and validation loss convergence over 60 epochs&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Training Results:
- Converged at epoch 60 (early stopping)
- Final validation loss: 0.0023
- Production model MAE: 3.15% (13.9% improvement over POC)
- Smoothness: 88.81% (close to ground truth 89.6%)
- Shape correlation: 0.9545&lt;/p&gt;
&lt;p&gt;&lt;img alt="CDM Predictions" src="https://tinycomputers.io/images/cdm_predictions.png"&gt;
&lt;em&gt;Figure 7: Example predicted CDM curves compared to ground truth measurements&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;API Integration&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# ballistics/ml/cdm_transfer_learning.py&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;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nn"&gt;pickle&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;pathlib&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;Path&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;CDMTransferLearning&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Production CDM prediction service."""&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;model_path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;"models/cdm_transfer_learning/production_mlp.pkl"&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;model&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;_load_model&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_path&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;model&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;eval&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Feature statistics for normalization&lt;/span&gt;
        &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nb"&gt;open&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;model_path&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.pkl'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'_stats.pkl'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="s1"&gt;'rb'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;f&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;feature_stats&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;pickle&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&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;predict&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;bullet_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;"""Predict CDM curve from bullet specifications.&lt;/span&gt;

&lt;span class="sd"&gt;        Args:&lt;/span&gt;
&lt;span class="sd"&gt;            bullet_data: Dict with keys: caliber, weight_gr, bc_g1, bc_g7, etc.&lt;/span&gt;

&lt;span class="sd"&gt;        Returns:&lt;/span&gt;
&lt;span class="sd"&gt;            Dict with mach_numbers, drag_coefficients, validation_metrics&lt;/span&gt;
&lt;span class="sd"&gt;        """&lt;/span&gt;
        &lt;span class="c1"&gt;# Feature engineering&lt;/span&gt;
        &lt;span class="n"&gt;features&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;_extract_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;bullet_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;features_normalized&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;_normalize_features&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="c1"&gt;# Prediction&lt;/span&gt;
        &lt;span class="k"&gt;with&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;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
            &lt;span class="n"&gt;cd_pred&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;model&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;tensor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;features_normalized&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="c1"&gt;# Denormalize&lt;/span&gt;
        &lt;span class="n"&gt;cd_values&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cd_pred&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;numpy&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;

        &lt;span class="c1"&gt;# Validation&lt;/span&gt;
        &lt;span class="n"&gt;validation&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;_validate_prediction&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s1"&gt;'mach_numbers'&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;mach_points&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'drag_coefficients'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cd_values&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;tolist&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'source'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'ml_transfer_learning'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'method'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'mlp_prediction'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'validation'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;validation&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;_validate_prediction&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;cd_values&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;ndarray&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;"""Physics validation of predicted curve."""&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s1"&gt;'smoothness'&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;_calculate_smoothness&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_values&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'transonic_quality'&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;_check_transonic_peak&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_values&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
            &lt;span class="s1"&gt;'negative_cd_count'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_values&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="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
            &lt;span class="s1"&gt;'physical_plausibility'&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;_check_plausibility&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cd_values&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;REST API Endpoint&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# routes/bullets_unified.py&lt;/span&gt;

&lt;span class="nd"&gt;@bp&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;route&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'/search'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;methods&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'GET'&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;search_bullets&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Search unified bullet database with optional CDM prediction."""&lt;/span&gt;
    &lt;span class="n"&gt;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'q'&lt;/span&gt;&lt;span class="p"&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;use_cdm_prediction&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;request&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'use_cdm_prediction'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;lower&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="s1"&gt;'true'&lt;/span&gt;

    &lt;span class="c1"&gt;# Search database&lt;/span&gt;
    &lt;span class="n"&gt;results&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;search_database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;query&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="n"&gt;cdm_predictions_made&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;use_cdm_prediction&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;cdm_predictor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CDMTransferLearning&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;bullet&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;results&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;bullet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'cdm_data'&lt;/span&gt;&lt;span class="p"&gt;)&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="c1"&gt;# Predict CDM if not available&lt;/span&gt;
                &lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
                    &lt;span class="n"&gt;cdm_data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cdm_predictor&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="s1"&gt;'caliber'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'caliber'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                        &lt;span class="s1"&gt;'weight_gr'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'weight_gr'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
                        &lt;span class="s1"&gt;'bc_g1'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bc_g1'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'bc_g7'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'bc_g7'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'length_in'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'length_in'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                        &lt;span class="s1"&gt;'ogive_radius_cal'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'ogive_radius_cal'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                    &lt;span class="p"&gt;})&lt;/span&gt;

                    &lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'cdm_data'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;cdm_data&lt;/span&gt;
                    &lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'cdm_predicted'&lt;/span&gt;&lt;span class="p"&gt;]&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;cdm_predictions_made&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;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;warning&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;"CDM prediction failed for bullet &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;bullet&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'id'&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;e&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="n"&gt;jsonify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
        &lt;span class="s1"&gt;'results'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;results&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'cdm_prediction_enabled'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;use_cdm_prediction&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="s1"&gt;'cdm_predictions_made'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;cdm_predictions_made&lt;/span&gt;
    &lt;span class="p"&gt;})&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Example Response:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"results"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"manufacturer"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Sierra"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"model"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"MatchKing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"caliber"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.308&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"weight_gr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;168&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"bc_g1"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.462&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"bc_g7"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.237&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"cdm_data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;"mach_numbers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &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="w"&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="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.6&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;4.5&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;"drag_coefficients"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;0.287&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.289&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.295&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;...&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.312&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ml_transfer_learning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;"method"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"mlp_prediction"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nt"&gt;"validation"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;"smoothness"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;91.2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;"transonic_quality"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&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="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;"negative_cd_count"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="nt"&gt;"physical_plausibility"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="w"&gt;      &lt;/span&gt;&lt;span class="nt"&gt;"cdm_predicted"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"cdm_prediction_enabled"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;true&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nt"&gt;"cdm_predictions_made"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;18&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;hr&gt;
&lt;h3&gt;Part 5: Deployment and Monitoring&lt;/h3&gt;
&lt;h4&gt;Model Serving Architecture&lt;/h4&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;┌─────────────────┐
│   Client App    │
└────────┬────────┘
         │
         ▼
┌─────────────────────────┐
│  Google Cloud Function  │
│  (Python 3.12)          │
│  - Flask routing        │
│  - Request validation   │
│  - Response formatting  │
└────────┬────────────────┘
         │
         ▼
┌─────────────────────────┐
│  CDMTransferLearning    │
│  - PyTorch model (2.1MB)│
│  - CPU inference (&amp;lt;10ms)│
│  - Feature engineering  │
└────────┬────────────────┘
         │
         ▼
┌─────────────────────────┐
│  Physics Validation     │
│  - Smoothness check     │
│  - Peak detection       │
│  - Plausibility gates   │
└─────────────────────────┘
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Performance Characteristics&lt;/h4&gt;
&lt;p&gt;Model Size:
- PyTorch state dict: 2.1 MB
- TorchScript (optional): 2.3 MB
- ONNX (optional): 1.8 MB&lt;/p&gt;
&lt;p&gt;Inference Speed (CPU):
- Single prediction: 6-8 ms
- Batch of 10: 12-15 ms (1.2-1.5 ms per bullet)
- Batch of 100: 80-100 ms (0.8-1.0 ms per bullet)&lt;/p&gt;
&lt;p&gt;Cold Start:
- Model load time: 150-200 ms
- First prediction: 220-280 ms (including load)
- Subsequent predictions: 6-8 ms&lt;/p&gt;
&lt;p&gt;Memory Footprint:
- Model in memory: ~15 MB
- Peak during inference: ~30 MB&lt;/p&gt;
&lt;p&gt;&lt;img alt="Production Performance" src="https://tinycomputers.io/images/production_performance.png"&gt;
&lt;em&gt;Figure 8: Production inference performance metrics across different batch sizes&lt;/em&gt;&lt;/p&gt;
&lt;h4&gt;Monitoring and Observability&lt;/h4&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;newrelic.agent&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;MonitoredCDMPredictor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""CDM predictor with New Relic monitoring."""&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="bp"&gt;self&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;predictor&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;CDMTransferLearning&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;prediction_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&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;error_count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;

    &lt;span class="nd"&gt;@newrelic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;function_trace&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;predict&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;bullet_data&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;dict&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;"""Predict with telemetry."""&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;prediction_count&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;try&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="c1"&gt;# Track prediction time&lt;/span&gt;
            &lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="n"&gt;newrelic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;FunctionTrace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s1"&gt;'cdm_prediction'&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
                &lt;span class="n"&gt;result&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;predictor&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;bullet_data&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

            &lt;span class="c1"&gt;# Custom metrics&lt;/span&gt;
            &lt;span class="n"&gt;newrelic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_custom_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CDM/Predictions/Total'&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;prediction_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;newrelic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_custom_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CDM/Validation/Smoothness'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                               &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'validation'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'smoothness'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;
            &lt;span class="n"&gt;newrelic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_custom_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CDM/Validation/TransonicQuality'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                                               &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="s1"&gt;'validation'&lt;/span&gt;&lt;span class="p"&gt;][&lt;/span&gt;&lt;span class="s1"&gt;'transonic_quality'&lt;/span&gt;&lt;span class="p"&gt;])&lt;/span&gt;

            &lt;span class="c1"&gt;# Track feature availability&lt;/span&gt;
            &lt;span class="n"&gt;features_available&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt; &lt;span class="k"&gt;for&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;v&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;bullet_data&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;items&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;v&lt;/span&gt; &lt;span class="ow"&gt;is&lt;/span&gt; &lt;span class="ow"&gt;not&lt;/span&gt; &lt;span class="kc"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;newrelic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_custom_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CDM/Features/Available'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;features_available&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;result&lt;/span&gt;

        &lt;span class="k"&gt;except&lt;/span&gt; &lt;span class="ne"&gt;Exception&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;e&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;error_count&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;newrelic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;record_custom_metric&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'CDM/Errors/Total'&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;error_count&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
            &lt;span class="n"&gt;newrelic&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;agent&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;notice_error&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
            &lt;span class="k"&gt;raise&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Key Metrics Tracked:
- Prediction latency (p50, p95, p99)
- Validation scores (smoothness, transonic quality)
- Feature availability (how many inputs provided)
- Error rate and types
- Cache hit rate (if caching enabled)&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Lessons Learned&lt;/h3&gt;
&lt;h4&gt;1. Simple Architectures Often Win&lt;/h4&gt;
&lt;p&gt;We spent a week exploring Transformers and Neural ODEs, only to find the vanilla MLP performed best. Why?&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Data alignment: Our problem is function approximation, not sequence modeling&lt;/li&gt;
&lt;li&gt;Inductive bias mismatch: Transformers expect temporal dependencies; drag curves don't have them&lt;/li&gt;
&lt;li&gt;Regularization sufficiency: Dropout + weight decay provided enough regularization without physics constraints&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Lesson: Start simple. Add complexity only when data clearly demands it.&lt;/p&gt;
&lt;h4&gt;2. Physics Validation &amp;gt; Physics Loss&lt;/h4&gt;
&lt;p&gt;Hard-coded physics loss functions became a liability:
- Over-constrained the model
- Required manual tuning of loss weights
- Didn't generalize to all bullet types&lt;/p&gt;
&lt;p&gt;Better approach: Validate predictions post-hoc and flag anomalies. Let the model learn physics from data.&lt;/p&gt;
&lt;h4&gt;3. Synthetic Data Quality Matters More Than Quantity&lt;/h4&gt;
&lt;p&gt;We generated 704 synthetic CDMs, but spent equal time validating them. Key insight: One bad synthetic sample can poison dozens of real samples during training.&lt;/p&gt;
&lt;p&gt;Validation process:
1. Compare synthetic vs. real CDMs (where both exist)
2. Physics plausibility checks
3. Cross-validation with different BC values
4. Manual inspection of outliers&lt;/p&gt;
&lt;h4&gt;4. Feature Engineering &amp;gt; Model Complexity&lt;/h4&gt;
&lt;p&gt;The most impactful changes weren't architectural:
- Adding &lt;code&gt;sectional_density&lt;/code&gt; as a feature: -0.8% MAE
- Computing &lt;code&gt;form_factor_g1&lt;/code&gt; and &lt;code&gt;form_factor_g7&lt;/code&gt;: -0.6% MAE
- Imputing missing features (length, ogive) using physics-based defaults: -0.5% MAE&lt;/p&gt;
&lt;p&gt;&lt;img alt="Feature Importance" src="https://tinycomputers.io/images/feature_importance.png"&gt;&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Figure 9: Feature importance analysis showing impact of each input feature on prediction accuracy&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Combined improvement: -1.9% MAE with zero code changes to the model.&lt;/p&gt;
&lt;h4&gt;5. Production Deployment ≠ POC&lt;/h4&gt;
&lt;p&gt;Our POC model worked great in notebooks. Production required:
- Input validation and sanitization
- Graceful degradation when features missing
- Physics validation gates
- Monitoring and alerting
- Model versioning and rollback capability
- A/B testing infrastructure&lt;/p&gt;
&lt;p&gt;Time split: 30% research, 70% production engineering.&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;What's Next?&lt;/h3&gt;
&lt;h4&gt;Phase 2: Uncertainty Quantification&lt;/h4&gt;
&lt;p&gt;Current model outputs point estimates. We're implementing Bayesian Neural Networks to provide confidence intervals:&lt;/p&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;BayesianCDMPredictor&lt;/span&gt;&lt;span class="p"&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="w"&gt;    &lt;/span&gt;&lt;span class="sd"&gt;"""Bayesian NN with dropout as approximate inference."""&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;predict_with_uncertainty&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;features&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;n_samples&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="w"&gt;        &lt;/span&gt;&lt;span class="sd"&gt;"""Monte Carlo dropout for uncertainty estimation."""&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;train&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;  &lt;span class="c1"&gt;# Enable dropout during inference&lt;/span&gt;

        &lt;span class="n"&gt;predictions&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;_&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_samples&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;with&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;no_grad&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt;
                &lt;span class="n"&gt;pred&lt;/span&gt; &lt;span class="o"&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;features&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
                &lt;span class="n"&gt;predictions&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;pred&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;predictions&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;stack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;mean&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;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;std&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;predictions&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;std&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;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
            &lt;span class="s1"&gt;'cd_mean'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'cd_std'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
            &lt;span class="s1"&gt;'cd_lower'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="mf"&gt;1.96&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;  &lt;span class="c1"&gt;# 95% CI&lt;/span&gt;
            &lt;span class="s1"&gt;'cd_upper'&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;mean&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mf"&gt;1.96&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;std&lt;/span&gt;
        &lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Use case: Flag predictions with high uncertainty for manual review or experimental validation.&lt;/p&gt;
&lt;h3&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building a production ML system for ballistic drag prediction required more than just training a model:
- Data engineering (Claude Vision automation saved countless hours)
- Synthetic data generation (2.1× data augmentation)
- Architecture exploration (simple MLP won)
- Real-world validation (94% physics check pass rate)&lt;/p&gt;
&lt;p&gt;The result: 1,247 bullets now have accurate drag models that didn't exist before. Not bad for a side project.&lt;/p&gt;
&lt;p&gt;Read the full technical whitepaper for mathematical derivations, validation details, and complete bibliography: &lt;a href="https://tinycomputers.io/data/cdm_transfer_learning.pdf"&gt;cdm_transfer_learning.pdf&lt;/a&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h3&gt;Resources&lt;/h3&gt;
&lt;p&gt;References:
1. McCoy, R. L. (1999). &lt;em&gt;Modern Exterior Ballistics&lt;/em&gt;. Schiffer Publishing.
2. Litz, B. (2016). &lt;em&gt;Applied Ballistics for Long Range Shooting&lt;/em&gt; (3rd ed.).&lt;/p&gt;</description><category>ballistics</category><category>claude ai</category><category>computer vision</category><category>machine learning</category><category>neural networks</category><category>physics</category><category>pytorch</category><category>transfer learning</category><guid>https://tinycomputers.io/posts/transfer-learning-for-predictive-custom-drag-modeling-automated-generation-of-drag-coefficient-curves-using-multi-modal-ai.html</guid><pubDate>Fri, 10 Oct 2025 18:10:00 GMT</pubDate></item><item><title>Smart Ballistics: How Machine Learning Helps Calculate Bullet Stability When Data Is Missing</title><link>https://tinycomputers.io/posts/smart-ballistics-how-machine-learning-helps-calculate-bullet-stability-when-data-is-missing.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div&gt;&lt;p&gt;When a bullet leaves a rifle barrel, it's spinning, sometimes over 200,000 RPM. This spin is crucial: without it, the projectile would tumble unpredictably through the air like a thrown stick. But here's the problem: calculating whether a bullet will fly stable requires knowing its exact dimensions, and manufacturers often keep critical measurements secret. This is where machine learning comes to the rescue, not by replacing physics, but by filling in the missing pieces.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://tinycomputers.io/posts/smart-ballistics-how-machine-learning-helps-calculate-bullet-stability-when-data-is-missing.html?utm_source=feed&amp;amp;utm_medium=rss&amp;amp;utm_campaign=rss"&gt;Read more…&lt;/a&gt; (7 min remaining to read)&lt;/p&gt;&lt;/div&gt;</description><category>ballistics</category><category>hybrid models</category><category>machine learning</category><category>physics</category><category>random forest</category><guid>https://tinycomputers.io/posts/smart-ballistics-how-machine-learning-helps-calculate-bullet-stability-when-data-is-missing.html</guid><pubDate>Sun, 24 Aug 2025 21:28:03 GMT</pubDate></item><item><title>Open Sourcing a High Performance Rust-based Ballistics Engine</title><link>https://tinycomputers.io/posts/open-sourcing-a-high-performance-rust-based-ballistics-engine.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/open-sourcing-a-high-performance-rust-based-ballistics-engine_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;12 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;h2&gt;From SaaS to Open Source: The Evolution of a Ballistics Engine&lt;/h2&gt;
&lt;p&gt;When I first built &lt;a href="https://baud.rs/H2gonn"&gt;Ballistics Insight&lt;/a&gt;, my ML-augmented ballistics calculation platform, I faced a classic engineering dilemma: how to balance performance, accuracy, and maintainability across multiple platforms. The solution came in the form of a high-performance Rust core that became the beating heart of the system. Today, I'm excited to share that journey and announce the open-sourcing of this engine as a standalone library with full FFI bindings for iOS and Android.&lt;/p&gt;
&lt;h3&gt;The Genesis: A Python Problem&lt;/h3&gt;
&lt;p&gt;The story begins with a Python Flask application serving ballistics calculations through a REST API. The initial implementation worked well enough for proof-of-concept, but as I added more sophisticated physics models (Magnus effect, Coriolis force, transonic drag corrections, gyroscopic precession) the performance limitations became apparent. A single trajectory calculation that should take milliseconds was stretching into seconds. Monte Carlo simulations with thousands of iterations were becoming impractical.&lt;/p&gt;
&lt;p&gt;The Python implementation had another challenge: code duplication. I maintained separate implementations for atmospheric calculations, drag computations, and trajectory integration. Each time I fixed a bug or improved an algorithm, I had to ensure consistency across multiple code paths. The maintenance burden was growing exponentially with the feature set.&lt;/p&gt;
&lt;h3&gt;The Rust Revolution&lt;/h3&gt;
&lt;p&gt;The decision to rewrite the core physics engine in Rust wasn't taken lightly. I evaluated several options: optimizing the Python code with NumPy vectorization, using Cython for critical paths, or even moving to C++. Rust won for several compelling reasons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Memory Safety Without Garbage Collection&lt;/strong&gt;: Ballistics calculations involve extensive numerical computation with predictable memory patterns. Rust's ownership system eliminated entire categories of bugs while maintaining deterministic performance.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Zero-Cost Abstractions&lt;/strong&gt;: I could write high-level, maintainable code that compiled down to assembly as efficient as hand-optimized C.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Excellent FFI Story&lt;/strong&gt;: Rust's ability to expose C-compatible interfaces meant I could integrate with any platform: Python, iOS, Android, or web via WebAssembly.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Modern Tooling&lt;/strong&gt;: Cargo, Rust's build system and package manager, made dependency management and cross-compilation straightforward.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The results were dramatic. Atmospheric calculations went from 4.5ms in Python to 0.8ms in Rust, a 5.6x improvement. Complete trajectory calculations saw 15-20x performance gains. Monte Carlo simulations that previously took minutes now completed in seconds.&lt;/p&gt;
&lt;h3&gt;Architecture: From Monolith to Modular&lt;/h3&gt;
&lt;p&gt;The closed-source Ballistics Insight platform is a sophisticated system with ML augmentations, weather integration, and a comprehensive ammunition database. It includes features like:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Neural network-based BC (Ballistic Coefficient) prediction&lt;/li&gt;
&lt;li&gt;Regional weather model integration with ERA5, OpenWeather, and NOAA data&lt;/li&gt;
&lt;li&gt;Magnus effect auto-calibration based on bullet classification&lt;/li&gt;
&lt;li&gt;Yaw damping prediction using gyroscopic stability factors&lt;/li&gt;
&lt;li&gt;A database of 2,000+ bullets with manufacturer specifications&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;For the open-source release, I took a different approach. Rather than trying to extract everything, I focused on the core physics engine, the foundation that makes everything else possible. This meant:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Extracting Pure Physics&lt;/strong&gt;: I separated the deterministic physics calculations from the ML augmentations. The open-source engine provides the fundamental ballistics math, while the SaaS platform layers intelligent corrections on top.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Creating Clean Interfaces&lt;/strong&gt;: I designed a new FFI layer from scratch, ensuring that iOS and Android developers could easily integrate the engine without understanding Rust or ballistics physics.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Building Standalone Tools&lt;/strong&gt;: The engine includes a full-featured command-line interface, making it useful for researchers, enthusiasts, and developers who need quick calculations without writing code.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The FFI Challenge: Making Rust Speak Every Language&lt;/h3&gt;
&lt;p&gt;One of my primary goals was to make the engine accessible from any platform. This meant creating robust Foreign Function Interface (FFI) bindings that could be consumed by Swift, Kotlin, Java, Python, or any language that can call C functions.&lt;/p&gt;
&lt;p&gt;The FFI layer presented unique challenges:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="cp"&gt;#[repr(C)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;struct&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;FFIBallisticInputs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;muzzle_velocity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;c_double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="c1"&gt;// m/s&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ballistic_coefficient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;c_double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;mass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;c_double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                   &lt;/span&gt;&lt;span class="c1"&gt;// kg&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;diameter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;c_double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;               &lt;/span&gt;&lt;span class="c1"&gt;// meters&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;drag_model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;c_int&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="c1"&gt;// 0=G1, 1=G7&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;pub&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;sight_height&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;c_double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="c1"&gt;// meters&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ... many more fields&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I had to ensure:
- &lt;strong&gt;C-compatible memory layouts&lt;/strong&gt; using &lt;code&gt;#[repr(C)]&lt;/code&gt;
- &lt;strong&gt;Safe memory management&lt;/strong&gt; across language boundaries
- &lt;strong&gt;Graceful error handling&lt;/strong&gt; without exceptions
- &lt;strong&gt;Zero-copy data transfer&lt;/strong&gt; where possible&lt;/p&gt;
&lt;p&gt;The result is a library that can be dropped into an iOS app as a static library, integrated into Android via JNI, or called from Python using ctypes. Each platform sees a native interface while the Rust engine handles the heavy lifting.&lt;/p&gt;
&lt;h3&gt;The Mobile Story: Binary Libraries for iOS and Android&lt;/h3&gt;
&lt;p&gt;Creating mobile bindings required careful consideration of each platform's requirements:&lt;/p&gt;
&lt;h4&gt;iOS Integration&lt;/h4&gt;
&lt;p&gt;For iOS, I compile the Rust library to a universal static library supporting both ARM64 (devices) and x86_64 (simulator). Swift developers interact with the engine through a bridging header:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;inputs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;FFIBallisticInputs&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;muzzle_velocity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;823.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;ballistic_coefficient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.475&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;mass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.0109&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;diameter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.00782&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="kd"&gt;let&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;result&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ballistics_calculate_trajectory&lt;/span&gt;&lt;span class="p"&gt;(&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;inputs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;nil&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;1000.0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;0.1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;defer&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;ballistics_free_trajectory_result&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="bp"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Max range: &lt;/span&gt;&lt;span class="si"&gt;\(&lt;/span&gt;&lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pointee&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;max_range&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s"&gt; meters"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h4&gt;Android Integration&lt;/h4&gt;
&lt;p&gt;For Android, I provide pre-compiled libraries for multiple architectures (armeabi-v7a, arm64-v8a, x86, x86_64). The engine integrates seamlessly through JNI:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="kd"&gt;class&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nc"&gt;BallisticsEngine&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;external&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;fun&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nf"&gt;calculateTrajectory&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;muzzleVelocity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;ballisticCoefficient&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;mass&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;diameter&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;Double&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;maxRange&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kt"&gt;Double&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;TrajectoryResult&lt;/span&gt;

&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="kd"&gt;companion&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kd"&gt;object&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;init&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;loadLibrary&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"ballistics_engine"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/pre&gt;&lt;/div&gt;

&lt;h3&gt;Performance: The Numbers That Matter&lt;/h3&gt;
&lt;p&gt;The open-source engine achieves remarkable performance across all platforms:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Single Trajectory (1000m)&lt;/strong&gt;: ~5ms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Monte Carlo Simulation (1000 runs)&lt;/strong&gt;: ~500ms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;BC Estimation&lt;/strong&gt;: ~50ms&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Zero Calculation&lt;/strong&gt;: ~10ms&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;These numbers represent pure computation time on modern hardware. The engine uses RK4 (4th-order Runge-Kutta) integration by default for maximum accuracy, with an option to switch to Euler's method for even faster computation when precision requirements are relaxed.&lt;/p&gt;
&lt;h3&gt;Advanced Physics: More Than Just Parabolas&lt;/h3&gt;
&lt;p&gt;While the basic trajectory of a projectile follows a parabolic path in a vacuum, real-world ballistics is far more complex. The engine models:&lt;/p&gt;
&lt;h4&gt;Aerodynamic Effects&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Velocity-dependent drag&lt;/strong&gt; using standard drag functions (G1, G7) or custom curves&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transonic drag rise&lt;/strong&gt; as projectiles approach the speed of sound&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Reynolds number corrections&lt;/strong&gt; for viscous effects at low velocities&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Form factor adjustments&lt;/strong&gt; based on projectile shape&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Gyroscopic Phenomena&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Spin drift&lt;/strong&gt; from the Magnus effect on spinning projectiles&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Precession and nutation&lt;/strong&gt; of the projectile's axis&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Spin decay&lt;/strong&gt; over the flight path&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Yaw of repose&lt;/strong&gt; in crosswinds&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Environmental Factors&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Coriolis effect&lt;/strong&gt; from Earth's rotation (critical for long-range shots)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Wind shear&lt;/strong&gt; modeling with altitude-dependent wind variations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Atmospheric stratification&lt;/strong&gt; using ICAO standard atmosphere&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Humidity effects&lt;/strong&gt; on air density&lt;/li&gt;
&lt;/ul&gt;
&lt;h4&gt;Stability Analysis&lt;/h4&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Dynamic stability&lt;/strong&gt; calculations&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Pitch damping&lt;/strong&gt; coefficients through transonic regions&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Gyroscopic stability&lt;/strong&gt; factors&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Transonic instability&lt;/strong&gt; warnings&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;The Command Line Interface: Power at Your Fingertips&lt;/h3&gt;
&lt;p&gt;The engine includes a comprehensive CLI that rivals commercial ballistics software:&lt;/p&gt;
&lt;div class="code"&gt;&lt;pre class="code literal-block"&gt;&lt;span class="c1"&gt;# Basic trajectory with auto-zeroing&lt;/span&gt;
./ballistics&lt;span class="w"&gt; &lt;/span&gt;trajectory&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2700&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-b&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.475&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;168&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.308&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--auto-zero&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;200&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--max-range&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;

&lt;span class="c1"&gt;# Monte Carlo simulation for load development&lt;/span&gt;
./ballistics&lt;span class="w"&gt; &lt;/span&gt;monte-carlo&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2700&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-b&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.475&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;168&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.308&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;-n&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;1000&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--velocity-std&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--bc-std&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.01&lt;span class="w"&gt; &lt;/span&gt;--target-distance&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;600&lt;/span&gt;

&lt;span class="c1"&gt;# Estimate BC from observed drops&lt;/span&gt;
./ballistics&lt;span class="w"&gt; &lt;/span&gt;estimate-bc&lt;span class="w"&gt; &lt;/span&gt;-v&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;2700&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-m&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;168&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;-d&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.308&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="se"&gt;\&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;--distance1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;100&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--drop1&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.0&lt;span class="w"&gt; &lt;/span&gt;--distance2&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;300&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--drop2&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;.075
&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The CLI supports both imperial (default) and metric units, multiple output formats (table, JSON, CSV), and can enable individual physics models as needed.&lt;/p&gt;
&lt;h3&gt;Lessons Learned: The Open Source Journey&lt;/h3&gt;
&lt;p&gt;Extracting and open-sourcing a core component from a larger system taught me valuable lessons:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Clear Boundaries Matter&lt;/strong&gt;: Separating deterministic physics from ML augmentations made the extraction cleaner and the resulting library more focused.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Documentation is Code&lt;/strong&gt;: I invested heavily in documentation, from inline Rust docs to comprehensive README examples. Good documentation dramatically increases adoption.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Performance Benchmarks Build Trust&lt;/strong&gt;: Publishing concrete performance numbers helps users understand what they're getting and sets realistic expectations.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;FFI Design is Critical&lt;/strong&gt;: A well-designed FFI layer makes the difference between a library that's theoretically cross-platform and one that's actually used across platforms.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Community Feedback is Gold&lt;/strong&gt;: Early users found edge cases I never considered and suggested features that made the engine more valuable.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The Website: ballistics.rs&lt;/h3&gt;
&lt;p&gt;To support the open-source project, I created &lt;a href="https://baud.rs/jliUH9"&gt;ballistics.rs&lt;/a&gt;, a dedicated website that serves as the central hub for documentation, downloads, and community engagement. Built as a static site hosted on Google Cloud Platform with global CDN distribution, it provides fast access to resources from anywhere in the world.&lt;/p&gt;
&lt;p&gt;The website showcases:
- Comprehensive documentation and API references
- Platform-specific integration guides
- Performance benchmarks and comparisons
- Example code and use cases
- Links to the GitHub repository and issue tracker&lt;/p&gt;
&lt;h3&gt;Looking Forward: The Future of Open Ballistics&lt;/h3&gt;
&lt;p&gt;Open-sourcing the ballistics engine is just the beginning. I'm excited about several upcoming developments:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;WebAssembly Support&lt;/strong&gt;: Bringing high-performance ballistics calculations directly to web browsers.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;GPU Acceleration&lt;/strong&gt;: For massive Monte Carlo simulations and trajectory optimization.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Extended Drag Models&lt;/strong&gt;: Supporting more specialized drag functions for specific projectile types.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Community Contributions&lt;/strong&gt;: I'm already seeing pull requests for new features and improvements.&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;&lt;strong&gt;Educational Resources&lt;/strong&gt;: Creating interactive visualizations and tutorials to help people understand ballistics physics.&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;
&lt;h3&gt;The Business Model: Open Core Done Right&lt;/h3&gt;
&lt;p&gt;My approach follows the "open core" model. The fundamental physics engine is open source and will always remain so. The value-added features in Ballistics Insight (ML augmentations, weather integration, ammunition databases, and the web API) constitute our commercial offering.&lt;/p&gt;
&lt;p&gt;This model benefits everyone:
- Developers get a production-ready ballistics engine for their applications
- Researchers have a reference implementation for ballistics algorithms
- The community can contribute improvements that benefit all users
- I maintain a sustainable business while giving back to the open-source ecosystem&lt;/p&gt;
&lt;h3&gt;Conclusion: Precision Through Open Collaboration&lt;/h3&gt;
&lt;p&gt;The journey from a closed-source SaaS platform to an open-source library with mobile bindings represents more than just a code release. It's a commitment to the principle that fundamental scientific calculations should be open, verifiable, and accessible to all.&lt;/p&gt;
&lt;p&gt;By open-sourcing the ballistics engine, I'm not just sharing code; I'm inviting collaboration from developers, researchers, and enthusiasts worldwide. Whether you're building a mobile app for hunters, creating educational software for physics students, or conducting research on projectile dynamics, you now have access to a battle-tested, high-performance engine that handles the complex mathematics of ballistics.&lt;/p&gt;
&lt;p&gt;The combination of Rust's performance and safety, comprehensive physics modeling, and carefully designed FFI bindings creates a unique resource in the ballistics software ecosystem. I'm excited to see what the community builds with it.&lt;/p&gt;
&lt;p&gt;Visit &lt;a href="https://baud.rs/jliUH9"&gt;ballistics.rs&lt;/a&gt; to get started, browse the documentation, or contribute to the project. The repository is available on &lt;a href="https://baud.rs/QckusG"&gt;GitHub&lt;/a&gt;, and I welcome issues, pull requests, and feedback.&lt;/p&gt;
&lt;p&gt;In the world of ballistics, precision is everything. With this open-source release, I'm putting that precision in your hands.&lt;/p&gt;</description><category>android</category><category>ballistics</category><category>ffi</category><category>ios</category><category>open-source</category><category>physics</category><category>rust</category><category>simulation</category><guid>https://tinycomputers.io/posts/open-sourcing-a-high-performance-rust-based-ballistics-engine.html</guid><pubDate>Sat, 16 Aug 2025 21:11:16 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, and 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><item><title>Modeling Ballistic Trajectories with Calculus and Numerical Methods</title><link>https://tinycomputers.io/posts/modeling-ballistic-trajectories-with-calculus-and-numerical-methods.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/modeling-ballistic-trajectories-with-calculus-and-numerical-methods_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;13 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Introduction&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;a href="https://baud.rs/X9VLcH"&gt;&lt;img src="https://tinycomputers.io/images/308.png" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;&lt;/a&gt;Ballistics is the study of the motion of projectiles under the influence of gravity and air resistance - a complex phenomenon with far-reaching implications in various industries, including military, aerospace, and sports. The importance of understanding ballistics cannot be overstated: in these fields, accuracy, safety, and performance are often directly tied to the ability to predict and control the trajectory of an object in flight.&lt;/p&gt;
&lt;p&gt;At its core, ballistics is concerned with four key concepts: ballistic coefficient, muzzle velocity, bullet trajectory, and distance to target. The ballistic coefficient, a measure of a projectile's aerodynamic efficiency, plays a crucial role in determining how much air resistance it will encounter - and thus, how far it will travel. Muzzle velocity, the speed at which a projectile exits a gun or launcher, is another critical factor in this equation.&lt;/p&gt;
&lt;p&gt;By understanding these concepts and applying mathematical techniques to model ballistic trajectories, we can gain a deeper insight into the intricacies of projectile motion. In this article, we'll explore the use of calculus and numerical methods to achieve just that - providing a more accurate and reliable way to predict and control the trajectory of objects in flight.&lt;/p&gt;
&lt;p&gt;As a teenager in the early 1990s, I was deeply interested in ballistics.  These were the pre-internet days and books were the primary means of acquiring information.  Projectiles, when pushed out the barrel, travel in an arc and not in a completely flat trajectory.  One of the things I was keenly interested in was the maximum height above the muzzle that the arc reaches. Another metric that I wanted was how much the bullet drops from the muzzle at a particular distance.  There were a couple problems with me reaching those objectives: my math skills were rudimentary and my knowledge was limited to the books on handloading ammunition that I had as well as what could be found at the local library.&lt;/p&gt;
&lt;p&gt;I poured over the &lt;a href="https://baud.rs/u3yWL0"&gt;handloading manuals&lt;/a&gt; trying to come up with equations that I could understand.  My programming framework of choice was Visual Basic.  I really wanted to make an application that I could just plug in variable values and the software would calculate the numbers I was interested.  Fast forward over thirty years, I have an infinite amount of information at my finger tips, I have access to generative AI, and I have years of mathematics and problem solving skills.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The Field of Ballistics&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Ballistics is a multidisciplinary field of study that encompasses the science and engineering of projectiles in motion. At its core, ballistics is concerned with understanding the complex interactions between a projectile, its environment, and the forces that act upon it.&lt;/p&gt;
&lt;p&gt;The field of ballistics can be broadly divided into three subfields: interior, exterior, and terminal ballistics. Interior ballistics deals with the behavior of propellants and projectiles within a gun or launcher, while exterior ballistics focuses on the motion of the projectile in free flight. Terminal ballistics, on the other hand, examines the impact and penetration characteristics of a projectile upon striking its target.&lt;/p&gt;
&lt;p&gt;Understanding ballistics is crucial in various fields, including military, hunting, and aerospace. In these industries, accuracy, safety, and performance are often directly tied to the ability to predict and control the trajectory of an object in flight. For instance, in military applications, understanding ballistic trajectories can mean the difference between hitting a target and missing it by miles. Similarly, in hunting, a deep understanding of ballistics can help hunters make clean kills and avoid wounding animals.&lt;/p&gt;
&lt;p&gt;So what factors affect ballistic trajectories? Air resistance, gravity, and spin are just a few of the key players that influence the motion of a projectile. Air resistance, for example, can slow down a projectile depending on its shape, size, and velocity. Gravity, of course, pulls the projectile downwards, while spin can impart a stabilizing force that helps maintain a consistent flight path. By understanding these factors and their complex interactions, ballisticians can develop more accurate models of projectile motion and improve performance in various applications.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ballistic Coefficient: Measurement and Significance&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/signal-2024-09-12-202612_008.jpeg" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;In the world of ballistics, precision is paramount. Whether it's a military operation, a hunting expedition, or a competitive shooting event, the trajectory of a projectile can make all the difference between success and failure. At the heart of this quest for accuracy lies the ballistic coefficient (BC), a fundamental concept that describes the aerodynamic efficiency of a projectile.&lt;/p&gt;
&lt;p&gt;In simple terms, the BC is a measure of how well a bullet can cut through the air with minimal resistance. It's a dimensionless quantity that characterizes the relationship between a projectile's mass, size, shape, and velocity, and the drag force acting on it. But what exactly determines the ballistic coefficient of a projectile?&lt;/p&gt;
&lt;p&gt;Several factors come into play, including the bullet's shape, size, and weight, as well as its velocity and angle of attack. The BC can be measured using various techniques, such as wind tunnel testing or Doppler radar. Wind tunnel testing involves firing a projectile through a controlled environment with known air density and pressure conditions. By analyzing the data collected from these tests, ballisticians can calculate the ballistic coefficient with high accuracy.&lt;/p&gt;
&lt;p&gt;But why is the ballistic coefficient so important in predicting bullet trajectory and accuracy? The answer lies in its relationship to drag force. A higher BC indicates less drag resistance, which means a projectile will travel farther and straighter before being slowed down by air resistance. Conversely, a lower BC signifies more drag resistance, resulting in a shorter range and greater deviation from the intended target.&lt;/p&gt;
&lt;p&gt;The implications of this are far-reaching. In military applications, understanding the ballistic coefficient can mean the difference between hitting or missing a target, with potentially catastrophic consequences. In hunting, it can determine whether a shot is effective or not, affecting both the welfare of the animal and the success of the hunt. And in sport shooting, it's essential for achieving optimal performance and accuracy.&lt;/p&gt;
&lt;p&gt;As such, accurately measuring the ballistic coefficient is crucial for achieving precision in various applications. By doing so, ballisticians can create more accurate models of bullet trajectory, taking into account factors such as air density, temperature, and humidity. This, in turn, enables them to optimize projectile design, selecting the right shape, size, and material to achieve the desired level of aerodynamic efficiency.&lt;/p&gt;
&lt;p&gt;The ballistic coefficient is a fundamental concept that underlies the art of ballistics. By understanding its relationship to drag force and accurately measuring it, ballisticians can unlock the secrets of aerodynamic efficiency, creating more accurate models of bullet trajectory and achieving optimal performance in various applications. Whether it's military, hunting, or sport shooting, precision is paramount – and the ballistic coefficient is key to achieving it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Calculus in Ballistics: Modeling Trajectories&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In ballistics, understanding the motion of projectiles is crucial for predicting their trajectory and accuracy. Differential equations play a vital role in modeling various aspects of ballistics, as they provide a mathematical framework for describing complex phenomena. A differential equation is an equation that describes how a quantity changes over time or space.&lt;/p&gt;
&lt;p&gt;One of the most fundamental applications of calculus in ballistics is modeling bullet trajectory under the influence of gravity and air resistance. The point mass model is a classic example of this approach. It assumes that the projectile can be treated as a single point with no dimensions, and its motion is governed by the following differential equation:&lt;/p&gt;
&lt;math xmlns="http://www.w3.org/1998/Math/MathML" style="font-size: 18pt;"&gt;
  &lt;mfrac&gt;
    &lt;mrow&gt;
      &lt;msup&gt;
        &lt;mi&gt;d&lt;/mi&gt;
        &lt;mn&gt;2&lt;/mn&gt;
      &lt;/msup&gt;
      &lt;mi&gt;x&lt;/mi&gt;
    &lt;/mrow&gt;
    &lt;mrow&gt;
      &lt;msup&gt;
        &lt;mi&gt;dt&lt;/mi&gt;
        &lt;mn&gt;2&lt;/mn&gt;
      &lt;/msup&gt;
    &lt;/mrow&gt;
  &lt;/mfrac&gt;
  &lt;mo&gt;=&lt;/mo&gt;
  &lt;mrow&gt;
    &lt;mo&gt;(&lt;/mo&gt;
    &lt;mi&gt;a&lt;/mi&gt;
    &lt;mo&gt;-&lt;/mo&gt;
    &lt;mi&gt;b&lt;/mi&gt;
    &lt;msup&gt;
      &lt;mi&gt;v&lt;/mi&gt;
      &lt;mfrac&gt;
        &lt;mn&gt;2&lt;/mn&gt;
        &lt;mn&gt;3&lt;/mn&gt;
      &lt;/mfrac&gt;
    &lt;/msup&gt;
    &lt;mo&gt;)&lt;/mo&gt;
    &lt;mi&gt;x&lt;/mi&gt;
  &lt;/mrow&gt;
  &lt;mo&gt;=&lt;/mo&gt;
  &lt;mrow&gt;
    &lt;mi&gt;a&lt;/mi&gt;
    &lt;mi&gt;t&lt;/mi&gt;
    &lt;mo&gt;-&lt;/mo&gt;
    &lt;mi&gt;b&lt;/mi&gt;
    &lt;msup&gt;
      &lt;mi&gt;v&lt;/mi&gt;
      &lt;mn&gt;3&lt;/mn&gt;
    &lt;/msup&gt;
    &lt;mo&gt;-&lt;/mo&gt;
    &lt;mi&gt;c&lt;/mi&gt;
    &lt;msup&gt;
      &lt;mi&gt;t&lt;/mi&gt;
      &lt;mn&gt;2&lt;/mn&gt;
    &lt;/msup&gt;
  &lt;/mrow&gt;
&lt;/math&gt;

&lt;p&gt;where x is the position of the projectile, v is its velocity, a and b are constants representing air resistance, c represents gravity, and t is time.&lt;/p&gt;
&lt;p&gt;In addition to modeling bullet trajectory, calculus can also be used to describe more complex phenomena such as spin-stabilized projectiles and ricochet dynamics. The &lt;a href="https://baud.rs/K8kvyA"&gt;6-DOF&lt;/a&gt; (six degrees of freedom) model, for example, takes into account the rotation and translation of a projectile in three-dimensional space.&lt;/p&gt;
&lt;p&gt;These are just a few examples of how calculus is used in ballistics to model various aspects of projectile motion. By applying mathematical techniques such as differential equations, researchers can gain valuable insights into the complex behavior of projectiles under different conditions.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Numerical Methods for Ballistic Trajectory Modeling&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;When it comes to modeling ballistic trajectories, numerical methods are an essential tool for solving complex differential equations that govern the motion of projectiles. In this context, numerical methods refer to techniques used to approximate solutions to these equations, which cannot be solved analytically.&lt;/p&gt;
&lt;p&gt;One of the most fundamental numerical methods in ballistics is &lt;a href="https://baud.rs/tXqKWL"&gt;Euler's method&lt;/a&gt;. This technique involves discretizing the solution space and approximating the trajectory using a series of small steps, each representing a short time interval. Mathematically, this can be represented as:&lt;/p&gt;
&lt;math xmlns="http://www.w3.org/1998/Math/MathML" style="font-size: 18pt;"&gt;
  &lt;mrow&gt;
    &lt;msub&gt;
      &lt;mi&gt;x&lt;/mi&gt;
      &lt;mrow data-mjx-texclass="ORD"&gt;
        &lt;mn&gt;1&lt;/mn&gt;
      &lt;/mrow&gt;
    &lt;/msub&gt;
    &lt;mo&gt;=&lt;/mo&gt;
    &lt;msub&gt;
      &lt;mi&gt;x&lt;/mi&gt;
      &lt;mrow data-mjx-texclass="ORD"&gt;
        &lt;mn&gt;0&lt;/mn&gt;
      &lt;/mrow&gt;
    &lt;/msub&gt;
    &lt;mo&gt;+&lt;/mo&gt;
    &lt;msup&gt;
      &lt;mi&gt;h&lt;/mi&gt;
      &lt;mrow data-mjx-texclass="ORD"&gt;
        &lt;mn&gt;1&lt;/mn&gt;
      &lt;/mrow&gt;
    &lt;/msup&gt;
    &lt;msub&gt;
      &lt;mi&gt;f&lt;/mi&gt;
      &lt;mrow data-mjx-texclass="ORD"&gt;
        &lt;mo stretchy="false"&gt;(&lt;/mo&gt;
        &lt;msub&gt;
          &lt;mi&gt;x&lt;/mi&gt;
          &lt;mrow data-mjx-texclass="ORD"&gt;
            &lt;mn&gt;0&lt;/mn&gt;
          &lt;/mrow&gt;
        &lt;/msub&gt;
        &lt;mo&gt;,&lt;/mo&gt;
        &lt;msub&gt;
          &lt;mi&gt;t&lt;/mi&gt;
          &lt;mrow data-mjx-texclass="ORD"&gt;
            &lt;mn&gt;0&lt;/mn&gt;
          &lt;/mrow&gt;
        &lt;/msub&gt;
        &lt;mo stretchy="false"&gt;)&lt;/mo&gt;
      &lt;/mrow&gt;
    &lt;/msub&gt;
  &lt;/mrow&gt;
&lt;/math&gt;

&lt;p&gt;where x is the position of the projectile, h is the time step, f(x,t) represents the acceleration at time t and position x.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://tinycomputers.io/images/308-plot.png" style="width: 480px; box-shadow: 0 30px 40px rgba(0,0,0,.1); float: left; padding: 20px 20px 20px 20px;"&gt;While Euler's method provides a basic framework for approximating solutions to differential equations, more sophisticated techniques such as the &lt;a href="https://baud.rs/Ixb4ee"&gt;Runge-Kutta&lt;/a&gt; methods offer greater accuracy and stability. The Runge-Kutta methods involves using multiple intermediate steps to improve the approximation of the solution, rather than relying on a single step as in Euler's method.&lt;/p&gt;
&lt;p&gt;Numerical methods have numerous advantages in ballistics, including their ability to handle complex systems and provide accurate solutions for non-linear equations. However, these methods also have limitations, such as the potential for numerical instability and the computational resources required to achieve high accuracy.&lt;/p&gt;
&lt;p&gt;Numerical methods are a powerful tool for modeling ballistic trajectories, offering a means of approximating solutions to complex differential equations that govern projectile motion. I have also covered numerical methods in other write-ups, namely, the pricing of stock options. While there are various techniques available, each with its own strengths and weaknesses, these methods provide an essential framework for analyzing and understanding ballistic phenomena.&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;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;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;# 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="c1"&gt;# m/s^2, acceleration due to gravity&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;780&lt;/span&gt;  &lt;span class="c1"&gt;# m/s, muzzle velocity of .308 Winchester&lt;/span&gt;
&lt;span class="n"&gt;theta&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;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="mi"&gt;180&lt;/span&gt;  &lt;span class="c1"&gt;# rad, angle of projection (25 degrees)&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;10.4e-3&lt;/span&gt;  &lt;span class="c1"&gt;# kg, mass of the projectile (10.4 grams)&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.5&lt;/span&gt;  &lt;span class="c1"&gt;# drag coefficient&lt;/span&gt;
&lt;span class="n"&gt;Bc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mf"&gt;0.47&lt;/span&gt;  &lt;span class="c1"&gt;# ballistic coefficient (G7 model)&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, air density at sea level&lt;/span&gt;

&lt;span class="c1"&gt;# Differential equations for projectile motion with air resistance&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;deriv&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;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;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="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;X&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="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;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;Bc&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="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="n"&gt;Fd&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;vx&lt;/span&gt; &lt;span class="o"&gt;/&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;v&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;ay&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="o"&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;vy&lt;/span&gt; &lt;span class="o"&gt;/&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;v&lt;/span&gt;&lt;span class="p"&gt;)&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;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;ax&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;ay&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Initial conditions&lt;/span&gt;
&lt;span class="n"&gt;X0&lt;/span&gt; &lt;span class="o"&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;0&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;cos&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta&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;sin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;theta&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;

&lt;span class="c1"&gt;# Time points&lt;/span&gt;
&lt;span class="n"&gt;t_flight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;  &lt;span class="c1"&gt;# seconds&lt;/span&gt;
&lt;span class="n"&gt;t&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_flight&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="c1"&gt;# Solve ODE&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;deriv&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;t&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;=&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;cumsum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;sol&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="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="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;t&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_x&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="n"&gt;x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; 
&lt;span class="n"&gt;min_x&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;min&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;scaled_x&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="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;min_x&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;max_x&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;min_x&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;
&lt;span class="n"&gt;y&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="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;

&lt;span class="c1"&gt;# Find the maximum height&lt;/span&gt;
&lt;span class="n"&gt;max_height&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="n"&gt;y&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;"The maximum height of the arc is &lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="n"&gt;max_height&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; m"&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;plot&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;scaled_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;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;'Horizontal distance (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="s1"&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;title&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'.308 Winchester 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;grid&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;p&gt;This code uses the &lt;code&gt;odeint&lt;/code&gt; function from SciPy to solve the system of differential equations that model the projectile motion with air resistance. The &lt;code&gt;deriv&lt;/code&gt; function defines the derivatives of the position and velocity with respect to time, including the effects of drag and gravity. The initial conditions are set for a .308 Winchester rifle fired at an angle of 25 degrees. The ballistic coefficient is used to calculate the drag force.&lt;/p&gt;
&lt;p&gt;The code also outputs the maximum arch height and projectile height from muzzle.&lt;/p&gt;
&lt;p&gt;Note that this simulation assumes a constant air density and neglects other factors such as wind resistance, spin stabilization, and variations in muzzle velocity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Conclusion&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;In this analysis, we explored the application of calculus and numerical methods to model the trajectory of a .308 Winchester bullet. By solving the system of differential equations that govern the motion of the projectile, we were able to accurately predict the bullet's path under various environmental conditions. Our results demonstrated the importance of considering air resistance in ballistic trajectories, as well as the need for precise calculations to ensure accuracy.&lt;/p&gt;
&lt;p&gt;Understanding ballistics is crucial for a range of applications, from military and hunting to aerospace engineering. Calculus and numerical methods play a vital role in modeling these complex systems, allowing us to make predictions and optimize performance. As demonstrated in this analysis, a deep understanding of mathematical concepts can have real-world implications, highlighting the importance of continued investment in STEM education and research.&lt;/p&gt;</description><category>.308 winchester</category><category>ballistics</category><category>calculus</category><category>engineering</category><category>numerical methods</category><category>physics</category><category>projectile motion</category><category>trajectory modeling</category><guid>https://tinycomputers.io/posts/modeling-ballistic-trajectories-with-calculus-and-numerical-methods.html</guid><pubDate>Fri, 13 Sep 2024 00:28:41 GMT</pubDate></item></channel></rss>