speedandcurrent 2.3.4 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -66,7 +66,7 @@ Once configured, all consumers on the Signal K bus — KIP, OpenCPN, Instrument
66
66
  | `environment.current.drift` | m/s | Estimated current speed. |
67
67
  | `environment.current.setTrue` | rad | Estimated current direction (the direction the water moves *toward*). |
68
68
 
69
- A 60-second stabilisation period applies after startup. No output is published during this window.
69
+ A 60-second stabilisation period applies after startup. Corrected boatspeed may be published during this window, but current estimation and table learning wait until it ends.
70
70
 
71
71
  ---
72
72
 
@@ -104,7 +104,7 @@ The **Inputs** section shows live readings from the raw sensors as they arrive f
104
104
 
105
105
  | Setting | Default | Description |
106
106
  |---------|---------|-------------|
107
- | **Staleness Detection** | On | Mark an input as unavailable when it stops updating. Disable when testing the plugin with simulated or replayed data, where timing is irregular and would otherwise trigger spurious warnings. |
107
+ | **Input staleness handling** | Built in | Inputs are marked stale when they stop updating, and the webapp displays a warning. `navigation.state` is an event path and is not treated as stale between state transitions. |
108
108
 
109
109
  ---
110
110
 
@@ -144,23 +144,20 @@ The **Correction Table Learning** section has its own independent toggle. Learni
144
144
 
145
145
  The panel shows the **smoothed** sensor inputs used for table updates — heading, boat speed, ground speed, and attitude. These are the same signals as in the Estimation panel, but averaged over the smoother window before being fed into the learning algorithm. This averaging reduces the influence of short-term fluctuations on the table update.
146
146
 
147
- When **Assume Current** is enabled, the smoothed current estimate is also shown as a learning input.
148
-
149
147
  Warnings appear here if any smoothed input is unavailable.
150
148
 
151
- The panel also shows whether learning is currently active, suspended, or skipped for the latest observation. Learning is automatically suspended when `navigation.state` is `anchored` or `moored` when that path is available. It can also be suspended for `motoring` if enabled in the settings. Observations below the current table speed-step threshold are skipped.
149
+ The panel also shows whether learning is currently active, suspended, or skipped for the latest observation. Learning is normally gated by SOG: observations below the current table speed-step threshold are skipped. When **Suspend on navigation.state = motoring** is enabled and `navigation.state` is known, `anchored`, `moored`, and `motoring` suspend learning; an unavailable or unknown state falls back to the SOG gate.
152
150
 
153
151
  ### Settings
154
152
 
155
153
  | Setting | Default | Description |
156
154
  |---------|---------|-------------|
157
155
  | **Update Correction Table** | On | Master toggle. Allow the table to update from current observations. |
158
- | **Suspend on navigation.state = motoring** | Off | Suspend learning when `navigation.state` reports `motoring`. `anchored` and `moored` always suspend learning when that path is available. |
159
- | **Stability (1–20)** | 7 | How quickly the correction table adapts to new observations. Higher = slower, more conservative. Lower = faster but noisier. See the technical section for detail. |
160
- | **Assume Current (experimental)** | Off | Include the running current estimate in the table update calculation. Only enable once the current estimate has had time to stabilise and tidal conditions are relatively steady. |
156
+ | **Suspend on navigation.state = motoring** | Off | When enabled, suspend learning when a known `navigation.state` reports `anchored`, `moored`, or `motoring`. When disabled, `navigation.state` does not override the SOG-based moving check. |
157
+ | **Correction drift** | 0.3 knots/month | How quickly the correction may change over time (knots/month). 0.3 knots/month means the correction can typically change by about 0.3 knots over a month. Higher values adapt faster but may be less stable. Range: 0.0–3.0 knots/month. |
161
158
  | **Show Statistics (σ)** | Off | Display standard deviation alongside each smoothed value. Useful for spotting noisy sensors. |
162
159
 
163
- ### Smoother settings
160
+ ### Smoothing and learning cadence
164
161
 
