necksense 1.1.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.
- necksense-1.1.0/LICENSE +21 -0
- necksense-1.1.0/MANIFEST.in +10 -0
- necksense-1.1.0/PKG-INFO +299 -0
- necksense-1.1.0/README.md +263 -0
- necksense-1.1.0/csrc/Makefile +21 -0
- necksense-1.1.0/csrc/necksense.c +719 -0
- necksense-1.1.0/csrc/necksense.h +214 -0
- necksense-1.1.0/csrc/tools/necksense_selftest.c +460 -0
- necksense-1.1.0/necksense/__init__.py +37 -0
- necksense-1.1.0/necksense/__main__.py +6 -0
- necksense-1.1.0/necksense/antennal.py +336 -0
- necksense-1.1.0/necksense/cli.py +399 -0
- necksense-1.1.0/necksense/kenyon.py +487 -0
- necksense-1.1.0/necksense/mushroom.py +390 -0
- necksense-1.1.0/necksense/py.typed +1 -0
- necksense-1.1.0/necksense.egg-info/PKG-INFO +299 -0
- necksense-1.1.0/necksense.egg-info/SOURCES.txt +21 -0
- necksense-1.1.0/necksense.egg-info/dependency_links.txt +1 -0
- necksense-1.1.0/necksense.egg-info/entry_points.txt +2 -0
- necksense-1.1.0/necksense.egg-info/requires.txt +7 -0
- necksense-1.1.0/necksense.egg-info/top_level.txt +1 -0
- necksense-1.1.0/pyproject.toml +71 -0
- necksense-1.1.0/setup.cfg +4 -0
necksense-1.1.0/LICENSE
ADDED
|
@@ -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.
|
necksense-1.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: necksense
|
|
3
|
+
Version: 1.1.0
|
|
4
|
+
Summary: Drosophila Antennal Lobe + Mushroom Body concentration-invariant sparse hashing, 1-shot novelty detection, and online associative classification in Python and zero-heap ANSI C99
|
|
5
|
+
Author: Amandeep Singh Bains
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/indian-aman/necklink/tree/main/packages/necksense
|
|
8
|
+
Project-URL: Repository, https://github.com/indian-aman/necklink/tree/main/packages/necksense
|
|
9
|
+
Project-URL: Releases, https://github.com/indian-aman/necksense/releases
|
|
10
|
+
Project-URL: Issues, https://github.com/indian-aman/necklink/issues
|
|
11
|
+
Project-URL: Documentation, https://github.com/indian-aman/necklink/tree/main/packages/necksense#readme
|
|
12
|
+
Keywords: necksense,necklink,drosophila,flyhash,mushroom-body,antennal-lobe,kenyon-cells,locality-sensitive-hashing,anomaly-detection,novelty-detection,olfaction,e-nose,embedded-ai,esp32,tinyml
|
|
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
|
|
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
|
+
<a href="https://github.com/indian-aman/necklink"><img src="https://raw.githubusercontent.com/indian-aman/necklink/main/assets/logo/necklink-mark.svg" alt="NeckLink" width="52"></a>
|
|
40
|
+
|
|
41
|
+
# NeckSense (`necksense` v1.1.0)
|
|
42
|
+
|
|
43
|
+
**Concentration-invariant normalisation, sparse binary hashing, one-shot novelty detection and one-shot classification, modelled on the *Drosophila* antennal lobe and mushroom body, in Python (NumPy) and zero-heap, zero-float `Q16.16` ANSI C99 that is bit-identical to Python.**
|
|
44
|
+
|
|
45
|
+
[](https://github.com/indian-aman/necklink/tree/main/packages/necksense)
|
|
46
|
+
[](https://github.com/indian-aman/necklink/tree/main/packages/necksense)
|
|
47
|
+
[](csrc/)
|
|
48
|
+
[](tests/)
|
|
49
|
+
[](LICENSE)
|
|
50
|
+
|
|
51
|
+
[**Install**](#install) ·
|
|
52
|
+
[**Quick start**](#quick-start) ·
|
|
53
|
+
[**C99 core**](#zero-heap-zero-float-ansi-c99-core-csrc) ·
|
|
54
|
+
[**Benchmarks**](#benchmarks-5-seed-mean) ·
|
|
55
|
+
[**When to use it**](#when-to-use-necksense-and-when-not-to) ·
|
|
56
|
+
[**Changelog**](#changelog)
|
|
57
|
+
|
|
58
|
+
</div>
|
|
59
|
+
|
|
60
|
+
<p align="center">
|
|
61
|
+
<img src="https://raw.githubusercontent.com/indian-aman/necklink/main/packages/necksense/assets/necksense_benchmarks.png" alt="NeckSense benchmarks" width="100%">
|
|
62
|
+
</p>
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## What it is
|
|
67
|
+
|
|
68
|
+
Multi-channel chemical, vibration and telemetry sensors face slow baseline drift, large amplitude swings and small microcontrollers. NeckSense turns the fly's four-stage olfactory circuit into a sensor-conditioning and memory library:
|
|
69
|
+
|
|
70
|
+
```
|
|
71
|
+
raw sensor vector x(t)
|
|
72
|
+
│ [1] ORN dual-timescale adaptation: subtract slow (tonic) drift, keep fast transients
|
|
73
|
+
│ [2] Antennal-lobe divisive normalisation: r_i = p_i^n / (sigma^n + (gamma*mean)^n + p_i^n)
|
|
74
|
+
│ [3] Mushroom-body projection: each Kenyon cell sums d_proj=6 inputs (additions only),
|
|
75
|
+
│ then global winner-take-all keeps the top k cells -> packed uint64 bit tag
|
|
76
|
+
├──► KenyonIndex popcount similarity search over binary tags
|
|
77
|
+
├──► MushroomNoveltyDetector one-shot habituation -> novelty score in [0, 1]
|
|
78
|
+
└──► MushroomValenceClassifier one-shot / few-shot associative classifier
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
- **Concentration invariance.** Channel *ratios* survive scaling: the minimum cosine similarity between 1× and 100× inputs is `> 0.99999` over 200 random 8-channel patterns. Near-zero inputs are squelched by `sigma` rather than amplified.
|
|
82
|
+
- **No weight matrix.** Each Kenyon cell's 6 input indices come from a deterministic PRNG, so projection needs **0 bytes of weights and no multiplications**, only additions.
|
|
83
|
+
- **One-shot memory.** Sparse codes let a new class or normal mode be stored from one example with little interference: 2% active cells for novelty, 5% for classification and search.
|
|
84
|
+
- **Bit-identical Python ↔ C.** With `c_parity=True`, Python reproduces the C core's `Q16.16` outputs, tags and scores exactly. The test suite checks this against golden vectors and against the live C binary.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Install
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
pip install "git+https://github.com/indian-aman/necklink.git#subdirectory=packages/necksense"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Quick start
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
import numpy as np
|
|
100
|
+
from necksense import (
|
|
101
|
+
AntennalNormalizer,
|
|
102
|
+
KenyonIndex,
|
|
103
|
+
MushroomNoveltyDetector,
|
|
104
|
+
MushroomValenceClassifier,
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
ethanol_1x = np.array([0.90, 0.35, 0.10, 0.02, 0.50, 0.05, 0.01, 0.20])
|
|
108
|
+
acetone_1x = np.array([0.45, 0.15, 0.08, 0.80, 0.75, 0.10, 0.30, 0.05])
|
|
109
|
+
|
|
110
|
+
# 1. Antennal lobe: divisive normalisation keeps channel ratios across concentration
|
|
111
|
+
ant = AntennalNormalizer(n_channels=8, sigma=0.008, gamma=1.0, hill_n=2.0, tonic_alpha=0.03)
|
|
112
|
+
|
|
113
|
+
# 2. One-shot associative learning: recognise a 20x plume from a single 1x example
|
|
114
|
+
clf = MushroomValenceClassifier(n_inputs=8, antennal=ant, n_kc=1024, d_proj=6, wta_frac=0.05)
|
|
115
|
+
clf.learn(ethanol_1x, "ethanol", reward=1.0)
|
|
116
|
+
clf.learn(acetone_1x, "acetone", reward=1.0)
|
|
117
|
+
assert clf.predict(20.0 * acetone_1x) == "acetone"
|
|
118
|
+
|
|
119
|
+
# 3. Streaming novelty: familiar patterns habituate after a few exposures
|
|
120
|
+
nov = MushroomNoveltyDetector(n_inputs=8, antennal=AntennalNormalizer(8, sigma=0.008), lr=0.55)
|
|
121
|
+
scores = [nov.update_and_score(ethanol_1x) for _ in range(3)] # 1.00 -> 0.45 -> 0.20
|
|
122
|
+
s_anomaly = nov.update_and_score(acetone_1x) # unseen pattern: 0.82
|
|
123
|
+
print([round(s, 2) for s in scores], round(s_anomaly, 2))
|
|
124
|
+
|
|
125
|
+
# 4. Popcount similarity search over 128-byte (1024-bit) Kenyon tags
|
|
126
|
+
idx = KenyonIndex(n_inputs=64, n_kc=1024, d_proj=6, wta_frac=0.05)
|
|
127
|
+
db = np.random.default_rng(0).standard_normal((5000, 64))
|
|
128
|
+
idx.add(db)
|
|
129
|
+
assert idx.query(db[42] + 0.05, top_k=5)[0].id == 42
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Command line
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
necksense demo # four-stage circuit walkthrough
|
|
136
|
+
necksense benchmark-quick # short version of the three benchmarks
|
|
137
|
+
necksense hash-csv sensors.csv --id-col 0 --n-kc 1024 --antennal-normalize -o tags.csv
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Zero-heap, zero-float ANSI C99 core (`csrc/`)
|
|
143
|
+
|
|
144
|
+
[`csrc/necksense.h`](csrc/necksense.h) and [`csrc/necksense.c`](csrc/necksense.c) use only `int32_t`, `uint32_t` and `uint64_t` (`Q16.16`, `1.0 == 65536`), with no `malloc` and no `float`/`double`. They compile cleanly with `-O2 -std=c99 -Wall -Wextra -pedantic -Werror`.
|
|
145
|
+
|
|
146
|
+
```bash
|
|
147
|
+
make -C csrc clean all test
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
```c
|
|
151
|
+
#include "necksense.h"
|
|
152
|
+
|
|
153
|
+
static ns_antennal_t ant;
|
|
154
|
+
static ns_kenyon_t ken;
|
|
155
|
+
static ns_novelty_t nov;
|
|
156
|
+
|
|
157
|
+
void sensor_init(void) {
|
|
158
|
+
ns_antennal_init(&ant, 8u, 512, NS_Q16_ONE, 2u, 655, 1311, 32768, 22938);
|
|
159
|
+
ns_kenyon_init(&ken, 8u, 1024u, 6u, 20u, 42u); /* 20 of 1024 active (2%) for novelty */
|
|
160
|
+
ns_novelty_init(&nov, 1024u, 36045 /* lr = 0.55 */, 0);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
int32_t sensor_tick_q16(const int32_t raw_q16[8]) {
|
|
164
|
+
int32_t norm_q16[8];
|
|
165
|
+
uint64_t kc_tag[16];
|
|
166
|
+
ns_antennal_step(&ant, raw_q16, norm_q16);
|
|
167
|
+
ns_kenyon_hash(&ken, norm_q16, kc_tag);
|
|
168
|
+
return ns_novelty_update_and_score(&nov, kc_tag); /* 0 .. 65536 */
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
Static RAM per struct, as printed by the self-test:
|
|
173
|
+
|
|
174
|
+
| Struct | Default build (`NS_MAX_KC=2048`, `NS_MAX_CLASSES=16`) | `-DNS_MAX_KC=1024 -DNS_MAX_CLASSES=6` |
|
|
175
|
+
| :--- | :---: | :---: |
|
|
176
|
+
| `ns_antennal_t` | `548 B` | `548 B` |
|
|
177
|
+
| `ns_kenyon_t` (no weight table) | `24 B` | `24 B` |
|
|
178
|
+
| `ns_novelty_t` | `8,216 B` | `4,120 B` |
|
|
179
|
+
| `ns_valence_t` (classifier) | `131,172 B` | `24,628 B` |
|
|
180
|
+
| **Total** | **~140 KB** | **~29.3 KB** |
|
|
181
|
+
|
|
182
|
+
Full pipeline (normalise + hash + novelty, `D=16`, `M=1024`): **~25–30 us per sample** on a desktop x86-64 core at `-O2`. It has not been measured on MCU hardware.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Benchmarks (5-seed mean)
|
|
187
|
+
|
|
188
|
+
```bash
|
|
189
|
+
python scripts/benchmark_necksense.py # ~40 s; writes results/benchmark_summary.json and the chart
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### 1. Nearest-neighbour retrieval (`N = 5,000`, `D = 64`, Recall@10)
|
|
193
|
+
|
|
194
|
+
**A. Equal number of active bits `k`.** This is the protocol of Dasgupta, Stevens & Navlakha (*Science* 2017). FlyHash uses `M = 20k` Kenyon cells; SimHash uses `k` Gaussian hyperplanes.
|
|
195
|
+
|
|
196
|
+
| `k` | FlyHash | SimHash | FlyHash float multiplications | SimHash float multiplications |
|
|
197
|
+
| :--- | :---: | :---: | :---: | :---: |
|
|
198
|
+
| 16 | **`17.85% ± 0.58%`** | `9.96% ± 0.21%` | 0 | 1,024 |
|
|
199
|
+
| 32 | **`23.21% ± 0.30%`** | `13.69% ± 0.43%` | 0 | 2,048 |
|
|
200
|
+
| 64 | **`29.93% ± 0.44%`** | `16.68% ± 0.48%` | 0 | 4,096 |
|
|
201
|
+
| 128 | **`36.19% ± 0.35%`** | `22.53% ± 0.18%` | 0 | 8,192 |
|
|
202
|
+
|
|
203
|
+
**B. Equal stored bits per item.** Both are stored as dense bit vectors. **SimHash wins clearly**, because a 50%-dense bit carries more information than a 5%-sparse one:
|
|
204
|
+
|
|
205
|
+
| Tag size | FlyHash | **SimHash** | FlyHash weight RAM | SimHash weight RAM |
|
|
206
|
+
| :--- | :---: | :---: | :---: | :---: |
|
|
207
|
+
| 64 B (512 bits) | `21.67%` | **`40.64%`** | 0 KB | 128 KB |
|
|
208
|
+
| 128 B (1024 bits) | `27.50%` | **`50.51%`** | 0 KB | 256 KB |
|
|
209
|
+
| 256 B (2048 bits) | `34.02%` | **`59.80%`** | 0 KB | 512 KB |
|
|
210
|
+
|
|
211
|
+
FlyHash's advantage is that it needs no weight matrix and no multiplications, which matters on an MCU. If you have the RAM and an FPU, SimHash, PQ or HNSW give better recall per byte.
|
|
212
|
+
|
|
213
|
+
### 2. Gas classification: 8 sensors, 6 gases, 3 calibration shots per gas
|
|
214
|
+
|
|
215
|
+
300 test plumes per seed at `0.1×–25×` concentration, with sinusoidal + linear baseline drift in the drift regimes. **"Baseline-corrected L2 k-NN"** is the standard e-nose pipeline: subtract a moving average of the clean-air readings between plumes (the same information NeckSense's drift tracker gets), L2-normalise, then 3-NN.
|
|
216
|
+
|
|
217
|
+
| Regime | NeckSense (3-shot) | NeckSense (1-shot) | **Baseline-corrected L2 k-NN** | L2 k-NN (no drift correction) | Z-score centroid |
|
|
218
|
+
| :--- | :---: | :---: | :---: | :---: | :---: |
|
|
219
|
+
| Clean 1× | `100%` | `100%` | `100%` | `100%` | `100%` |
|
|
220
|
+
| `0.1×–25×`, no drift | `100%` | `100%` | `100%` | `100%` | `89.9%` |
|
|
221
|
+
| Weak `0.1×–0.5×` + drift | `99.3% ± 0.7%` | `99.5% ± 0.6%` | **`100%`** | `33.4%` | `30.1%` |
|
|
222
|
+
| Full `0.1×–25×` + drift | `99.5% ± 0.5%` | `99.5% ± 0.5%` | **`100%`** | `76.5%` | `72.7%` |
|
|
223
|
+
|
|
224
|
+
What this shows:
|
|
225
|
+
- Drift correction plus ratio normalisation is what solves this task. Any pipeline with those two steps, NeckSense or the classical one, reaches ≥ 99%.
|
|
226
|
+
- NeckSense's version runs in integer C with one-shot learning.
|
|
227
|
+
- Without drift correction, k-NN collapses: dividing `0.2·gas + drift` by its norm points mostly along the drift.
|
|
228
|
+
|
|
229
|
+
### 3. Streaming one-shot anomaly detection (`D = 16`, 20 recurring normal modes at `0.5×–5×`, 5% novel chords)
|
|
230
|
+
|
|
231
|
+
| Detector | AUROC | State | Python time / sample |
|
|
232
|
+
| :--- | :---: | :---: | :---: |
|
|
233
|
+
| **NeckSense novelty** (2% sparse, 1024 cells) | **`0.9942 ± 0.0018`** | 4 KB (`Q16` weights) | ~100–130 us |
|
|
234
|
+
| L2 + nearest-neighbour memory, 512 slots | **`0.9993 ± 0.0002`** | 32 KB (`float32`) | ~11 us |
|
|
235
|
+
| L2 + nearest-neighbour memory, 64 slots | `0.9403 ± 0.0081` | 4 KB (`float32`) | ~10 us |
|
|
236
|
+
| L2 + online Mahalanobis | `0.9581 ± 0.0098` | 1 KB + `O(D²)` math | ~20 us |
|
|
237
|
+
| Online Mahalanobis (raw) | `0.8453 ± 0.0209` | | ~18 us |
|
|
238
|
+
| Rolling z-score (raw) | `0.6349 ± 0.0400` | | ~12 us |
|
|
239
|
+
| Half-space, 32 projections (raw) | `0.5902 ± 0.0346` | | ~36 us |
|
|
240
|
+
|
|
241
|
+
What this shows:
|
|
242
|
+
- **At equal memory** (4 KB), NeckSense beats a nearest-neighbour memory (0.994 vs 0.940).
|
|
243
|
+
- **Given 8× the memory** and floating point, plain nearest-neighbour is better (0.999).
|
|
244
|
+
- Normalisation matters most: every raw-input detector does poorly.
|
|
245
|
+
- The Python implementation is slower than the baselines; use the C core where speed matters.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## When to use NeckSense, and when not to
|
|
250
|
+
|
|
251
|
+
**Good fit:**
|
|
252
|
+
- **One-shot novelty or one-shot classification on a microcontroller.** Integer-only, no weight matrix, a few KB of state, and bit-identical Python for development.
|
|
253
|
+
- **Sensors with amplitude swings and slow drift.** `AntennalNormalizer` gives drift tracking and ratio normalisation in one integer-friendly block.
|
|
254
|
+
- **Sparse binary codes** that can be ANDed, popcounted and updated online.
|
|
255
|
+
|
|
256
|
+
**Use something else when:**
|
|
257
|
+
- **You have a server with RAM and an FPU.** SimHash, PQ or HNSW (`faiss`, `hnswlib`) beat FlyHash on recall per byte, and a plain normalised nearest-neighbour memory beats the novelty detector if you can afford the memory.
|
|
258
|
+
- **You already run baseline correction plus normalisation** feeding k-NN. On our gas benchmark it is as accurate (100% vs 99.5%).
|
|
259
|
+
- **You have thousands of labelled examples.** Gradient-boosted trees or a trained network will outperform one-shot associative rules.
|
|
260
|
+
- **Faults are single-channel amplitude spikes.** A per-channel z-score is simpler and faster.
|
|
261
|
+
|
|
262
|
+
---
|
|
263
|
+
|
|
264
|
+
## Repository layout
|
|
265
|
+
|
|
266
|
+
| Path | Contents |
|
|
267
|
+
| :--- | :--- |
|
|
268
|
+
| [`necksense/antennal.py`](necksense/antennal.py) | `AntennalNormalizer`: drift tracking, divisive normalisation, `Q16.16` parity |
|
|
269
|
+
| [`necksense/kenyon.py`](necksense/kenyon.py) | `KenyonHasher` (FlyHash) and `KenyonIndex` (popcount search, `.npz` save/load) |
|
|
270
|
+
| [`necksense/mushroom.py`](necksense/mushroom.py) | `MushroomNoveltyDetector` and `MushroomValenceClassifier` |
|
|
271
|
+
| [`necksense/cli.py`](necksense/cli.py) | `necksense demo`, `benchmark-quick`, `hash-csv` |
|
|
272
|
+
| [`csrc/`](csrc/) | C99 core, `Makefile`, 5 self-tests and the `--json-parity` emitter |
|
|
273
|
+
| [`scripts/benchmark_necksense.py`](scripts/benchmark_necksense.py) | Reproducible benchmark and chart generator |
|
|
274
|
+
| [`tests/`](tests/) | 17 tests, including every README example, a static zero-heap/zero-float audit of the C sources and Python ↔ C bit parity |
|
|
275
|
+
|
|
276
|
+
---
|
|
277
|
+
|
|
278
|
+
## Changelog
|
|
279
|
+
|
|
280
|
+
**1.1.0**
|
|
281
|
+
- *Improvement:* `MushroomNoveltyDetector` now defaults to 2% sparse codes (`wta_frac=0.02`). Streaming anomaly AUROC rose from `0.9658` to `0.9942` on the benchmark seeds and from `0.961` to `0.997` on held-out seeds. For the C core, pass `k = 20` (of 1024) to `ns_kenyon_init` for novelty.
|
|
282
|
+
- *Benchmark:* added baseline-corrected k-NN, nearest-neighbour memories at equal and 8× memory, and L2 + Mahalanobis. 1.0.0 compared only against baselines without drift correction or normalisation, which overstated the gap: "99.5% where k-NN drops to 33%", and "0.966 vs 0.845 AUROC".
|
|
283
|
+
- *Docs:* the install instructions no longer claim a PyPI package. The C memory table now includes the classifier struct (131 KB by default). Every Python example runs as written.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
287
|
+
## Scientific foundations
|
|
288
|
+
|
|
289
|
+
1. Olsen, S. R. & Wilson, R. I. (2008). Lateral presynaptic inhibition mediates gain control in an olfactory circuit. *Nature*, 452, 956–960.
|
|
290
|
+
2. Nagel, K. I. & Wilson, R. I. (2011). Biophysical mechanisms underlying olfactory receptor neuron dynamics. *Nature Neuroscience*, 14, 208–216.
|
|
291
|
+
3. Dasgupta, S., Stevens, C. F. & Navlakha, S. (2017). A neural algorithm for a fundamental computing problem. *Science*, 358(6364), 793–796.
|
|
292
|
+
4. Hattori, D. et al. (2017). Representations of novelty and familiarity in a mushroom body compartment. *Cell*, 169(5), 956–969.
|
|
293
|
+
5. Aso, Y. et al. (2014). Mushroom body output neurons encode valence and guide memory-based action selection in *Drosophila*. *eLife*, 3, e04580.
|
|
294
|
+
|
|
295
|
+
---
|
|
296
|
+
|
|
297
|
+
## License
|
|
298
|
+
|
|
299
|
+
Released under the [MIT License](LICENSE). Copyright (c) 2026 Amandeep Singh Bains.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
<a href="https://github.com/indian-aman/necklink"><img src="https://raw.githubusercontent.com/indian-aman/necklink/main/assets/logo/necklink-mark.svg" alt="NeckLink" width="52"></a>
|
|
4
|
+
|
|
5
|
+
# NeckSense (`necksense` v1.1.0)
|
|
6
|
+
|
|
7
|
+
**Concentration-invariant normalisation, sparse binary hashing, one-shot novelty detection and one-shot classification, modelled on the *Drosophila* antennal lobe and mushroom body, in Python (NumPy) and zero-heap, zero-float `Q16.16` ANSI C99 that is bit-identical to Python.**
|
|
8
|
+
|
|
9
|
+
[](https://github.com/indian-aman/necklink/tree/main/packages/necksense)
|
|
10
|
+
[](https://github.com/indian-aman/necklink/tree/main/packages/necksense)
|
|
11
|
+
[](csrc/)
|
|
12
|
+
[](tests/)
|
|
13
|
+
[](LICENSE)
|
|
14
|
+
|
|
15
|
+
[**Install**](#install) ·
|
|
16
|
+
[**Quick start**](#quick-start) ·
|
|
17
|
+
[**C99 core**](#zero-heap-zero-float-ansi-c99-core-csrc) ·
|
|
18
|
+
[**Benchmarks**](#benchmarks-5-seed-mean) ·
|
|
19
|
+
[**When to use it**](#when-to-use-necksense-and-when-not-to) ·
|
|
20
|
+
[**Changelog**](#changelog)
|
|
21
|
+
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="https://raw.githubusercontent.com/indian-aman/necklink/main/packages/necksense/assets/necksense_benchmarks.png" alt="NeckSense benchmarks" width="100%">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## What it is
|
|
31
|
+
|
|
32
|
+
Multi-channel chemical, vibration and telemetry sensors face slow baseline drift, large amplitude swings and small microcontrollers. NeckSense turns the fly's four-stage olfactory circuit into a sensor-conditioning and memory library:
|
|
33
|
+
|
|
34
|
+
```
|
|
35
|
+
raw sensor vector x(t)
|
|
36
|
+
│ [1] ORN dual-timescale adaptation: subtract slow (tonic) drift, keep fast transients
|
|
37
|
+
│ [2] Antennal-lobe divisive normalisation: r_i = p_i^n / (sigma^n + (gamma*mean)^n + p_i^n)
|
|
38
|
+
│ [3] Mushroom-body projection: each Kenyon cell sums d_proj=6 inputs (additions only),
|
|
39
|
+
│ then global winner-take-all keeps the top k cells -> packed uint64 bit tag
|
|
40
|
+
├──► KenyonIndex popcount similarity search over binary tags
|
|
41
|
+
├──► MushroomNoveltyDetector one-shot habituation -> novelty score in [0, 1]
|
|
42
|
+
└──► MushroomValenceClassifier one-shot / few-shot associative classifier
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
- **Concentration invariance.** Channel *ratios* survive scaling: the minimum cosine similarity between 1× and 100× inputs is `> 0.99999` over 200 random 8-channel patterns. Near-zero inputs are squelched by `sigma` rather than amplified.
|
|
46
|
+
- **No weight matrix.** Each Kenyon cell's 6 input indices come from a deterministic PRNG, so projection needs **0 bytes of weights and no multiplications**, only additions.
|
|
47
|
+
- **One-shot memory.** Sparse codes let a new class or normal mode be stored from one example with little interference: 2% active cells for novelty, 5% for classification and search.
|
|
48
|
+
- **Bit-identical Python ↔ C.** With `c_parity=True`, Python reproduces the C core's `Q16.16` outputs, tags and scores exactly. The test suite checks this against golden vectors and against the live C binary.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Install
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
pip install "git+https://github.com/indian-aman/necklink.git#subdirectory=packages/necksense"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quick start
|
|
61
|
+
|
|
62
|
+
```python
|
|
63
|
+
import numpy as np
|
|
64
|
+
from necksense import (
|
|
65
|
+
AntennalNormalizer,
|
|
66
|
+
KenyonIndex,
|
|
67
|
+
MushroomNoveltyDetector,
|
|
68
|
+
MushroomValenceClassifier,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
ethanol_1x = np.array([0.90, 0.35, 0.10, 0.02, 0.50, 0.05, 0.01, 0.20])
|
|
72
|
+
acetone_1x = np.array([0.45, 0.15, 0.08, 0.80, 0.75, 0.10, 0.30, 0.05])
|
|
73
|
+
|
|
74
|
+
# 1. Antennal lobe: divisive normalisation keeps channel ratios across concentration
|
|
75
|
+
ant = AntennalNormalizer(n_channels=8, sigma=0.008, gamma=1.0, hill_n=2.0, tonic_alpha=0.03)
|
|
76
|
+
|
|
77
|
+
# 2. One-shot associative learning: recognise a 20x plume from a single 1x example
|
|
78
|
+
clf = MushroomValenceClassifier(n_inputs=8, antennal=ant, n_kc=1024, d_proj=6, wta_frac=0.05)
|
|
79
|
+
clf.learn(ethanol_1x, "ethanol", reward=1.0)
|
|
80
|
+
clf.learn(acetone_1x, "acetone", reward=1.0)
|
|
81
|
+
assert clf.predict(20.0 * acetone_1x) == "acetone"
|
|
82
|
+
|
|
83
|
+
# 3. Streaming novelty: familiar patterns habituate after a few exposures
|
|
84
|
+
nov = MushroomNoveltyDetector(n_inputs=8, antennal=AntennalNormalizer(8, sigma=0.008), lr=0.55)
|
|
85
|
+
scores = [nov.update_and_score(ethanol_1x) for _ in range(3)] # 1.00 -> 0.45 -> 0.20
|
|
86
|
+
s_anomaly = nov.update_and_score(acetone_1x) # unseen pattern: 0.82
|
|
87
|
+
print([round(s, 2) for s in scores], round(s_anomaly, 2))
|
|
88
|
+
|
|
89
|
+
# 4. Popcount similarity search over 128-byte (1024-bit) Kenyon tags
|
|
90
|
+
idx = KenyonIndex(n_inputs=64, n_kc=1024, d_proj=6, wta_frac=0.05)
|
|
91
|
+
db = np.random.default_rng(0).standard_normal((5000, 64))
|
|
92
|
+
idx.add(db)
|
|
93
|
+
assert idx.query(db[42] + 0.05, top_k=5)[0].id == 42
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Command line
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
necksense demo # four-stage circuit walkthrough
|
|
100
|
+
necksense benchmark-quick # short version of the three benchmarks
|
|
101
|
+
necksense hash-csv sensors.csv --id-col 0 --n-kc 1024 --antennal-normalize -o tags.csv
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Zero-heap, zero-float ANSI C99 core (`csrc/`)
|
|
107
|
+
|
|
108
|
+
[`csrc/necksense.h`](csrc/necksense.h) and [`csrc/necksense.c`](csrc/necksense.c) use only `int32_t`, `uint32_t` and `uint64_t` (`Q16.16`, `1.0 == 65536`), with no `malloc` and no `float`/`double`. They compile cleanly with `-O2 -std=c99 -Wall -Wextra -pedantic -Werror`.
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
make -C csrc clean all test
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```c
|
|
115
|
+
#include "necksense.h"
|
|
116
|
+
|
|
117
|
+
static ns_antennal_t ant;
|
|
118
|
+
static ns_kenyon_t ken;
|
|
119
|
+
static ns_novelty_t nov;
|
|
120
|
+
|
|
121
|
+
void sensor_init(void) {
|
|
122
|
+
ns_antennal_init(&ant, 8u, 512, NS_Q16_ONE, 2u, 655, 1311, 32768, 22938);
|
|
123
|
+
ns_kenyon_init(&ken, 8u, 1024u, 6u, 20u, 42u); /* 20 of 1024 active (2%) for novelty */
|
|
124
|
+
ns_novelty_init(&nov, 1024u, 36045 /* lr = 0.55 */, 0);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
int32_t sensor_tick_q16(const int32_t raw_q16[8]) {
|
|
128
|
+
int32_t norm_q16[8];
|
|
129
|
+
uint64_t kc_tag[16];
|
|
130
|
+
ns_antennal_step(&ant, raw_q16, norm_q16);
|
|
131
|
+
ns_kenyon_hash(&ken, norm_q16, kc_tag);
|
|
132
|
+
return ns_novelty_update_and_score(&nov, kc_tag); /* 0 .. 65536 */
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
Static RAM per struct, as printed by the self-test:
|
|
137
|
+
|
|
138
|
+
| Struct | Default build (`NS_MAX_KC=2048`, `NS_MAX_CLASSES=16`) | `-DNS_MAX_KC=1024 -DNS_MAX_CLASSES=6` |
|
|
139
|
+
| :--- | :---: | :---: |
|
|
140
|
+
| `ns_antennal_t` | `548 B` | `548 B` |
|
|
141
|
+
| `ns_kenyon_t` (no weight table) | `24 B` | `24 B` |
|
|
142
|
+
| `ns_novelty_t` | `8,216 B` | `4,120 B` |
|
|
143
|
+
| `ns_valence_t` (classifier) | `131,172 B` | `24,628 B` |
|
|
144
|
+
| **Total** | **~140 KB** | **~29.3 KB** |
|
|
145
|
+
|
|
146
|
+
Full pipeline (normalise + hash + novelty, `D=16`, `M=1024`): **~25–30 us per sample** on a desktop x86-64 core at `-O2`. It has not been measured on MCU hardware.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## Benchmarks (5-seed mean)
|
|
151
|
+
|
|
152
|
+
```bash
|
|
153
|
+
python scripts/benchmark_necksense.py # ~40 s; writes results/benchmark_summary.json and the chart
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
### 1. Nearest-neighbour retrieval (`N = 5,000`, `D = 64`, Recall@10)
|
|
157
|
+
|
|
158
|
+
**A. Equal number of active bits `k`.** This is the protocol of Dasgupta, Stevens & Navlakha (*Science* 2017). FlyHash uses `M = 20k` Kenyon cells; SimHash uses `k` Gaussian hyperplanes.
|
|
159
|
+
|
|
160
|
+
| `k` | FlyHash | SimHash | FlyHash float multiplications | SimHash float multiplications |
|
|
161
|
+
| :--- | :---: | :---: | :---: | :---: |
|
|
162
|
+
| 16 | **`17.85% ± 0.58%`** | `9.96% ± 0.21%` | 0 | 1,024 |
|
|
163
|
+
| 32 | **`23.21% ± 0.30%`** | `13.69% ± 0.43%` | 0 | 2,048 |
|
|
164
|
+
| 64 | **`29.93% ± 0.44%`** | `16.68% ± 0.48%` | 0 | 4,096 |
|
|
165
|
+
| 128 | **`36.19% ± 0.35%`** | `22.53% ± 0.18%` | 0 | 8,192 |
|
|
166
|
+
|
|
167
|
+
**B. Equal stored bits per item.** Both are stored as dense bit vectors. **SimHash wins clearly**, because a 50%-dense bit carries more information than a 5%-sparse one:
|
|
168
|
+
|
|
169
|
+
| Tag size | FlyHash | **SimHash** | FlyHash weight RAM | SimHash weight RAM |
|
|
170
|
+
| :--- | :---: | :---: | :---: | :---: |
|
|
171
|
+
| 64 B (512 bits) | `21.67%` | **`40.64%`** | 0 KB | 128 KB |
|
|
172
|
+
| 128 B (1024 bits) | `27.50%` | **`50.51%`** | 0 KB | 256 KB |
|
|
173
|
+
| 256 B (2048 bits) | `34.02%` | **`59.80%`** | 0 KB | 512 KB |
|
|
174
|
+
|
|
175
|
+
FlyHash's advantage is that it needs no weight matrix and no multiplications, which matters on an MCU. If you have the RAM and an FPU, SimHash, PQ or HNSW give better recall per byte.
|
|
176
|
+
|
|
177
|
+
### 2. Gas classification: 8 sensors, 6 gases, 3 calibration shots per gas
|
|
178
|
+
|
|
179
|
+
300 test plumes per seed at `0.1×–25×` concentration, with sinusoidal + linear baseline drift in the drift regimes. **"Baseline-corrected L2 k-NN"** is the standard e-nose pipeline: subtract a moving average of the clean-air readings between plumes (the same information NeckSense's drift tracker gets), L2-normalise, then 3-NN.
|
|
180
|
+
|
|
181
|
+
| Regime | NeckSense (3-shot) | NeckSense (1-shot) | **Baseline-corrected L2 k-NN** | L2 k-NN (no drift correction) | Z-score centroid |
|
|
182
|
+
| :--- | :---: | :---: | :---: | :---: | :---: |
|
|
183
|
+
| Clean 1× | `100%` | `100%` | `100%` | `100%` | `100%` |
|
|
184
|
+
| `0.1×–25×`, no drift | `100%` | `100%` | `100%` | `100%` | `89.9%` |
|
|
185
|
+
| Weak `0.1×–0.5×` + drift | `99.3% ± 0.7%` | `99.5% ± 0.6%` | **`100%`** | `33.4%` | `30.1%` |
|
|
186
|
+
| Full `0.1×–25×` + drift | `99.5% ± 0.5%` | `99.5% ± 0.5%` | **`100%`** | `76.5%` | `72.7%` |
|
|
187
|
+
|
|
188
|
+
What this shows:
|
|
189
|
+
- Drift correction plus ratio normalisation is what solves this task. Any pipeline with those two steps, NeckSense or the classical one, reaches ≥ 99%.
|
|
190
|
+
- NeckSense's version runs in integer C with one-shot learning.
|
|
191
|
+
- Without drift correction, k-NN collapses: dividing `0.2·gas + drift` by its norm points mostly along the drift.
|
|
192
|
+
|
|
193
|
+
### 3. Streaming one-shot anomaly detection (`D = 16`, 20 recurring normal modes at `0.5×–5×`, 5% novel chords)
|
|
194
|
+
|
|
195
|
+
| Detector | AUROC | State | Python time / sample |
|
|
196
|
+
| :--- | :---: | :---: | :---: |
|
|
197
|
+
| **NeckSense novelty** (2% sparse, 1024 cells) | **`0.9942 ± 0.0018`** | 4 KB (`Q16` weights) | ~100–130 us |
|
|
198
|
+
| L2 + nearest-neighbour memory, 512 slots | **`0.9993 ± 0.0002`** | 32 KB (`float32`) | ~11 us |
|
|
199
|
+
| L2 + nearest-neighbour memory, 64 slots | `0.9403 ± 0.0081` | 4 KB (`float32`) | ~10 us |
|
|
200
|
+
| L2 + online Mahalanobis | `0.9581 ± 0.0098` | 1 KB + `O(D²)` math | ~20 us |
|
|
201
|
+
| Online Mahalanobis (raw) | `0.8453 ± 0.0209` | | ~18 us |
|
|
202
|
+
| Rolling z-score (raw) | `0.6349 ± 0.0400` | | ~12 us |
|
|
203
|
+
| Half-space, 32 projections (raw) | `0.5902 ± 0.0346` | | ~36 us |
|
|
204
|
+
|
|
205
|
+
What this shows:
|
|
206
|
+
- **At equal memory** (4 KB), NeckSense beats a nearest-neighbour memory (0.994 vs 0.940).
|
|
207
|
+
- **Given 8× the memory** and floating point, plain nearest-neighbour is better (0.999).
|
|
208
|
+
- Normalisation matters most: every raw-input detector does poorly.
|
|
209
|
+
- The Python implementation is slower than the baselines; use the C core where speed matters.
|
|
210
|
+
|
|
211
|
+
---
|
|
212
|
+
|
|
213
|
+
## When to use NeckSense, and when not to
|
|
214
|
+
|
|
215
|
+
**Good fit:**
|
|
216
|
+
- **One-shot novelty or one-shot classification on a microcontroller.** Integer-only, no weight matrix, a few KB of state, and bit-identical Python for development.
|
|
217
|
+
- **Sensors with amplitude swings and slow drift.** `AntennalNormalizer` gives drift tracking and ratio normalisation in one integer-friendly block.
|
|
218
|
+
- **Sparse binary codes** that can be ANDed, popcounted and updated online.
|
|
219
|
+
|
|
220
|
+
**Use something else when:**
|
|
221
|
+
- **You have a server with RAM and an FPU.** SimHash, PQ or HNSW (`faiss`, `hnswlib`) beat FlyHash on recall per byte, and a plain normalised nearest-neighbour memory beats the novelty detector if you can afford the memory.
|
|
222
|
+
- **You already run baseline correction plus normalisation** feeding k-NN. On our gas benchmark it is as accurate (100% vs 99.5%).
|
|
223
|
+
- **You have thousands of labelled examples.** Gradient-boosted trees or a trained network will outperform one-shot associative rules.
|
|
224
|
+
- **Faults are single-channel amplitude spikes.** A per-channel z-score is simpler and faster.
|
|
225
|
+
|
|
226
|
+
---
|
|
227
|
+
|
|
228
|
+
## Repository layout
|
|
229
|
+
|
|
230
|
+
| Path | Contents |
|
|
231
|
+
| :--- | :--- |
|
|
232
|
+
| [`necksense/antennal.py`](necksense/antennal.py) | `AntennalNormalizer`: drift tracking, divisive normalisation, `Q16.16` parity |
|
|
233
|
+
| [`necksense/kenyon.py`](necksense/kenyon.py) | `KenyonHasher` (FlyHash) and `KenyonIndex` (popcount search, `.npz` save/load) |
|
|
234
|
+
| [`necksense/mushroom.py`](necksense/mushroom.py) | `MushroomNoveltyDetector` and `MushroomValenceClassifier` |
|
|
235
|
+
| [`necksense/cli.py`](necksense/cli.py) | `necksense demo`, `benchmark-quick`, `hash-csv` |
|
|
236
|
+
| [`csrc/`](csrc/) | C99 core, `Makefile`, 5 self-tests and the `--json-parity` emitter |
|
|
237
|
+
| [`scripts/benchmark_necksense.py`](scripts/benchmark_necksense.py) | Reproducible benchmark and chart generator |
|
|
238
|
+
| [`tests/`](tests/) | 17 tests, including every README example, a static zero-heap/zero-float audit of the C sources and Python ↔ C bit parity |
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## Changelog
|
|
243
|
+
|
|
244
|
+
**1.1.0**
|
|
245
|
+
- *Improvement:* `MushroomNoveltyDetector` now defaults to 2% sparse codes (`wta_frac=0.02`). Streaming anomaly AUROC rose from `0.9658` to `0.9942` on the benchmark seeds and from `0.961` to `0.997` on held-out seeds. For the C core, pass `k = 20` (of 1024) to `ns_kenyon_init` for novelty.
|
|
246
|
+
- *Benchmark:* added baseline-corrected k-NN, nearest-neighbour memories at equal and 8× memory, and L2 + Mahalanobis. 1.0.0 compared only against baselines without drift correction or normalisation, which overstated the gap: "99.5% where k-NN drops to 33%", and "0.966 vs 0.845 AUROC".
|
|
247
|
+
- *Docs:* the install instructions no longer claim a PyPI package. The C memory table now includes the classifier struct (131 KB by default). Every Python example runs as written.
|
|
248
|
+
|
|
249
|
+
---
|
|
250
|
+
|
|
251
|
+
## Scientific foundations
|
|
252
|
+
|
|
253
|
+
1. Olsen, S. R. & Wilson, R. I. (2008). Lateral presynaptic inhibition mediates gain control in an olfactory circuit. *Nature*, 452, 956–960.
|
|
254
|
+
2. Nagel, K. I. & Wilson, R. I. (2011). Biophysical mechanisms underlying olfactory receptor neuron dynamics. *Nature Neuroscience*, 14, 208–216.
|
|
255
|
+
3. Dasgupta, S., Stevens, C. F. & Navlakha, S. (2017). A neural algorithm for a fundamental computing problem. *Science*, 358(6364), 793–796.
|
|
256
|
+
4. Hattori, D. et al. (2017). Representations of novelty and familiarity in a mushroom body compartment. *Cell*, 169(5), 956–969.
|
|
257
|
+
5. Aso, Y. et al. (2014). Mushroom body output neurons encode valence and guide memory-based action selection in *Drosophila*. *eLife*, 3, e04580.
|
|
258
|
+
|
|
259
|
+
---
|
|
260
|
+
|
|
261
|
+
## License
|
|
262
|
+
|
|
263
|
+
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 := necksense.c
|
|
6
|
+
|
|
7
|
+
.PHONY: all test clean
|
|
8
|
+
|
|
9
|
+
all: $(BUILD)/necksense_selftest
|
|
10
|
+
|
|
11
|
+
$(BUILD):
|
|
12
|
+
mkdir -p $(BUILD)
|
|
13
|
+
|
|
14
|
+
$(BUILD)/necksense_selftest: tools/necksense_selftest.c $(CORE_SRCS) necksense.h | $(BUILD)
|
|
15
|
+
$(CC) $(CFLAGS) -o $@ tools/necksense_selftest.c $(CORE_SRCS)
|
|
16
|
+
|
|
17
|
+
test: $(BUILD)/necksense_selftest
|
|
18
|
+
./$(BUILD)/necksense_selftest
|
|
19
|
+
|
|
20
|
+
clean:
|
|
21
|
+
rm -rf $(BUILD)
|