<?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 ai safety)</title><link>https://tinycomputers.io/</link><description></description><atom:link href="https://tinycomputers.io/categories/ai-safety.xml" rel="self" type="application/rss+xml"></atom:link><language>en</language><copyright>Contents © 2026 A.C. Jokela 
&lt;!-- div style="width: 100%" --&gt;
&lt;a rel="license" href="http://creativecommons.org/licenses/by-sa/4.0/"&gt;&lt;img alt="" style="border-width:0" src="https://i.creativecommons.org/l/by-sa/4.0/80x15.png" /&gt; Creative Commons Attribution-ShareAlike&lt;/a&gt;&amp;nbsp;|&amp;nbsp;
&lt;!-- /div --&gt;
</copyright><lastBuildDate>Wed, 11 Mar 2026 00:05:41 GMT</lastBuildDate><generator>Nikola (getnikola.com)</generator><docs>http://blogs.law.harvard.edu/tech/rss</docs><item><title>Physics-First ML: Why AI Should Correct, Not Replace, Scientific Models</title><link>https://tinycomputers.io/posts/physics-first-ml-why-ai-should-correct-not-replace-scientific-models.html?utm_source=feed&amp;utm_medium=rss&amp;utm_campaign=rss</link><dc:creator>A.C. Jokela</dc:creator><description>&lt;div class="audio-widget"&gt;
&lt;div class="audio-widget-header"&gt;
&lt;span class="audio-widget-icon"&gt;🎧&lt;/span&gt;
&lt;span class="audio-widget-label"&gt;Listen to this article&lt;/span&gt;
&lt;/div&gt;
&lt;audio controls preload="metadata"&gt;
&lt;source src="https://tinycomputers.io/physics-first-ml-why-ai-should-correct-not-replace-scientific-models_tts.mp3" type="audio/mpeg"&gt;
&lt;/source&gt;&lt;/audio&gt;
&lt;div class="audio-widget-footer"&gt;16 min · AI-generated narration&lt;/div&gt;
&lt;/div&gt;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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