165
162
  Controls how raw sensor values are averaged before being used for table updates. These settings have no effect on the published corrected values — only on the inputs to the learning algorithm.
166
163
 
@@ -168,12 +165,9 @@ The smoother serves a second purpose beyond noise reduction: its output variance
168
165
 
169
166
  | Setting | Default | Description |
170
167
  |---------|---------|-------------|
171
- | **Smoother Type** | Moving average | Moving Average (window), Exponential decay (τ), or Kalman filter. Moving average is the most predictable. |
172
- | **Window size** | 5 s | (Moving average) Integration window. Larger = smoother but slower to respond. Minimum 2 s. |
173
- | **Time constant (τ)** | 3 s | (Exponential) Decay time constant. Larger = smoother. Minimum 1 s. |
174
- | **Kalman gain** | 0.2 | (Kalman) Steady-state gain. Lower = smoother / slower. Range 0.01–0.99. |
168
+ | **Window size** | 5 s | Moving-average integration window. Larger = smoother but slower to respond. Minimum 5 s. |
175
169
 
176
- A 5-second moving average window suits typical 1 Hz instrument update rates. In rough conditions with high sensor noise, increasing it to 8–10 seconds may improve table quality at the cost of temporal resolution.
170
+ A 5-second moving average window suits typical 1 Hz instrument update rates. Learning attempts occur every `window + 1 second`, leaving a one-second gap between observation windows. In rough conditions with high sensor noise, increasing the window to 8–10 seconds may improve table quality at the cost of temporal resolution. Older configurations that selected another smoother are silently converted to moving average; their valid window duration is retained and shorter values are clamped to 5 seconds.
177
171
 
178
172
  ---
179
173
 
@@ -183,16 +177,19 @@ The **Correction Table** section displays the active correction table. Rows are
183
177
 
184
178
  ### Reading the table
185
179
 
186
- Each cell that has received at least one observation shows two values:
180
+ Each cell that has received at least one observation shows its correction values. Cells participating in the latest estimate also show their normalized fusion weight as a blue bar along the bottom:
181
+
182
+ - **Speed factor** (e.g. `+3.2%` or `−1.5%`): how much faster or slower the true speed is compared to what the paddle wheel reads.
183
+ - **Leeway** (e.g. `+4°`): the observed lateral correction for that speed/heel combination. Positive is starboard.
184
+ - **Fusion weight**: the blue bar is that cell's normalized share of the current precision-weighted estimate. A full-width bar represents 100%, and the displayed weights sum to approximately 100%.
187
185
 
188
- - **Speed factor** (e.g. `+3.2%` or `−1.5%`): how much faster or slower the true speed is compared to what the paddle wheel reads. **Green** = paddle wheel reads slow (the correction adds speed). **Orange** = paddle wheel reads fast (the correction reduces speed). Colour intensity scales with the magnitude of the factor relative to the largest factor in the table.
189
- - **Leeway** (e.g. `+4°`): the observed lateral correction for that speed/heel combination. Positive is starboard. When leeway is non-zero the cell background shows diagonal stripes; the stripe angle encodes the leeway direction visually.
186
+ Cell backgrounds encode the speed factor: green means the paddlewheel reads slow and orange means it reads fast. When leeway is available, stripe angle encodes its direction.
190
187
 
191
188
  Empty cells have not yet received any observations and show no correction.
192
189
 
193
- ### Active cell and neighbours
190
+ ### Active cell
194
191
 
195
- The **active cell** — the one most recently updated by an incoming STW sample — is highlighted with a bold border and blue text. The **neighbour cells** contributing to the current interpolation are shown with a faint tint. Watching these as you sail shows exactly which part of the table is being applied and updated at any moment.
192
+ The **active cell** — the one most recently updated by an incoming STW sample — is shown with a black border.
196
193
 
197
194
  ### Table management
198
195
 
@@ -205,13 +202,19 @@ The **active cell** — the one most recently updated by an incoming STW sample
205
202
 
