neckcompass 1.0.0__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Amandeep Singh Bains
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,10 @@
1
+ include LICENSE
2
+ include README.md
3
+ include pyproject.toml
4
+ recursive-include neckcompass *.py
5
+ recursive-include csrc *.h *.c Makefile
6
+ prune assets
7
+ prune csrc/build
8
+ prune results
9
+ prune scripts
10
+ prune tests
@@ -0,0 +1,269 @@
1
+ Metadata-Version: 2.4
2
+ Name: neckcompass
3
+ Version: 1.0.0
4
+ Summary: NeckCompass: Drosophila Central Complex ring-attractor compass and GPS-denied path integrator with natural ferromagnetic spike rejection
5
+ Author: Amandeep Singh Bains
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/indian-aman/neckcompass
8
+ Project-URL: Repository, https://github.com/indian-aman/neckcompass
9
+ Project-URL: Releases, https://github.com/indian-aman/neckcompass/releases
10
+ Project-URL: Issues, https://github.com/indian-aman/neckcompass/issues
11
+ Project-URL: Documentation, https://github.com/indian-aman/neckcompass#readme
12
+ Keywords: neckcompass,necklink,ring-attractor,central-complex,drosophila,compass,ahrs,magnetometer,imu,path-integration,gps-denied,robotics,usv,auv,esp32
13
+ Classifier: Development Status :: 5 - Production/Stable
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Science/Research
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: C
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.9
20
+ Classifier: Programming Language :: Python :: 3.10
21
+ Classifier: Programming Language :: Python :: 3.11
22
+ Classifier: Programming Language :: Python :: 3.12
23
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
24
+ Classifier: Topic :: Scientific/Engineering :: Bio-Informatics
25
+ Classifier: Topic :: System :: Hardware :: Hardware Drivers
26
+ Requires-Python: >=3.9
27
+ Description-Content-Type: text/markdown
28
+ License-File: LICENSE
29
+ Requires-Dist: numpy>=1.24
30
+ Provides-Extra: dev
31
+ Requires-Dist: pytest>=7.0; extra == "dev"
32
+ Requires-Dist: matplotlib>=3.7; extra == "dev"
33
+ Requires-Dist: build>=1.0; extra == "dev"
34
+ Requires-Dist: twine>=5.0; extra == "dev"
35
+ Dynamic: license-file
36
+
37
+ <div align="center">
38
+
39
+ # NeckCompass (`neckcompass` v1.0.0)
40
+
41
+ **Biophysical *Drosophila* Central Complex ring-attractor compass and GPS-denied path integrator with natural ferromagnetic spike rejection — in Python and zero-heap, zero-float ANSI C99 (`316 B` RAM, `~1 us`/step).**
42
+
43
+ [![PyPI](https://img.shields.io/pypi/v/neckcompass?style=flat-square&logo=pypi&logoColor=white&label=PyPI%20neckcompass&color=3775A9)](https://pypi.org/project/neckcompass/)
44
+ [![Release](https://img.shields.io/badge/release-v1.0.0-0EA5E9?style=flat-square)](https://github.com/indian-aman/neckcompass/releases/tag/v1.0.0)
45
+ [![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://pypi.org/project/neckcompass/)
46
+ [![ANSI C99](https://img.shields.io/badge/C99-zero--heap%20%7C%20zero--float%20%7C%20316%20B%20RAM-00599C?style=flat-square&logo=c&logoColor=white)](csrc/)
47
+ [![C99 Self-Tests](https://img.shields.io/badge/C99%20Q15%20tests-6%2F6%20passed-10B981?style=flat-square)](csrc/tools/neckcompass_selftest.c)
48
+ [![Pytest](https://img.shields.io/badge/pytest-19%2F19%20passed-10B981?style=flat-square)](tests/test_neckcompass.py)
49
+ [![License: MIT](https://img.shields.io/badge/license-MIT-F59E0B?style=flat-square)](LICENSE)
50
+
51
+ [**30-Second Quick Start**](#-30-second-quick-start) ·
52
+ [**Plastic Harbor Calibration**](#2-online-soft-iron--harbor-distortion-learning-plasticlandmarkring) ·
53
+ [**Zero-Float ANSI C99 MCU Core**](#3-zero-heap-zero-float-ansi-c99-core-csrc) ·
54
+ [**Honest Benchmarks**](#-honest-empirical-benchmarks-5-seeds--50-hz) ·
55
+ [**When to Use (and When Not To)**](#-when-to-use-neckcompass--and-when-not-to)
56
+
57
+ </div>
58
+
59
+ <p align="center">
60
+ <img src="https://raw.githubusercontent.com/indian-aman/neckcompass/main/assets/neckcompass_benchmarks.png" alt="NeckCompass v1.0.0 Ring Attractor & Path Integrator Benchmarks" width="100%">
61
+ </p>
62
+
63
+ ---
64
+
65
+ ## Why NeckCompass?
66
+
67
+ Marine USVs, inspection drones, subsea rovers, and factory AGVs operate around steel ship hulls, sheet-pile docks, high-current motor leads, and buried power cables that inject intermittent **`70°–150°` ferromagnetic heading spikes** lasting `0.5–2.0 s`. Standard linear-Gaussian estimators break down in this regime:
68
+
69
+ - **Gyro Dead-Reckoning** ignores magnetic anomalies completely, but MEMS gyro bias (`~0.8–1.5°/s`) drifts unboundedly (`50°–100°+` within minutes).
70
+ - **2-State Circular Extended Kalman Filters (`[heading, gyro_bias]`)** are minimum-variance optimal under textbook Gaussian noise, **but** a `120°` ferromagnetic anomaly feeds through the Kalman cross-covariance straight into the **gyro bias state**, causing both massive heading yanks (`45°+` P95) and post-spike bias hangovers.
71
+
72
+ **NeckCompass** ports the neural navigation circuit of the fruit-fly (*Drosophila melanogaster*) **Central Complex** into a deterministic software and microcontroller library — **Product #3** in the **[NeckLink](https://github.com/indian-aman/necklink)** fruit-fly brain engineering family:
73
+
74
+ 1. **Ellipsoid Body (`E-PG`) Ring Attractor + `Delta7` Inhibition + `P-EN` Shifters (`RingCompass`)**: Maintains a single topological activity bump across `N=16` (or `N=32`) circular wedges with **zero `-π ↔ +π` wrap-around discontinuity** (`< 10⁻¹⁵ rad` harmonic aliasing on even-`N` rings). Asymmetric Left/Right `P-EN` projections integrate IMU yaw rate (`omega_rad_s * dt`), while localized `R-neuron` sensory cues (`mag_heading_rad`, `sun_heading_rad`) undergo cooperative dendritic coincidence gating against the active `E-PG` bump — smoothly anchoring gyro drift inside the attractor basin while naturally shunting **`70°–150°` ferromagnetic spikes** (`10.2×` lower RMSE than 2-State EKF).
75
+ 2. **Associative `R-Neuron → E-PG` Synaptic Plasticity (`PlasticLandmarkRing`)**: Movement-gated Hebbian / anti-Hebbian ring weight matrix that learns warped local soft-iron and hard-iron magnetic distortion tables online around steel harbors or indoor rooms — with or without an external sun/visual reference.
76
+ 3. **Fan-Shaped Body `CPU4` Path Integrator + `PFL3`/`PFL2` Steering (`FanBodyNavigator`)**: `N=16` push-pull opponent memory columns integrate translational speed along the live `E-PG` compass bump with common-mode cancellation, computing exact `(distance_m, home_bearing_rad)` and `±67.5°` phase-shifted `PFL3` + `180°` `PFL2` motor yaw commands to steer straight home during complete **GPS blackouts** (`0.84 m` miss after a `1,125 m` spiky mission).
77
+ 4. **Zero-Heap, Zero-Float ANSI C99 Core (`csrc/`)**: Pure `Q15` / `int32_t` 16-wedge ring attractor + `CPU4`/`PFL3` navigator using **`316 bytes` of struct RAM** (`576 bytes` total including `260 B` Flash LUTs), **zero `malloc`**, **zero floating-point instructions**, and **`~1.0 us` per update** — ready for `$4` `ESP32`, `STM32`, `RP2040`, and flight controllers.
78
+
79
+ ```mermaid
80
+ flowchart LR
81
+ IMU["IMU Gyro Yaw Rate (omega_rad_s)"] --> PEN["P-EN Left/Right Velocity Shifters"]
82
+ PEN --> EPG["16-Wedge E-PG Ring Attractor + Delta7 Inhibition"]
83
+ MAG["Spiky Magnetometer / Sun Azimuth"] --> RN["R2 / R4d Ring Neurons (Plastic W + Basin Gate)"]
84
+ RN -->|"Coherent cues anchor drift; 70-150 deg spikes shunted"| EPG
85
+ EPG --> CPU4["16 Opponent CPU4 Memory Columns (+ Speed)"]
86
+ CPU4 --> PFL3["PFL3 (+/-67.5 deg) + PFL2 (180 deg) Steering"]
87
+ PFL3 --> HOME["GPS-Denied Homebound Yaw Command"]
88
+ ```
89
+
90
+ ---
91
+
92
+ ## ⚡ 30-Second Quick Start
93
+
94
+ ### 1. Python (`pip install neckcompass`)
95
+
96
+ ```bash
97
+ pip install neckcompass
98
+ # Or directly from source:
99
+ pip install git+https://github.com/indian-aman/neckcompass.git
100
+ ```
101
+
102
+ Fuse a biased IMU gyro with a spiky magnetometer and steer home through a complete GPS blackout in 14 lines:
103
+
104
+ ```python
105
+ import math
106
+ from neckcompass import RingCompass, FanBodyNavigator
107
+
108
+ compass = RingCompass(n_wedges=16, initial_heading_rad=0.0)
109
+ nav = FanBodyNavigator(n_columns=16, compass=compass)
110
+
111
+ # Outbound step @ 50 Hz (dt = 0.02 s): 125-deg hull spike is rejected automatically!
112
+ heading_rad, dist_m, home_bearing_rad = nav.step_with_compass(
113
+ omega_rad_s=0.15,
114
+ speed_m_s=1.5,
115
+ dt=0.02,
116
+ mag_heading_rad=math.radians(125.0), # Transient ferromagnetic spike -> gated out
117
+ )
118
+
119
+ # During GPS blackout, generate bounded [-0.8, +0.8] rad/s yaw command back to (0, 0):
120
+ yaw_rate_cmd = nav.steering_cmd(max_turn_rate=0.8)
121
+ ```
122
+
123
+ Run the built-in CLI demo or head-to-head quick benchmark from your terminal:
124
+
125
+ ```bash
126
+ neckcompass demo
127
+ neckcompass benchmark-quick
128
+ ```
129
+
130
+ ---
131
+
132
+ ### 2. Online Soft-Iron / Harbor Distortion Learning (`PlasticLandmarkRing`)
133
+
134
+ When operating repeatedly inside a steel marina slip or indoor robot bay with static nonlinear magnetic warp (`+20° sin(ψ) + 16° sin(2ψ)`), `PlasticLandmarkRing` learns the `R-neuron → E-PG` unwarping matrix online:
135
+
136
+ ```python
137
+ from neckcompass import PlasticLandmarkRing
138
+
139
+ plastic = PlasticLandmarkRing(n_wedges=16, n_ring_neurons=32)
140
+
141
+ # Self-supervised during normal turns (or pass reference_heading_rad=sun_rad when sky is visible)
142
+ corrected_heading = plastic.update(
143
+ omega_rad_s=imu_gz,
144
+ dt=0.02,
145
+ raw_mag_heading_rad=raw_mag_rad,
146
+ learn=True,
147
+ )
148
+ unwarped_mag_rad, confidence = plastic.map_raw_heading(raw_mag_rad)
149
+ ```
150
+
151
+ ---
152
+
153
+ ### 3. Zero-Heap, Zero-Float ANSI C99 Core (`csrc/`)
154
+
155
+ Drop [`csrc/neckcompass.h`](csrc/neckcompass.h) and [`csrc/neckcompass.c`](csrc/neckcompass.c) into any `ESP32`, `STM32`, `RP2040`, ArduPilot, or bare-metal firmware project (`-std=c99 -Wall -Wextra -pedantic -Werror`, zero FPU, zero `malloc`):
156
+
157
+ ```bash
158
+ make -C csrc clean all test
159
+ ```
160
+
161
+ ```c
162
+ #include "neckcompass.h"
163
+
164
+ nc_compass_t comp;
165
+ nc_navigator_t nav;
166
+
167
+ void autopilot_init(void) {
168
+ nc_compass_init(&comp);
169
+ nc_compass_reset(&comp, nc_cdeg_to_bam(0));
170
+ nc_navigator_init(&nav, 1500); /* 1.5 m smooth arrival taper */
171
+ }
172
+
173
+ /* 50 Hz deterministic fixed-point loop (~1.0 us on desktop, < 8 us on ESP32) */
174
+ int32_t autopilot_step_50hz(int32_t gyro_cdeg_s, int32_t mag_cdeg, int32_t speed_mm_s) {
175
+ nc_bam_t heading = nc_compass_update(
176
+ &comp,
177
+ nc_cdeg_to_bam(gyro_cdeg_s),
178
+ 20u, /* dt = 20 ms */
179
+ nc_cdeg_to_bam(mag_cdeg), 1u, NC_Q15_ONE,
180
+ 0, 0u, 0 /* optional sun/sky sensor */
181
+ );
182
+ nc_navigator_update(&nav, speed_mm_s, 20u, heading);
183
+ return nc_navigator_steering_cmd(&nav, heading, nc_cdeg_to_bam(4500));
184
+ }
185
+ ```
186
+
187
+ ---
188
+
189
+ ## 📊 Honest Empirical Benchmarks (5 Seeds @ `50 Hz`)
190
+
191
+ Reproduce all numbers and regenerate [`assets/neckcompass_benchmarks.png`](assets/neckcompass_benchmarks.png) + [`results/benchmark_summary.json`](results/benchmark_summary.json) locally:
192
+
193
+ ```bash
194
+ python scripts/benchmark_neckcompass.py
195
+ ```
196
+
197
+ All four estimators receive the **exact same** `50 Hz` sensor streams across 5 seeds (`101–105`): realistic MEMS gyro bias (`~0.9°/s` initial offset + `0.08°/s/√s` random walk + `1.5°/s` white noise) and `3.0°` Gaussian magnetometer noise. The **2-State Circular EKF** is given the exact true clean discrete-time process and measurement noise covariances `(Q, R)` with zero handicap.
198
+
199
+ ### 1. Heading Accuracy: Clean Gaussian Noise vs. `70°–150°` Ferromagnetic Spikes
200
+
201
+ Mean ± std across 5 seeds (`180 s` trajectories @ `50 Hz`):
202
+
203
+ | Estimator | **Scenario A: Clean Gaussian RMSE** | **Scenario A: P95** | **Scenario B: Spiky Mag RMSE** | **Scenario B: Spiky Mag P95** | **Scenario B: Max Error** |
204
+ | :--- | :---: | :---: | :---: | :---: | :---: |
205
+ | **`NeckCompass` (`E-PG` 16w)** | `0.43° ± 0.02°` | `0.86° ± 0.03°` | **`1.70° ± 0.18°`** | **`4.27° ± 0.37°`** | **`10.55° ± 1.88°`** |
206
+ | **`2-State Circular EKF`** | **`0.39° ± 0.02°`** *(wins)* | **`0.77° ± 0.03°`** | `17.42° ± 0.93°` | `45.28° ± 2.63°` | `82.53° ± 7.50°` |
207
+ | **`Complementary IMU+Mag`** | `3.64° ± 1.54°` | `5.34° ± 1.80°` | `11.80° ± 0.54°` | `27.77° ± 1.30°` | `47.45° ± 4.87°` |
208
+ | **`Gyro Dead-Reckoning`** | `90.22° ± 32.41°` | `141.70° ± 46.28°` | `84.80° ± 30.74°` | `139.91° ± 53.31°` | `145.98° ± 55.90°` |
209
+
210
+ ### 2. Scenario C: `600 s` Outbound Survey + Blind GPS-Blackout Return (`~1,125 m` Total Path)
211
+
212
+ A USV / rover executes a `10-minute` (`900 m`) multi-leg survey in complete GPS blackout (`2%` wheel/water-speed noise + biased MEMS gyro + intermittent `70°–150°` ferromagnetic spikes), then autonomously steers back to `(0, 0)`:
213
+
214
+ | Navigation Stack | **Return-to-Home Miss (`m`)** | **Miss `%` of `1.12 km` Path** | **RAM / State Footprint** | **Step Latency** |
215
+ | :--- | :---: | :---: | :---: | :---: |
216
+ | **`NeckCompass` (C99 `Q15` Core)** | **`0.32 m`** *(158 m selftest)* | **`0.20%`** | **`316 B` RAM (`576 B` w/ LUT)** | **`~1.04 us / step`** |
217
+ | **`NeckCompass` (Python `E-PG + CPU4`)** | **`0.84 ± 0.23 m`** | **`0.07% ± 0.02%`** | `3,143 B` | `171.26 us / step` |
218
+ | **`Complementary IMU+Mag + DR`** | `13.31 ± 3.08 m` | `1.19% ± 0.28%` | `~256 B` | `~6.5 us / step` |
219
+ | **`2-State Circular EKF + DR`** | `14.29 ± 6.95 m` | `1.28% ± 0.63%` | `5,792 B` *(Py)* | `13.58 us / step` *(Py)* |
220
+ | **`Gyro Dead-Reckoning`** | `256.74 ± 52.38 m` | `24.16% ± 5.69%` | `~128 B` | `~2.0 us / step` |
221
+
222
+ ---
223
+
224
+ ## 🎯 When to Use NeckCompass — and When Not To
225
+
226
+ > [!TIP]
227
+ > **Reach for NeckCompass when:**
228
+ > - **Operating near steel, motors, docks, ship hulls, or power cables**: Intermittent `60°–160°` ferromagnetic spikes yank standard complementary filters and corrupt EKF gyro-bias states; `RingCompass` shunts off-basin anomalies naturally (`1.70°` vs `17.42°` RMSE).
229
+ > - **Surviving GPS blackouts, jamming, or subsea/indoor dropouts**: `FanBodyNavigator` (`CPU4` + `PFL3`/`PFL2`) maintains a bounded, common-mode-cancelled home vector and smooth return controller over `10+ minute` (`> 1 km`) dead-reckoning missions.
230
+ > - **Deploying on tiny FPU-less or ultra-low-power microcontrollers (`$1–$4` MCUs)**: `csrc/neckcompass.c` uses `316 bytes` of RAM, `0` heap allocations, `0` floats, and `~1 us` per step.
231
+ > - **Learning warped harbor or indoor magnetic scenes online**: `PlasticLandmarkRing` unwarps repeatable soft-iron / hard-iron distortions continuously without offline ellipsoid-fitting batch scripts.
232
+
233
+ > [!NOTE]
234
+ > **Stick with a classical EKF / Factor Graph when:**
235
+ > - **Sensors are clean, pre-calibrated, and strictly Gaussian (e.g. open-air UAV far from iron/high-current leads)**: As shown in Scenario A, a properly tuned 2-State Circular EKF achieves slightly lower steady-state RMSE (`0.39°` vs `NeckCompass` `0.43°`) because linear Kalman filtering is minimum-variance optimal under pure Gaussian noise.
236
+ > - **Pure Python ultra-high-rate (`> 10 kHz`) loops without C bindings**: Scalar 2-State Python EKF runs in `~14 us/step` vs `~171 us/step` for 16-wedge NumPy vector ops (`~5.8 kHz`). For `> 5 kHz` embedded or C/C++ loops, compile [`csrc/neckcompass.c`](csrc/neckcompass.c) (`~1.0 us/step`).
237
+ > - **Full 6-DOF visual-inertial SLAM (`SE(3)` pose graphs + loop closure)**: `NeckCompass` is a lightweight 2D/yaw heading & home-vector module (`SO(2)` + `R²`), not a replacement for full 3D LiDAR/camera SLAM when rich 3D visual features and GPU/CPU compute are available.
238
+
239
+ ---
240
+
241
+ ## 🗂️ Repository Layout
242
+
243
+ | Path | Contents |
244
+ | :--- | :--- |
245
+ | [`neckcompass/ring_attractor.py`](neckcompass/ring_attractor.py) | `RingCompass` (`N=16`/`32` `E-PG` ring attractor + `P-EN` shifters + multi-cue basin gating) & `PlasticLandmarkRing` (`R-neuron → E-PG` Hebbian/anti-Hebbian distortion learner) |
246
+ | [`neckcompass/path_integrator.py`](neckcompass/path_integrator.py) | `FanBodyNavigator` (`N=16` opponent `CPU4` vector memory + `PFL3`/`PFL2` GPS-denied homebound steering) |
247
+ | [`neckcompass/cli.py`](neckcompass/cli.py) | `neckcompass` CLI (`neckcompass demo`, `neckcompass benchmark-quick`) |
248
+ | [`csrc/`](csrc/) | Zero-heap, zero-float ANSI C99 fixed-point `Q15` core (`neckcompass.h`, `neckcompass.c`, `Makefile`, and `6/6` `neckcompass_selftest.c`) |
249
+ | [`scripts/benchmark_neckcompass.py`](scripts/benchmark_neckcompass.py) | Reproducible 5-seed empirical benchmark & 3-panel hero chart generator |
250
+ | [`results/benchmark_summary.json`](results/benchmark_summary.json) | Exact 5-seed benchmark metrics and C99/Python latency & memory measurements |
251
+ | [`tests/test_neckcompass.py`](tests/test_neckcompass.py) | `19`-test `pytest` suite covering wrap-around continuity, spike rejection, soft-iron plasticity, GPS-denied return, and CLI |
252
+
253
+ ---
254
+
255
+ ## 📚 Scientific Foundations
256
+
257
+ 1. **Seelig, J. D. & Jayaraman, V.** (2015). Neural dynamics for landmark orientation and angular path integration. *Nature*, 521, 186–191.
258
+ 2. **Stone, T. et al.** (2017). An anatomically constrained model for path integration in the bee brain. *Current Biology*, 27(20), 3069–3085.
259
+ 3. **Turner-Evans, D. et al.** (2017). Angular velocity integration in a fly heading circuit. *eLife*, 6, e23496.
260
+ 4. **Fisher, Y. E., Lu, J., D'Alessandro, I. & Wilson, R. I.** (2019). Sensorimotor experience remaps visual input to a heading network. *Nature*, 576, 121–125.
261
+ 5. **Kim, S. S., Hermundstad, A. M., Romani, S., Abbott, L. F. & Jayaraman, V.** (2019). Generation of stable heading representations in diverse visual scenes. *Nature*, 576, 126–131.
262
+ 6. **Lyu, C., Abbott, L. F. & Maimon, G.** (2022). Building an allocentric travelling direction signal via vector computation. *Nature*, 601, 92–97.
263
+ 7. **Westeinde, E. A. et al.** (2024). Transforming a head direction signal into a goal-oriented steering command. *Nature*, 626, 819–826.
264
+
265
+ ---
266
+
267
+ ## License
268
+
269
+ Released under the [MIT License](LICENSE). Copyright (c) 2026 Amandeep Singh Bains.
@@ -0,0 +1,233 @@
1
+ <div align="center">
2
+
3
+ # NeckCompass (`neckcompass` v1.0.0)
4
+
5
+ **Biophysical *Drosophila* Central Complex ring-attractor compass and GPS-denied path integrator with natural ferromagnetic spike rejection — in Python and zero-heap, zero-float ANSI C99 (`316 B` RAM, `~1 us`/step).**
6
+
7
+ [![PyPI](https://img.shields.io/pypi/v/neckcompass?style=flat-square&logo=pypi&logoColor=white&label=PyPI%20neckcompass&color=3775A9)](https://pypi.org/project/neckcompass/)
8
+ [![Release](https://img.shields.io/badge/release-v1.0.0-0EA5E9?style=flat-square)](https://github.com/indian-aman/neckcompass/releases/tag/v1.0.0)
9
+ [![Python](https://img.shields.io/badge/python-3.9%2B-3776AB?style=flat-square&logo=python&logoColor=white)](https://pypi.org/project/neckcompass/)
10
+ [![ANSI C99](https://img.shields.io/badge/C99-zero--heap%20%7C%20zero--float%20%7C%20316%20B%20RAM-00599C?style=flat-square&logo=c&logoColor=white)](csrc/)
11
+ [![C99 Self-Tests](https://img.shields.io/badge/C99%20Q15%20tests-6%2F6%20passed-10B981?style=flat-square)](csrc/tools/neckcompass_selftest.c)
12
+ [![Pytest](https://img.shields.io/badge/pytest-19%2F19%20passed-10B981?style=flat-square)](tests/test_neckcompass.py)
13
+ [![License: MIT](https://img.shields.io/badge/license-MIT-F59E0B?style=flat-square)](LICENSE)
14
+
15
+ [**30-Second Quick Start**](#-30-second-quick-start) ·
16
+ [**Plastic Harbor Calibration**](#2-online-soft-iron--harbor-distortion-learning-plasticlandmarkring) ·
17
+ [**Zero-Float ANSI C99 MCU Core**](#3-zero-heap-zero-float-ansi-c99-core-csrc) ·
18
+ [**Honest Benchmarks**](#-honest-empirical-benchmarks-5-seeds--50-hz) ·
19
+ [**When to Use (and When Not To)**](#-when-to-use-neckcompass--and-when-not-to)
20
+
21
+ </div>
22
+
23
+ <p align="center">
24
+ <img src="https://raw.githubusercontent.com/indian-aman/neckcompass/main/assets/neckcompass_benchmarks.png" alt="NeckCompass v1.0.0 Ring Attractor & Path Integrator Benchmarks" width="100%">
25
+ </p>
26
+
27
+ ---
28
+
29
+ ## Why NeckCompass?
30
+
31
+ Marine USVs, inspection drones, subsea rovers, and factory AGVs operate around steel ship hulls, sheet-pile docks, high-current motor leads, and buried power cables that inject intermittent **`70°–150°` ferromagnetic heading spikes** lasting `0.5–2.0 s`. Standard linear-Gaussian estimators break down in this regime:
32
+
33
+ - **Gyro Dead-Reckoning** ignores magnetic anomalies completely, but MEMS gyro bias (`~0.8–1.5°/s`) drifts unboundedly (`50°–100°+` within minutes).
34
+ - **2-State Circular Extended Kalman Filters (`[heading, gyro_bias]`)** are minimum-variance optimal under textbook Gaussian noise, **but** a `120°` ferromagnetic anomaly feeds through the Kalman cross-covariance straight into the **gyro bias state**, causing both massive heading yanks (`45°+` P95) and post-spike bias hangovers.
35
+
36
+ **NeckCompass** ports the neural navigation circuit of the fruit-fly (*Drosophila melanogaster*) **Central Complex** into a deterministic software and microcontroller library — **Product #3** in the **[NeckLink](https://github.com/indian-aman/necklink)** fruit-fly brain engineering family:
37
+
38
+ 1. **Ellipsoid Body (`E-PG`) Ring Attractor + `Delta7` Inhibition + `P-EN` Shifters (`RingCompass`)**: Maintains a single topological activity bump across `N=16` (or `N=32`) circular wedges with **zero `-π ↔ +π` wrap-around discontinuity** (`< 10⁻¹⁵ rad` harmonic aliasing on even-`N` rings). Asymmetric Left/Right `P-EN` projections integrate IMU yaw rate (`omega_rad_s * dt`), while localized `R-neuron` sensory cues (`mag_heading_rad`, `sun_heading_rad`) undergo cooperative dendritic coincidence gating against the active `E-PG` bump — smoothly anchoring gyro drift inside the attractor basin while naturally shunting **`70°–150°` ferromagnetic spikes** (`10.2×` lower RMSE than 2-State EKF).
39
+ 2. **Associative `R-Neuron → E-PG` Synaptic Plasticity (`PlasticLandmarkRing`)**: Movement-gated Hebbian / anti-Hebbian ring weight matrix that learns warped local soft-iron and hard-iron magnetic distortion tables online around steel harbors or indoor rooms — with or without an external sun/visual reference.
40
+ 3. **Fan-Shaped Body `CPU4` Path Integrator + `PFL3`/`PFL2` Steering (`FanBodyNavigator`)**: `N=16` push-pull opponent memory columns integrate translational speed along the live `E-PG` compass bump with common-mode cancellation, computing exact `(distance_m, home_bearing_rad)` and `±67.5°` phase-shifted `PFL3` + `180°` `PFL2` motor yaw commands to steer straight home during complete **GPS blackouts** (`0.84 m` miss after a `1,125 m` spiky mission).
41
+ 4. **Zero-Heap, Zero-Float ANSI C99 Core (`csrc/`)**: Pure `Q15` / `int32_t` 16-wedge ring attractor + `CPU4`/`PFL3` navigator using **`316 bytes` of struct RAM** (`576 bytes` total including `260 B` Flash LUTs), **zero `malloc`**, **zero floating-point instructions**, and **`~1.0 us` per update** — ready for `$4` `ESP32`, `STM32`, `RP2040`, and flight controllers.
42
+
43
+ ```mermaid
44
+ flowchart LR
45
+ IMU["IMU Gyro Yaw Rate (omega_rad_s)"] --> PEN["P-EN Left/Right Velocity Shifters"]
46
+ PEN --> EPG["16-Wedge E-PG Ring Attractor + Delta7 Inhibition"]
47
+ MAG["Spiky Magnetometer / Sun Azimuth"] --> RN["R2 / R4d Ring Neurons (Plastic W + Basin Gate)"]
48
+ RN -->|"Coherent cues anchor drift; 70-150 deg spikes shunted"| EPG
49
+ EPG --> CPU4["16 Opponent CPU4 Memory Columns (+ Speed)"]
50
+ CPU4 --> PFL3["PFL3 (+/-67.5 deg) + PFL2 (180 deg) Steering"]
51
+ PFL3 --> HOME["GPS-Denied Homebound Yaw Command"]
52
+ ```
53
+
54
+ ---
55
+
56
+ ## ⚡ 30-Second Quick Start
57
+
58
+ ### 1. Python (`pip install neckcompass`)
59
+
60
+ ```bash
61
+ pip install neckcompass
62
+ # Or directly from source:
63
+ pip install git+https://github.com/indian-aman/neckcompass.git
64
+ ```
65
+
66
+ Fuse a biased IMU gyro with a spiky magnetometer and steer home through a complete GPS blackout in 14 lines:
67
+
68
+ ```python
69
+ import math
70
+ from neckcompass import RingCompass, FanBodyNavigator
71
+
72
+ compass = RingCompass(n_wedges=16, initial_heading_rad=0.0)
73
+ nav = FanBodyNavigator(n_columns=16, compass=compass)
74
+
75
+ # Outbound step @ 50 Hz (dt = 0.02 s): 125-deg hull spike is rejected automatically!
76
+ heading_rad, dist_m, home_bearing_rad = nav.step_with_compass(
77
+ omega_rad_s=0.15,
78
+ speed_m_s=1.5,
79
+ dt=0.02,
80
+ mag_heading_rad=math.radians(125.0), # Transient ferromagnetic spike -> gated out
81
+ )
82
+
83
+ # During GPS blackout, generate bounded [-0.8, +0.8] rad/s yaw command back to (0, 0):
84
+ yaw_rate_cmd = nav.steering_cmd(max_turn_rate=0.8)
85
+ ```
86
+
87
+ Run the built-in CLI demo or head-to-head quick benchmark from your terminal:
88
+
89
+ ```bash
90
+ neckcompass demo
91
+ neckcompass benchmark-quick
92
+ ```
93
+
94
+ ---
95
+
96
+ ### 2. Online Soft-Iron / Harbor Distortion Learning (`PlasticLandmarkRing`)
97
+
98
+ When operating repeatedly inside a steel marina slip or indoor robot bay with static nonlinear magnetic warp (`+20° sin(ψ) + 16° sin(2ψ)`), `PlasticLandmarkRing` learns the `R-neuron → E-PG` unwarping matrix online:
99
+
100
+ ```python
101
+ from neckcompass import PlasticLandmarkRing
102
+
103
+ plastic = PlasticLandmarkRing(n_wedges=16, n_ring_neurons=32)
104
+
105
+ # Self-supervised during normal turns (or pass reference_heading_rad=sun_rad when sky is visible)
106
+ corrected_heading = plastic.update(
107
+ omega_rad_s=imu_gz,
108
+ dt=0.02,
109
+ raw_mag_heading_rad=raw_mag_rad,
110
+ learn=True,
111
+ )
112
+ unwarped_mag_rad, confidence = plastic.map_raw_heading(raw_mag_rad)
113
+ ```
114
+
115
+ ---
116
+
117
+ ### 3. Zero-Heap, Zero-Float ANSI C99 Core (`csrc/`)
118
+
119
+ Drop [`csrc/neckcompass.h`](csrc/neckcompass.h) and [`csrc/neckcompass.c`](csrc/neckcompass.c) into any `ESP32`, `STM32`, `RP2040`, ArduPilot, or bare-metal firmware project (`-std=c99 -Wall -Wextra -pedantic -Werror`, zero FPU, zero `malloc`):
120
+
121
+ ```bash
122
+ make -C csrc clean all test
123
+ ```
124
+
125
+ ```c
126
+ #include "neckcompass.h"
127
+
128
+ nc_compass_t comp;
129
+ nc_navigator_t nav;
130
+
131
+ void autopilot_init(void) {
132
+ nc_compass_init(&comp);
133
+ nc_compass_reset(&comp, nc_cdeg_to_bam(0));
134
+ nc_navigator_init(&nav, 1500); /* 1.5 m smooth arrival taper */
135
+ }
136
+
137
+ /* 50 Hz deterministic fixed-point loop (~1.0 us on desktop, < 8 us on ESP32) */
138
+ int32_t autopilot_step_50hz(int32_t gyro_cdeg_s, int32_t mag_cdeg, int32_t speed_mm_s) {
139
+ nc_bam_t heading = nc_compass_update(
140
+ &comp,
141
+ nc_cdeg_to_bam(gyro_cdeg_s),
142
+ 20u, /* dt = 20 ms */
143
+ nc_cdeg_to_bam(mag_cdeg), 1u, NC_Q15_ONE,
144
+ 0, 0u, 0 /* optional sun/sky sensor */
145
+ );
146
+ nc_navigator_update(&nav, speed_mm_s, 20u, heading);
147
+ return nc_navigator_steering_cmd(&nav, heading, nc_cdeg_to_bam(4500));
148
+ }
149
+ ```
150
+
151
+ ---
152
+
153
+ ## 📊 Honest Empirical Benchmarks (5 Seeds @ `50 Hz`)
154
+
155
+ Reproduce all numbers and regenerate [`assets/neckcompass_benchmarks.png`](assets/neckcompass_benchmarks.png) + [`results/benchmark_summary.json`](results/benchmark_summary.json) locally:
156
+
157
+ ```bash
158
+ python scripts/benchmark_neckcompass.py
159
+ ```
160
+
161
+ All four estimators receive the **exact same** `50 Hz` sensor streams across 5 seeds (`101–105`): realistic MEMS gyro bias (`~0.9°/s` initial offset + `0.08°/s/√s` random walk + `1.5°/s` white noise) and `3.0°` Gaussian magnetometer noise. The **2-State Circular EKF** is given the exact true clean discrete-time process and measurement noise covariances `(Q, R)` with zero handicap.
162
+
163
+ ### 1. Heading Accuracy: Clean Gaussian Noise vs. `70°–150°` Ferromagnetic Spikes
164
+
165
+ Mean ± std across 5 seeds (`180 s` trajectories @ `50 Hz`):
166
+
167
+ | Estimator | **Scenario A: Clean Gaussian RMSE** | **Scenario A: P95** | **Scenario B: Spiky Mag RMSE** | **Scenario B: Spiky Mag P95** | **Scenario B: Max Error** |
168
+ | :--- | :---: | :---: | :---: | :---: | :---: |
169
+ | **`NeckCompass` (`E-PG` 16w)** | `0.43° ± 0.02°` | `0.86° ± 0.03°` | **`1.70° ± 0.18°`** | **`4.27° ± 0.37°`** | **`10.55° ± 1.88°`** |
170
+ | **`2-State Circular EKF`** | **`0.39° ± 0.02°`** *(wins)* | **`0.77° ± 0.03°`** | `17.42° ± 0.93°` | `45.28° ± 2.63°` | `82.53° ± 7.50°` |
171
+ | **`Complementary IMU+Mag`** | `3.64° ± 1.54°` | `5.34° ± 1.80°` | `11.80° ± 0.54°` | `27.77° ± 1.30°` | `47.45° ± 4.87°` |
172
+ | **`Gyro Dead-Reckoning`** | `90.22° ± 32.41°` | `141.70° ± 46.28°` | `84.80° ± 30.74°` | `139.91° ± 53.31°` | `145.98° ± 55.90°` |
173
+
174
+ ### 2. Scenario C: `600 s` Outbound Survey + Blind GPS-Blackout Return (`~1,125 m` Total Path)
175
+
176
+ A USV / rover executes a `10-minute` (`900 m`) multi-leg survey in complete GPS blackout (`2%` wheel/water-speed noise + biased MEMS gyro + intermittent `70°–150°` ferromagnetic spikes), then autonomously steers back to `(0, 0)`:
177
+
178
+ | Navigation Stack | **Return-to-Home Miss (`m`)** | **Miss `%` of `1.12 km` Path** | **RAM / State Footprint** | **Step Latency** |
179
+ | :--- | :---: | :---: | :---: | :---: |
180
+ | **`NeckCompass` (C99 `Q15` Core)** | **`0.32 m`** *(158 m selftest)* | **`0.20%`** | **`316 B` RAM (`576 B` w/ LUT)** | **`~1.04 us / step`** |
181
+ | **`NeckCompass` (Python `E-PG + CPU4`)** | **`0.84 ± 0.23 m`** | **`0.07% ± 0.02%`** | `3,143 B` | `171.26 us / step` |
182
+ | **`Complementary IMU+Mag + DR`** | `13.31 ± 3.08 m` | `1.19% ± 0.28%` | `~256 B` | `~6.5 us / step` |
183
+ | **`2-State Circular EKF + DR`** | `14.29 ± 6.95 m` | `1.28% ± 0.63%` | `5,792 B` *(Py)* | `13.58 us / step` *(Py)* |
184
+ | **`Gyro Dead-Reckoning`** | `256.74 ± 52.38 m` | `24.16% ± 5.69%` | `~128 B` | `~2.0 us / step` |
185
+
186
+ ---
187
+
188
+ ## 🎯 When to Use NeckCompass — and When Not To
189
+
190
+ > [!TIP]
191
+ > **Reach for NeckCompass when:**
192
+ > - **Operating near steel, motors, docks, ship hulls, or power cables**: Intermittent `60°–160°` ferromagnetic spikes yank standard complementary filters and corrupt EKF gyro-bias states; `RingCompass` shunts off-basin anomalies naturally (`1.70°` vs `17.42°` RMSE).
193
+ > - **Surviving GPS blackouts, jamming, or subsea/indoor dropouts**: `FanBodyNavigator` (`CPU4` + `PFL3`/`PFL2`) maintains a bounded, common-mode-cancelled home vector and smooth return controller over `10+ minute` (`> 1 km`) dead-reckoning missions.
194
+ > - **Deploying on tiny FPU-less or ultra-low-power microcontrollers (`$1–$4` MCUs)**: `csrc/neckcompass.c` uses `316 bytes` of RAM, `0` heap allocations, `0` floats, and `~1 us` per step.
195
+ > - **Learning warped harbor or indoor magnetic scenes online**: `PlasticLandmarkRing` unwarps repeatable soft-iron / hard-iron distortions continuously without offline ellipsoid-fitting batch scripts.
196
+
197
+ > [!NOTE]
198
+ > **Stick with a classical EKF / Factor Graph when:**
199
+ > - **Sensors are clean, pre-calibrated, and strictly Gaussian (e.g. open-air UAV far from iron/high-current leads)**: As shown in Scenario A, a properly tuned 2-State Circular EKF achieves slightly lower steady-state RMSE (`0.39°` vs `NeckCompass` `0.43°`) because linear Kalman filtering is minimum-variance optimal under pure Gaussian noise.
200
+ > - **Pure Python ultra-high-rate (`> 10 kHz`) loops without C bindings**: Scalar 2-State Python EKF runs in `~14 us/step` vs `~171 us/step` for 16-wedge NumPy vector ops (`~5.8 kHz`). For `> 5 kHz` embedded or C/C++ loops, compile [`csrc/neckcompass.c`](csrc/neckcompass.c) (`~1.0 us/step`).
201
+ > - **Full 6-DOF visual-inertial SLAM (`SE(3)` pose graphs + loop closure)**: `NeckCompass` is a lightweight 2D/yaw heading & home-vector module (`SO(2)` + `R²`), not a replacement for full 3D LiDAR/camera SLAM when rich 3D visual features and GPU/CPU compute are available.
202
+
203
+ ---
204
+
205
+ ## 🗂️ Repository Layout
206
+
207
+ | Path | Contents |
208
+ | :--- | :--- |
209
+ | [`neckcompass/ring_attractor.py`](neckcompass/ring_attractor.py) | `RingCompass` (`N=16`/`32` `E-PG` ring attractor + `P-EN` shifters + multi-cue basin gating) & `PlasticLandmarkRing` (`R-neuron → E-PG` Hebbian/anti-Hebbian distortion learner) |
210
+ | [`neckcompass/path_integrator.py`](neckcompass/path_integrator.py) | `FanBodyNavigator` (`N=16` opponent `CPU4` vector memory + `PFL3`/`PFL2` GPS-denied homebound steering) |
211
+ | [`neckcompass/cli.py`](neckcompass/cli.py) | `neckcompass` CLI (`neckcompass demo`, `neckcompass benchmark-quick`) |
212
+ | [`csrc/`](csrc/) | Zero-heap, zero-float ANSI C99 fixed-point `Q15` core (`neckcompass.h`, `neckcompass.c`, `Makefile`, and `6/6` `neckcompass_selftest.c`) |
213
+ | [`scripts/benchmark_neckcompass.py`](scripts/benchmark_neckcompass.py) | Reproducible 5-seed empirical benchmark & 3-panel hero chart generator |
214
+ | [`results/benchmark_summary.json`](results/benchmark_summary.json) | Exact 5-seed benchmark metrics and C99/Python latency & memory measurements |
215
+ | [`tests/test_neckcompass.py`](tests/test_neckcompass.py) | `19`-test `pytest` suite covering wrap-around continuity, spike rejection, soft-iron plasticity, GPS-denied return, and CLI |
216
+
217
+ ---
218
+
219
+ ## 📚 Scientific Foundations
220
+
221
+ 1. **Seelig, J. D. & Jayaraman, V.** (2015). Neural dynamics for landmark orientation and angular path integration. *Nature*, 521, 186–191.
222
+ 2. **Stone, T. et al.** (2017). An anatomically constrained model for path integration in the bee brain. *Current Biology*, 27(20), 3069–3085.
223
+ 3. **Turner-Evans, D. et al.** (2017). Angular velocity integration in a fly heading circuit. *eLife*, 6, e23496.
224
+ 4. **Fisher, Y. E., Lu, J., D'Alessandro, I. & Wilson, R. I.** (2019). Sensorimotor experience remaps visual input to a heading network. *Nature*, 576, 121–125.
225
+ 5. **Kim, S. S., Hermundstad, A. M., Romani, S., Abbott, L. F. & Jayaraman, V.** (2019). Generation of stable heading representations in diverse visual scenes. *Nature*, 576, 126–131.
226
+ 6. **Lyu, C., Abbott, L. F. & Maimon, G.** (2022). Building an allocentric travelling direction signal via vector computation. *Nature*, 601, 92–97.
227
+ 7. **Westeinde, E. A. et al.** (2024). Transforming a head direction signal into a goal-oriented steering command. *Nature*, 626, 819–826.
228
+
229
+ ---
230
+
231
+ ## License
232
+
233
+ Released under the [MIT License](LICENSE). Copyright (c) 2026 Amandeep Singh Bains.
@@ -0,0 +1,21 @@
1
+ CC ?= gcc
2
+ CFLAGS ?= -O2 -std=c99 -Wall -Wextra -pedantic -Werror -I.
3
+ BUILD ?= build
4
+
5
+ CORE_SRCS := neckcompass.c
6
+
7
+ .PHONY: all test clean
8
+
9
+ all: $(BUILD)/neckcompass_selftest
10
+
11
+ $(BUILD):
12
+ mkdir -p $(BUILD)
13
+
14
+ $(BUILD)/neckcompass_selftest: tools/neckcompass_selftest.c $(CORE_SRCS) neckcompass.h | $(BUILD)
15
+ $(CC) $(CFLAGS) -o $@ tools/neckcompass_selftest.c $(CORE_SRCS)
16
+
17
+ test: $(BUILD)/neckcompass_selftest
18
+ ./$(BUILD)/neckcompass_selftest
19
+
20
+ clean:
21
+ rm -rf $(BUILD)