<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: NO-DO</title>
    <description>The latest articles on DEV Community by NO-DO (@nododev).</description>
    <link>https://dev.to/nododev</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4245912%2Fbf846dc8-7512-4678-8393-1e2a5b21a31c.png</url>
      <title>DEV Community: NO-DO</title>
      <link>https://dev.to/nododev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/nododev"/>
    <language>en</language>
    <item>
      <title>Your "went dark" alert is probably a coverage hole: sizing the missed-window rule for AIS</title>
      <dc:creator>NO-DO</dc:creator>
      <pubDate>Sun, 15 Nov 2026 17:13:17 +0000</pubDate>
      <link>https://dev.to/nododev/your-went-dark-alert-is-probably-a-coverage-hole-sizing-the-missed-window-rule-for-ais-57b7</link>
      <guid>https://dev.to/nododev/your-went-dark-alert-is-probably-a-coverage-hole-sizing-the-missed-window-rule-for-ais-57b7</guid>
      <description>&lt;p&gt;If you watch ships through a free AIS feed, sooner or later you'll want an alert for "this vessel stopped transmitting". The obvious rule is something like &lt;em&gt;not received for 3 consecutive windows&lt;/em&gt;. We shipped roughly that, measured it, and it was wrong a lot of the time.&lt;/p&gt;

&lt;h3&gt;
  
  
  How much of the traffic does one window see?
&lt;/h3&gt;

&lt;p&gt;Terrestrial AIS from community receivers is patchy even in busy water. We took 2-minute windows from a free stream and, for each window, looked at vessels we knew had been underway nearby during the previous hour. On 6 November 2026 a single window caught about &lt;strong&gt;36%&lt;/strong&gt; of them in the English Channel and &lt;strong&gt;54%&lt;/strong&gt; in the Strait of Gibraltar.&lt;/p&gt;

&lt;p&gt;So "missing from the last window" is the normal state for a large share of ships.&lt;/p&gt;

&lt;h3&gt;
  
  
  What a fixed rule does with that
&lt;/h3&gt;

&lt;p&gt;If a ship is received in any given window with probability p, and windows are roughly independent, the chance a perfectly ordinary ship is missed n windows in a row is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="no"&gt;P&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;false&lt;/span&gt; &lt;span class="n"&gt;alarm&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;1&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;**&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;With n = 3 and p = 0.36, that's 0.64³ ≈ 0.26. About one ordinary ship in four "goes dark" in the Channel.&lt;/p&gt;

&lt;h3&gt;
  
  
  Sizing n from the local receive rate
&lt;/h3&gt;

&lt;p&gt;Pick the false-alarm rate you can live with (we use 1%) and solve for n:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;

&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;min_missed_windows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;false_alarm&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&gt;float&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="n"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kt"&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="kt"&gt;float&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="s2"&gt;"""Consecutive missed windows needed before flagging, given local receive rate p."""&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&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="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;inf&lt;/span&gt;          &lt;span class="c1"&gt;# nothing is received here: never flag&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;floor&lt;/span&gt;
    &lt;span class="k"&gt;return&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;floor&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ceil&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;false_alarm&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="n"&gt;math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&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;p&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;p&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="p"&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="mf"&gt;0.36&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.54&lt;/span&gt;&lt;span class="p"&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="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="nf"&gt;min_missed_windows&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;print&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="s2"&gt;"p={p:.2f}  n={n}  fixed-3 false alarm={(1 - p) ** 3:.1%}"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p=0.20  n=21  fixed-3 false alarm=51.2%
p=0.36  n=11  fixed-3 false alarm=26.2%
p=0.54  n=6  fixed-3 false alarm=9.7%
p=0.80  n=3  fixed-3 false alarm=0.8%
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The useful part is that p isn't a global constant. We estimate it per alert, near where the ship should be:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;p = vessels received within 25 km in this window / vessels seen within 25 km in the last hour
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Low p means you need to wait longer before saying anything; p = 0 means you have no basis for an alert at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  The other three checks
&lt;/h3&gt;

&lt;p&gt;The window count is only one of four conditions we require:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;It was moving&lt;/strong&gt; at its last fix: over 1 knot and not reporting "at anchor" or "moored" (nav status 1 or 5). Parked ships drop out for boring reasons.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;We know where to look.&lt;/strong&gt; Dead-reckon from the last fix (speed × elapsed time along course), capped at 6 hours. Past that the estimated position is fiction.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Others are being received there.&lt;/strong&gt; At least 5 other vessels received within 25 km of the estimate in the current window. Otherwise it's a hole in coverage, not a quiet ship.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Enough missed windows,&lt;/strong&gt; sized as above, never fewer than 3.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;We apply it to class A transponders only; class B reception is too irregular for this to mean much.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the alert can and can't say
&lt;/h3&gt;

&lt;p&gt;Even with all four conditions, the honest wording is &lt;em&gt;no AIS reception despite apparent coverage&lt;/em&gt;. A dead transponder, a power fault or a bad antenna look identical to a deliberate switch-off. The alert tells you where to spend analyst time next: a same-day Sentinel-2 pass if the sky is clear, Sentinel-1 radar if it isn't.&lt;/p&gt;

&lt;h3&gt;
  
  
  Where the formula is optimistic
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;Windows aren't fully independent: a receiver that goes offline takes out several in a row. That makes the fixed rule even worse than the formula says, and our n slightly optimistic.&lt;/li&gt;
&lt;li&gt;The 25 km radius is a judgment call. Too small and you rarely have 5 neighbours; too large and you count ships a different receiver is hearing.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the logic behind the vessel watch in &lt;a href="https://tracez.no-do.dev/?utm_source=devto&amp;amp;utm_medium=organic&amp;amp;utm_campaign=ais-gap" rel="noopener noreferrer"&gt;TRACEZ&lt;/a&gt;, the investigation tool we build at NO-DO. If you run something similar and picked different thresholds, we'd like to compare notes in the comments.&lt;/p&gt;

</description>
      <category>osint</category>
      <category>python</category>
      <category>statistics</category>
      <category>gis</category>
    </item>
  </channel>
</rss>