206
203
  Multiple tables can coexist on disk; only the active one is used. This makes it straightforward to keep separate tables for different configurations such as racing vs. cruising sails, or before and after antifouling.
207
204
 
205
+ ### Correction-table file formats
206
+
207
+ The original format is schema version 1. It remains available as [`docs/correction-table-v1.schema.json`](docs/correction-table-v1.schema.json), while [`docs/correction-table.schema.json`](docs/correction-table.schema.json) is retained as the compatible legacy filename. New and saved tables use schema version 2, documented in [`docs/correction-table-v2.schema.json`](docs/correction-table-v2.schema.json). Version 2 adds a top-level `schemaVersion: 2`, the current serializable model descriptor, and `lastAcceptedAt` epoch milliseconds on every cell; empty cells use `null`.
208
+
209
+ Version 1 files, including older files without a version field, are migrated silently on load. Since individual legacy cell ages are unknown, the file modification time is assigned to every learned cell as a conservative approximation. If that timestamp is unavailable, load time is used and the fallback is written only to debug logging. Migration preserves table identity, dimensions, display attributes, means, covariance, and indices, and is persisted atomically only after the table has loaded successfully.
210
+
208
211
  ---
209
212
 
210
213
  ## Operating notes
211
214
 
212
215
  **Be patient with the correction table.** A fresh table has no data and produces no corrections. Cover a range of speeds and heel angles over a few sails and the table fills in progressively. Upwind sailing covers the heel bins well; downwind and reaching fill the low-heel, varying-speed bins.
213
216
 
214
- **The table learns while sailing normally.** No dedicated calibration runs are needed. Just sail with **Update Correction Table** on. Learning pauses automatically during startup stabilisation, when `navigation.state` indicates `anchored` or `moored`, optionally when it indicates `motoring`, and when SOG or STW are below the current table speed-step threshold.
217
+ **The table learns while sailing normally.** No dedicated calibration runs are needed. Just sail with **Update Correction Table** on. Learning pauses automatically during startup stabilisation and when SOG or STW are below the current table speed-step threshold. If **Suspend on navigation.state = motoring** is enabled, a known `navigation.state` of `anchored`, `moored`, or `motoring` also pauses learning.
215
218
 
216
219
  **Port and starboard are tracked independently.** Heel is signed: starboard positive, port negative. An asymmetric paddle wheel installation will show different corrections on each tack, and the table captures this naturally.
217
220
 
@@ -239,17 +242,42 @@ When a new paddle wheel sample arrives the plugin:
239
242
 
240
243
  ### Neighbour interpolation
241
244
 
242
- Rather than bi-linear interpolation the plugin uses **inverse-distance weighting** over the five nearest cells in speed/heel space:
245
+ The plugin combines learned cells using both their Kalman covariance and their distance from the requested speed and heel. Speed and heel distances are divided by their respective grid steps, so distance is measured in dimensionless cell units. A cell participates only when:
246
+
247
+ ```text
248
+ cell index > 0
249
+ cell distance <= 2.5
250
+ ```
251
+
252
+ For each participating cell, distance is converted into additional uncertainty:
243
253
 
244
- > wᵢ = 1 / (dᵢ + ε)
254
+ ```text
255
+ effective covariance = cell covariance + q * distance² * identity matrix
256
+ ```
245
257
 
246
- where dᵢ is the Euclidean distance from the query point to cell centre i in normalised speed/heel space. Only cells that have received at least one observation (N > 0) contribute.
258
+ The inverse effective covariance is the cell's precision. The correction and its covariance are obtained by full 2×2 precision fusion:
247
259
 
248
- The final correction is the normalised weighted average:
260
+ ```text
261
+ combined precision = sum(inverse(effective covariance))
262
+ correction covariance = inverse(combined precision)
263
+ correction = correction covariance
264
+ * sum(inverse(effective covariance) * cell correction)
265
+ ```
249
266
 
250
- > x̂ = Σ(wᵢ · xᵢ) / Σwᵢ, ŷ = Σ(wᵢ · yᵢ) / Σwᵢ
267
+ This gives nearby, mature cells the greatest influence. A fresh table borrows information more broadly because cell covariance is still large; a mature table becomes more local as its cell covariance falls. If no eligible cell exists, the plugin applies zero correction rather than extrapolating beyond the configured radius.
251
268
 
252
- The variance of each axis is propagated through the same weighting, so cells with low confidence (high covariance) influence the result less. If only one or two cells have data, the correction extrapolates gradually to nearby conditions. As more cells fill in, the correction becomes tighter and more local.
269
+ #### Automatic spatial variance (`q`)
270
+
271
+ `q` describes how much genuine correction variation is expected between adjacent cells, in correction variance per squared cell. It is calculated without user input from horizontal and vertical pairs where both cells have an index greater than 0. A cell with one accepted observation is therefore eligible while the explicit maturity threshold remains available for future tuning. For each adjacent pair:
272
+
273
+ ```text
274
+ pair q = (
275
+ squared distance between the two correction vectors
276
+ - trace(cell covariance 1 + cell covariance 2)
277
+ ) / 2
278
+ ```
279
+
280
+ The table uses the median pair estimate, which limits the effect of isolated rough or immature cells. At least three eligible adjacent pairs and a positive median are required. A zero or negative estimate means the observed differences do not resolve spatial variation above the cells' estimated uncertainty; because using zero would disable distance weighting, the plugin then uses a conservative fallback of `0.002 (m/s)²` per squared cell. `q` is calculated when a table is loaded and recalculated after every 600 accepted learning observations. The applied `q`, raw estimate, source, and supporting pair count are runtime diagnostics and are not saved in the correction-table file.
253
281
 
254
282
  ### How the correction table is populated
255
283
 
@@ -257,43 +285,44 @@ Each cell is an independent **2-dimensional Kalman filter** tracking the correct
257
285
 
258
286
  Every time conditions are right — plugin running for >60 seconds, smoothers settled, speed above minimum threshold — the plugin computes an **observation** of what the correction should be:
259
287
 
260
- > observation = R(ψ)⁻¹ · V_SOG − R(ψ)⁻¹ · V_current − V_STW
288
+ ```text
289
+ observation = R(ψ)⁻¹ · V_SOG − R(ψ)⁻¹ · V_current − V_STW
290
+ ```
261
291
 
262
- where R(ψ) rotates from ground frame to boat frame using true heading ψ. In plain terms: rotate GPS velocity into the boat frame, subtract the current estimate (also rotated), subtract the raw paddle wheel velocity. The residual is the implied sensor error for the current speed and heel.
292
+ where `R(ψ)` rotates from ground frame to boat frame using true heading `ψ`. In plain terms: rotate GPS velocity into the boat frame, subtract the raw paddle wheel velocity. The residual is the implied sensor error for the current speed and heel.
263
293
 
264
294
  The Kalman update combines this observation with the cell's existing belief:
265
295
 
266
- > K = P · (P + R_obs)⁻¹
267
- > x_new = x_old + K · (observation − x_old)
296
+ ```text
297
+ K = P · (P + R_obs)⁻¹
298
+ x_new = x_old + K · (observation − x_old)
299
+ ```
268
300
 
269
- where P is the cell's current covariance and R_obs is the observation covariance derived from the measurement uncertainty of all contributing signals (SOG variance + current variance + STW variance, rotated appropriately). **Noisy observations produce a smaller gain and move the cell estimate less.**
301
+ where P is the cell's current covariance and R_obs is the observation covariance derived from the measurement uncertainty of all contributing signals (SOG variance + STW variance, rotated appropriately). It also includes heading uncertainty in radians squared. For $u = R(-heading)(groundSpeed - current)$, the heading contribution is $J \sigma_h^2 J^T$, where $J = [u_y, -u_x]^T$. This is applied once to the combined ground-speed-minus-current vector because both vectors share the same heading error. **Noisy observations produce a smaller gain and move the cell estimate less.**
270
302
 
271
- ### The stability setting in detail
303
+ ### Time-scaled aging and correction drift
272
304
 
273
- Each cell has a small **process noise** that allows it to drift slowly over time, reflecting that a paddle wheel's error can change with fouling, recalibration, or crew weight distribution. Process noise is:
305
+ Each cell has a small **process-noise rate** that allows it to drift slowly over time, reflecting that a paddle wheel's error can change. For an elapsed interval `dt`, the process covariance added to each diagonal is:
274
306
 
275
- > Q = 10^(−stability)
307
+ ```text
308
+ effectiveDt = min(dt, 90 days)
309
+ Q(dt) = Q_rate × effectiveDt × I
310
+ ```
276
311
 
277
- With stability = 7 (default), Q = 10⁻⁷, making the cell very resistant to change — it effectively averages hundreds of observations before settling. Stability 4–5 makes the table react faster to recent conditions; stability 10–12 is appropriate for a well-characterised boat that changes rarely.
312
+ The user-facing `Correction drift` value is expressed in knots per month, using a 30-day month. Internally, it is converted to the SI process-noise rate in $(m/s)^2/s$. The default 0.3 knots/month corresponds to approximately $9.2 × 10^{-9}\ (m/s)^2/s$. Offline time counts toward aging, but aging stops after the internal 90-day cap. Aged covariance is calculated when gating, filtering, interpolation, spatial-q estimation, and reporting; stored covariance is not mutated by reads. Rejected observations do not advance timestamps; accepted observations store the posterior covariance and current UTC epoch-millisecond timestamp.
278
313
 
279
- In practical terms: high stability = trust the accumulated history; low stability = trust recent observations more.
314
+ In practical terms: a lower correction-drift rate trusts accumulated history more; a higher rate allows the table to adapt more quickly to long-term sensor changes.
280
315
 
281
316
  ### How current is estimated
282
317
 
283
318
  Current is estimated as:
284
319
 
285
- > V_current = V_SOG − R(ψ) · V_STW_corrected
320
+ ```text
321
+ V_current = V_SOG − R(ψ) · V_STW_corrected
322
+ ```
286
323
 
287
324
  The corrected STW vector is rotated into the ground frame using heading, then subtracted from the GPS velocity. The residual is the water velocity.
288
325
 
289
326
  This raw estimate is fed into a **Kalman smoother with very low process noise** (process variance ≈ 10⁻⁶), so it changes very slowly, integrating over many minutes rather than chasing individual GPS fluctuations. At startup the estimate is strongly initialised to zero.
290
327
 
291
328
  Current estimation requires an accurate boat speed, so it is gated by the 60-second stabilisation period and only runs when **Estimate Boat Speed** is enabled.
292
-
293
- ### Current and table learning
294
-
295
- Current estimation always runs alongside speed correction — it is a direct byproduct of comparing the GPS velocity with the corrected STW vector. You cannot have one without the other.
296
-
297
- For table *learning*, however, current can be left out of the equation without significant harm. When a boat sails different headings over time — tacking, gybing, reaching — any steady current appears as an error in one direction on one heading and the opposite direction on another. These errors cancel in the long-term average, so the table converges on the correct speed correction regardless. This is why the default (**Assume Current** off) uses a zero-current placeholder rather than the live estimate.
298
-
299
- Including the current estimate (**Assume Current** on) would be more accurate in principle, but it introduces a dependency: the current estimate is only as good as the speed correction that produced it. Early in the table's life, when corrections are rough, the current estimate is also rough, and feeding it back into learning can amplify rather than reduce error. There is also a circularity: better speed correction → better current estimate → better speed correction. Enabling this before the table has had time to settle can cause the two to pull each other in the wrong direction, particularly in changing tidal conditions. Use it only once the table is reasonably well populated and current conditions are stable.