necklink-cns 2.0.0__py3-none-any.whl
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.
- necklink_cns/__init__.py +41 -0
- necklink_cns/banc_v888_priors.json +215 -0
- necklink_cns/cli.py +74 -0
- necklink_cns/cns.py +278 -0
- necklink_cns/connectome.py +163 -0
- necklink_cns/mavlink.py +370 -0
- necklink_cns/ros2_node.py +170 -0
- necklink_cns-2.0.0.dist-info/METADATA +190 -0
- necklink_cns-2.0.0.dist-info/RECORD +13 -0
- necklink_cns-2.0.0.dist-info/WHEEL +5 -0
- necklink_cns-2.0.0.dist-info/entry_points.txt +2 -0
- necklink_cns-2.0.0.dist-info/licenses/LICENSE +21 -0
- necklink_cns-2.0.0.dist-info/top_level.txt +1 -0
necklink_cns/__init__.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
"""
|
|
2
|
+
NeckLink CNS (`necklink_cns` v2.0.0) — Unified Adult Drosophila Whole-CNS Edge Autonomy Stack.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from .cns import CNSOutputs, NeckLinkCNS
|
|
6
|
+
from .connectome import BANC_V888_PRIORS, ConnectomeArbitrator, DescendingState
|
|
7
|
+
from .mavlink import (
|
|
8
|
+
MavFrame,
|
|
9
|
+
MavHeartbeat,
|
|
10
|
+
MavLink2Codec,
|
|
11
|
+
MavLink2Parser,
|
|
12
|
+
MavLinkUdpBridge,
|
|
13
|
+
MavManualControl,
|
|
14
|
+
MavObstacleDistance,
|
|
15
|
+
MavScaledImu,
|
|
16
|
+
MavStatusText,
|
|
17
|
+
)
|
|
18
|
+
from .ros2_node import NeckLinkROS2Adapter, TwistCommand, create_ros2_node
|
|
19
|
+
|
|
20
|
+
__version__ = "2.0.0"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"__version__",
|
|
24
|
+
"BANC_V888_PRIORS",
|
|
25
|
+
"CNSOutputs",
|
|
26
|
+
"ConnectomeArbitrator",
|
|
27
|
+
"DescendingState",
|
|
28
|
+
"MavFrame",
|
|
29
|
+
"MavHeartbeat",
|
|
30
|
+
"MavLink2Codec",
|
|
31
|
+
"MavLink2Parser",
|
|
32
|
+
"MavLinkUdpBridge",
|
|
33
|
+
"MavManualControl",
|
|
34
|
+
"MavObstacleDistance",
|
|
35
|
+
"MavScaledImu",
|
|
36
|
+
"MavStatusText",
|
|
37
|
+
"NeckLinkCNS",
|
|
38
|
+
"NeckLinkROS2Adapter",
|
|
39
|
+
"TwistCommand",
|
|
40
|
+
"create_ros2_node",
|
|
41
|
+
]
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
{
|
|
2
|
+
"dataset": "Female Adult Drosophila Whole-CNS Connectome (BANC v888)",
|
|
3
|
+
"total_neurons": 157789,
|
|
4
|
+
"total_synapses": 23553107,
|
|
5
|
+
"total_directed_edges": 3036600,
|
|
6
|
+
"male_cns_v1_neurons": 167565,
|
|
7
|
+
"cell_counts": {
|
|
8
|
+
"EPG": 50,
|
|
9
|
+
"PEN": 42,
|
|
10
|
+
"Delta7": 40,
|
|
11
|
+
"PFL3": 25,
|
|
12
|
+
"PFL2": 12,
|
|
13
|
+
"T4": 4196,
|
|
14
|
+
"T5": 4682,
|
|
15
|
+
"LPLC2": 181,
|
|
16
|
+
"LC4": 114,
|
|
17
|
+
"LPLC1": 124,
|
|
18
|
+
"DNp01": 2,
|
|
19
|
+
"DNa01": 2,
|
|
20
|
+
"DNa02": 2,
|
|
21
|
+
"MDN": 4,
|
|
22
|
+
"DNp09": 2,
|
|
23
|
+
"APL": 2,
|
|
24
|
+
"ORN": 3006,
|
|
25
|
+
"Chordotonal": 2132,
|
|
26
|
+
"ALPN": 699,
|
|
27
|
+
"ALLN": 429,
|
|
28
|
+
"KC_all": 4553,
|
|
29
|
+
"KC_gamma": 2057,
|
|
30
|
+
"KC_alphabeta": 1943,
|
|
31
|
+
"KC_apbp": 122,
|
|
32
|
+
"MBON_all": 104,
|
|
33
|
+
"DAN_all": 301,
|
|
34
|
+
"DN_all": 1316,
|
|
35
|
+
"AN_all": 1849,
|
|
36
|
+
"MN_all": 805,
|
|
37
|
+
"MN_leg": 393,
|
|
38
|
+
"MN_wing": 62,
|
|
39
|
+
"MN_haltere": 25
|
|
40
|
+
},
|
|
41
|
+
"pathways": {
|
|
42
|
+
"T4->LPLC2": {
|
|
43
|
+
"edges": 897,
|
|
44
|
+
"synapses": 3330
|
|
45
|
+
},
|
|
46
|
+
"T5->LPLC2": {
|
|
47
|
+
"edges": 1748,
|
|
48
|
+
"synapses": 6586
|
|
49
|
+
},
|
|
50
|
+
"T4->LC4": {
|
|
51
|
+
"edges": 31,
|
|
52
|
+
"synapses": 173
|
|
53
|
+
},
|
|
54
|
+
"T5->LC4": {
|
|
55
|
+
"edges": 91,
|
|
56
|
+
"synapses": 305
|
|
57
|
+
},
|
|
58
|
+
"T4->LPLC1": {
|
|
59
|
+
"edges": 815,
|
|
60
|
+
"synapses": 3205
|
|
61
|
+
},
|
|
62
|
+
"T5->LPLC1": {
|
|
63
|
+
"edges": 1134,
|
|
64
|
+
"synapses": 4535
|
|
65
|
+
},
|
|
66
|
+
"LPLC2->DNp01": {
|
|
67
|
+
"edges": 84,
|
|
68
|
+
"synapses": 403
|
|
69
|
+
},
|
|
70
|
+
"LC4->DNp01": {
|
|
71
|
+
"edges": 21,
|
|
72
|
+
"synapses": 71
|
|
73
|
+
},
|
|
74
|
+
"LPLC2->DN_all": {
|
|
75
|
+
"edges": 529,
|
|
76
|
+
"synapses": 2896
|
|
77
|
+
},
|
|
78
|
+
"LC4->DN_all": {
|
|
79
|
+
"edges": 226,
|
|
80
|
+
"synapses": 1108
|
|
81
|
+
},
|
|
82
|
+
"LPLC1->DN_all": {
|
|
83
|
+
"edges": 182,
|
|
84
|
+
"synapses": 945
|
|
85
|
+
},
|
|
86
|
+
"EPG->Delta7": {
|
|
87
|
+
"edges": 901,
|
|
88
|
+
"synapses": 6998
|
|
89
|
+
},
|
|
90
|
+
"Delta7->EPG": {
|
|
91
|
+
"edges": 336,
|
|
92
|
+
"synapses": 4646
|
|
93
|
+
},
|
|
94
|
+
"EPG->PEN": {
|
|
95
|
+
"edges": 491,
|
|
96
|
+
"synapses": 6151
|
|
97
|
+
},
|
|
98
|
+
"PEN->EPG": {
|
|
99
|
+
"edges": 521,
|
|
100
|
+
"synapses": 7998
|
|
101
|
+
},
|
|
102
|
+
"EPG->PFL3": {
|
|
103
|
+
"edges": 91,
|
|
104
|
+
"synapses": 1442
|
|
105
|
+
},
|
|
106
|
+
"Delta7->PFL3": {
|
|
107
|
+
"edges": 249,
|
|
108
|
+
"synapses": 6066
|
|
109
|
+
},
|
|
110
|
+
"EPG->PFL2": {
|
|
111
|
+
"edges": 57,
|
|
112
|
+
"synapses": 1071
|
|
113
|
+
},
|
|
114
|
+
"Delta7->PFL2": {
|
|
115
|
+
"edges": 132,
|
|
116
|
+
"synapses": 3712
|
|
117
|
+
},
|
|
118
|
+
"PFL3->DNa02": {
|
|
119
|
+
"edges": 23,
|
|
120
|
+
"synapses": 259
|
|
121
|
+
},
|
|
122
|
+
"PFL3->DN_all": {
|
|
123
|
+
"edges": 169,
|
|
124
|
+
"synapses": 1772
|
|
125
|
+
},
|
|
126
|
+
"PFL2->DN_all": {
|
|
127
|
+
"edges": 44,
|
|
128
|
+
"synapses": 1047
|
|
129
|
+
},
|
|
130
|
+
"ORN->ALPN": {
|
|
131
|
+
"edges": 13678,
|
|
132
|
+
"synapses": 190238
|
|
133
|
+
},
|
|
134
|
+
"ORN->ALLN": {
|
|
135
|
+
"edges": 19599,
|
|
136
|
+
"synapses": 107521
|
|
137
|
+
},
|
|
138
|
+
"ALLN->ALPN": {
|
|
139
|
+
"edges": 17450,
|
|
140
|
+
"synapses": 170732
|
|
141
|
+
},
|
|
142
|
+
"ALPN->KC_all": {
|
|
143
|
+
"edges": 15844,
|
|
144
|
+
"synapses": 159648
|
|
145
|
+
},
|
|
146
|
+
"KC_all->APL": {
|
|
147
|
+
"edges": 3290,
|
|
148
|
+
"synapses": 18468
|
|
149
|
+
},
|
|
150
|
+
"APL->KC_all": {
|
|
151
|
+
"edges": 3311,
|
|
152
|
+
"synapses": 21486
|
|
153
|
+
},
|
|
154
|
+
"KC_all->MBON_all": {
|
|
155
|
+
"edges": 3121,
|
|
156
|
+
"synapses": 12544
|
|
157
|
+
},
|
|
158
|
+
"KC_gamma->MBON_all": {
|
|
159
|
+
"edges": 1393,
|
|
160
|
+
"synapses": 5645
|
|
161
|
+
},
|
|
162
|
+
"KC_alphabeta->MBON_all": {
|
|
163
|
+
"edges": 1533,
|
|
164
|
+
"synapses": 6110
|
|
165
|
+
},
|
|
166
|
+
"DAN_all->KC_all": {
|
|
167
|
+
"edges": 89,
|
|
168
|
+
"synapses": 304
|
|
169
|
+
},
|
|
170
|
+
"DAN_all->MBON_all": {
|
|
171
|
+
"edges": 115,
|
|
172
|
+
"synapses": 817
|
|
173
|
+
},
|
|
174
|
+
"MBON_all->DN_all": {
|
|
175
|
+
"edges": 149,
|
|
176
|
+
"synapses": 2008
|
|
177
|
+
},
|
|
178
|
+
"DNp01->MN_all": {
|
|
179
|
+
"edges": 6,
|
|
180
|
+
"synapses": 28
|
|
181
|
+
},
|
|
182
|
+
"DNa02->MN_all": {
|
|
183
|
+
"edges": 56,
|
|
184
|
+
"synapses": 775
|
|
185
|
+
},
|
|
186
|
+
"DNa01->MN_all": {
|
|
187
|
+
"edges": 38,
|
|
188
|
+
"synapses": 354
|
|
189
|
+
},
|
|
190
|
+
"MDN->MN_all": {
|
|
191
|
+
"edges": 0,
|
|
192
|
+
"synapses": 0
|
|
193
|
+
},
|
|
194
|
+
"DN_all->MN_all": {
|
|
195
|
+
"edges": 9068,
|
|
196
|
+
"synapses": 143565
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
"derived_biophysical_ratios": {
|
|
200
|
+
"optic_t5_to_t4_lplc2_off_dominance": 1.9778,
|
|
201
|
+
"optic_lplc2_dn_share": 0.5852,
|
|
202
|
+
"optic_lc4_dn_share": 0.2239,
|
|
203
|
+
"optic_lplc1_dn_share": 0.1909,
|
|
204
|
+
"cx_pfl2_to_pfl3_antigoal_ratio": 0.48,
|
|
205
|
+
"cx_delta7_to_epg_inhibition_ratio": 0.6639,
|
|
206
|
+
"mb_alpn_to_kc_claws_mean": 3.92,
|
|
207
|
+
"mb_alpn_to_kc_claws_median": 4.0,
|
|
208
|
+
"mb_kc_gamma_fast_fraction": 0.5142,
|
|
209
|
+
"mb_kc_alphabeta_ltm_fraction": 0.4858,
|
|
210
|
+
"neck_an_to_dn_ratio": 1.405,
|
|
211
|
+
"neck_bottleneck_fraction": 0.02006,
|
|
212
|
+
"cns_excitatory_fraction": 0.669,
|
|
213
|
+
"cns_inhibitory_fraction": 0.331
|
|
214
|
+
}
|
|
215
|
+
}
|
necklink_cns/cli.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""CLI entrypoint for `necklink-cns` (`v2.0.0`)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
import numpy as np
|
|
9
|
+
|
|
10
|
+
from . import __version__
|
|
11
|
+
from .cns import NeckLinkCNS
|
|
12
|
+
from .connectome import BANC_V888_PRIORS
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def run_demo() -> dict:
|
|
16
|
+
cns = NeckLinkCNS(vehicle="usv_t200", grid_size=32)
|
|
17
|
+
yy, xx = np.mgrid[0:32, 0:32]
|
|
18
|
+
bg = 150.0 + 15.0 * np.sin(xx * 0.4) * np.cos(yy * 0.4)
|
|
19
|
+
|
|
20
|
+
out = None
|
|
21
|
+
dodged = False
|
|
22
|
+
for step in range(24):
|
|
23
|
+
frame = bg.copy()
|
|
24
|
+
if step >= 6:
|
|
25
|
+
hw = 1 + (step - 6) // 3
|
|
26
|
+
frame[:, max(0, 9 - hw) : min(32, 9 + hw + 1)] = 10.0 # Left pole looming
|
|
27
|
+
fb = cns._last_cmds.copy()
|
|
28
|
+
if step >= 8:
|
|
29
|
+
fb[1] *= 0.10 # 90% weed-fouled thruster #1
|
|
30
|
+
out = cns.step(
|
|
31
|
+
dt=0.02,
|
|
32
|
+
speed_m_s=1.5,
|
|
33
|
+
gyro_xyz=np.array([0.05, -0.03, 0.0]),
|
|
34
|
+
accel_xyz=np.array([0.8, -0.6, 9.75]),
|
|
35
|
+
mag_xyz=np.array([0.42, 0.0, 0.90]),
|
|
36
|
+
camera_frame=frame,
|
|
37
|
+
feedback_thrust=fb,
|
|
38
|
+
)
|
|
39
|
+
if out.descending.behavior_mode in ("DODGE_LOOM", "MOONWALK_REVERSE"):
|
|
40
|
+
dodged = True
|
|
41
|
+
|
|
42
|
+
assert out is not None
|
|
43
|
+
return {
|
|
44
|
+
"version": __version__,
|
|
45
|
+
"banc_v888_neurons": BANC_V888_PRIORS["total_neurons"],
|
|
46
|
+
"thin_pole_dodged": dodged,
|
|
47
|
+
"final_mode": out.descending.behavior_mode,
|
|
48
|
+
"fouled_thruster1_health": round(float(out.actuator_health[1]), 3),
|
|
49
|
+
"cervical_4b_hex": "0x" + out.cervical_wire_4b.hex().upper(),
|
|
50
|
+
"mavlink_v2_bytes": len(out.mavlink_manual_pkt),
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def main(argv: list[str] | None = None) -> int:
|
|
55
|
+
parser = argparse.ArgumentParser(prog="necklink-cns", description="NeckLink Unified Whole-CNS Autonomy CLI")
|
|
56
|
+
parser.add_argument("--version", action="store_true", help="Print package version")
|
|
57
|
+
sub = parser.add_subparsers(dest="cmd")
|
|
58
|
+
sub.add_parser("demo", help="Run closed-loop 4-organ + MAVLink v2 survival demo")
|
|
59
|
+
sub.add_parser("priors", help="Print empirical BANC v888 connectome priors")
|
|
60
|
+
|
|
61
|
+
args = parser.parse_args(argv)
|
|
62
|
+
if args.version:
|
|
63
|
+
print(f"necklink-cns {__version__}")
|
|
64
|
+
return 0
|
|
65
|
+
if args.cmd == "priors":
|
|
66
|
+
print(json.dumps(BANC_V888_PRIORS["derived_biophysical_ratios"], indent=2))
|
|
67
|
+
return 0
|
|
68
|
+
res = run_demo()
|
|
69
|
+
print(json.dumps(res, indent=2))
|
|
70
|
+
return 0
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
if __name__ == "__main__":
|
|
74
|
+
sys.exit(main())
|
necklink_cns/cns.py
ADDED
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unified Adult Drosophila Whole-CNS Python Controller (`necklink_cns.cns`).
|
|
3
|
+
|
|
4
|
+
Integrates all four `v2.0.0` organs (`NeckSight`, `NeckCompass`, `NeckSense`, `NeckCord`)
|
|
5
|
+
with empirical `BANC v888` descending action selection, 4-byte CRC-4 cervical framing,
|
|
6
|
+
and wire-valid `MAVLink v2` packet generation.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
import neckcompass
|
|
15
|
+
import neckcord
|
|
16
|
+
import necksense
|
|
17
|
+
import necksight
|
|
18
|
+
import numpy as np
|
|
19
|
+
|
|
20
|
+
from .connectome import BANC_V888_PRIORS, ConnectomeArbitrator, DescendingState
|
|
21
|
+
from .mavlink import MavLink2Codec, MavManualControl
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class CNSOutputs:
|
|
26
|
+
"""Complete per-tick readout across all four CNS organs + cervical bottleneck."""
|
|
27
|
+
|
|
28
|
+
heading_rad: float
|
|
29
|
+
home_distance_m: float
|
|
30
|
+
home_bearing_rad: float
|
|
31
|
+
loom_score: float
|
|
32
|
+
thin_loom_score: float
|
|
33
|
+
loom_triggered: bool
|
|
34
|
+
avoid_x: float
|
|
35
|
+
avoid_y: float
|
|
36
|
+
novelty_score: float
|
|
37
|
+
whiff_rate: float
|
|
38
|
+
vib_novelty_score: float
|
|
39
|
+
predicted_class: Optional[int]
|
|
40
|
+
descending: DescendingState
|
|
41
|
+
cervical_wire_4b: bytes
|
|
42
|
+
actuator_commands: np.ndarray
|
|
43
|
+
actuator_health: np.ndarray
|
|
44
|
+
cord_min_health: float
|
|
45
|
+
mavlink_manual_pkt: bytes
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class NeckLinkCNS:
|
|
49
|
+
"""
|
|
50
|
+
Closed-loop 4-organ *Drosophila* Whole-CNS controller supporting:
|
|
51
|
+
- `vehicle="usv_t200"` (4-thruster asymmetric BlueRobotics T200 USV)
|
|
52
|
+
- `vehicle="hexacopter"` (6-rotor unidirectional `[0, u_max]` hexacopter)
|
|
53
|
+
- `vehicle="octocopter"` (8-rotor unidirectional `[0, u_max]` octocopter)
|
|
54
|
+
- `vehicle="hexapod"` (6-limb, 18-servo legged hexapod with 3-DOF IK)
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
def __init__(
|
|
58
|
+
self,
|
|
59
|
+
vehicle: str = "usv_t200",
|
|
60
|
+
grid_size: int = 32,
|
|
61
|
+
n_chem_channels: int = 8,
|
|
62
|
+
enable_plastic_mag: bool = False,
|
|
63
|
+
seed: int = 42,
|
|
64
|
+
) -> None:
|
|
65
|
+
self.vehicle = vehicle
|
|
66
|
+
self.grid_size = int(grid_size)
|
|
67
|
+
self.enable_plastic_mag = bool(enable_plastic_mag)
|
|
68
|
+
|
|
69
|
+
# 1. Optic Lobe (NeckSight v2.0.0)
|
|
70
|
+
self.sight = necksight.NeckSightPipeline(
|
|
71
|
+
grid_size=self.grid_size,
|
|
72
|
+
threshold=0.045,
|
|
73
|
+
enable_lc4_bar=True,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
# 2. Central Complex (NeckCompass v2.0.0)
|
|
77
|
+
self.compass = neckcompass.RingCompass(n_wedges=16, initial_heading_rad=0.0)
|
|
78
|
+
self.plastic_compass = neckcompass.PlasticLandmarkRing(n_wedges=16) if enable_plastic_mag else None
|
|
79
|
+
self.navigator = neckcompass.FanBodyNavigator(n_columns=16, compass=self.compass)
|
|
80
|
+
|
|
81
|
+
# 3. Antennal Lobe, Chordotonal Organ & Mushroom Body (NeckSense v2.0.0)
|
|
82
|
+
self.vib_fe = necksense.VibrationFrontEnd()
|
|
83
|
+
self.chem_nov = necksense.MushroomNoveltyDetector(
|
|
84
|
+
n_inputs=n_chem_channels,
|
|
85
|
+
antennal=True,
|
|
86
|
+
stateful_antennal=True,
|
|
87
|
+
n_kc=512,
|
|
88
|
+
seed=seed,
|
|
89
|
+
)
|
|
90
|
+
self.vib_nov = necksense.MushroomNoveltyDetector(
|
|
91
|
+
n_inputs=necksense.NS_VIB_CHANNELS,
|
|
92
|
+
antennal=True,
|
|
93
|
+
stateful_antennal=False,
|
|
94
|
+
n_kc=512,
|
|
95
|
+
seed=seed + 1,
|
|
96
|
+
)
|
|
97
|
+
self.chem_clf = necksense.MushroomValenceClassifier(
|
|
98
|
+
n_inputs=n_chem_channels,
|
|
99
|
+
antennal=True,
|
|
100
|
+
stateful_antennal=False,
|
|
101
|
+
n_kc=512,
|
|
102
|
+
ltm_alpha_beta=True,
|
|
103
|
+
seed=seed + 2,
|
|
104
|
+
)
|
|
105
|
+
self._has_learned_classes = False
|
|
106
|
+
|
|
107
|
+
# 4. Descending Arbitrator & Ventral Nerve Cord (NeckCord v2.0.0)
|
|
108
|
+
self.arbitrator = ConnectomeArbitrator()
|
|
109
|
+
if vehicle == "hexacopter":
|
|
110
|
+
self.cord = neckcord.ShuntingThrusterCord.multicopter_hex_6()
|
|
111
|
+
elif vehicle == "octocopter":
|
|
112
|
+
self.cord = neckcord.ShuntingThrusterCord.multicopter_octo_8()
|
|
113
|
+
else:
|
|
114
|
+
self.cord = neckcord.ShuntingThrusterCord.usv_t200_vectored_4()
|
|
115
|
+
|
|
116
|
+
self.esc_obs = neckcord.ESCObserver(
|
|
117
|
+
n_actuators=self.cord.n_actuators,
|
|
118
|
+
u_nom=self.cord.u_max,
|
|
119
|
+
rpm_max=9000.0,
|
|
120
|
+
)
|
|
121
|
+
self.cpg = neckcord.SegmentalCPGCord(adapt_to_damage=True)
|
|
122
|
+
self.mav = MavLink2Codec(sysid=42, compid=191)
|
|
123
|
+
self._seq = 0
|
|
124
|
+
self._last_cmds = np.zeros(self.cord.n_actuators, dtype=np.float64)
|
|
125
|
+
|
|
126
|
+
def learn_chemical(self, vec: np.ndarray, class_id: int, consolidate: bool = True) -> None:
|
|
127
|
+
self.chem_clf.learn(np.asarray(vec, dtype=np.float64), int(class_id))
|
|
128
|
+
if consolidate:
|
|
129
|
+
self.chem_clf.consolidate()
|
|
130
|
+
self._has_learned_classes = True
|
|
131
|
+
|
|
132
|
+
def step(
|
|
133
|
+
self,
|
|
134
|
+
*,
|
|
135
|
+
dt: float = 0.02,
|
|
136
|
+
speed_m_s: float = 1.0,
|
|
137
|
+
cruise_speed: float = 0.65,
|
|
138
|
+
return_home: bool = True,
|
|
139
|
+
gyro_xyz: Optional[np.ndarray] = None,
|
|
140
|
+
accel_xyz: Optional[np.ndarray] = None,
|
|
141
|
+
mag_xyz: Optional[np.ndarray] = None,
|
|
142
|
+
camera_frame: Optional[np.ndarray] = None,
|
|
143
|
+
chemical_vec: Optional[np.ndarray] = None,
|
|
144
|
+
vibration_window: Optional[np.ndarray] = None,
|
|
145
|
+
feedback_thrust: Optional[np.ndarray] = None,
|
|
146
|
+
esc_rpm: Optional[np.ndarray] = None,
|
|
147
|
+
esc_current_a: Optional[np.ndarray] = None,
|
|
148
|
+
) -> CNSOutputs:
|
|
149
|
+
g = np.zeros(3, dtype=np.float64) if gyro_xyz is None else np.asarray(gyro_xyz, dtype=np.float64).reshape(3)
|
|
150
|
+
a = np.array([0.0, 0.0, 9.81], dtype=np.float64) if accel_xyz is None else np.asarray(accel_xyz, dtype=np.float64).reshape(3)
|
|
151
|
+
|
|
152
|
+
# 1. Central Complex 9-DOF Tilt-Compensated Heading + CPU4 Homing
|
|
153
|
+
if mag_xyz is not None:
|
|
154
|
+
m = np.asarray(mag_xyz, dtype=np.float64).reshape(3)
|
|
155
|
+
hdg, dist_m, home_brg = self.navigator.step_with_compass_9dof(
|
|
156
|
+
float(g[0]), float(g[1]), float(g[2]),
|
|
157
|
+
float(a[0]), float(a[1]), float(a[2]),
|
|
158
|
+
float(m[0]), float(m[1]), float(m[2]),
|
|
159
|
+
speed_m_s=float(speed_m_s),
|
|
160
|
+
dt=float(dt),
|
|
161
|
+
)
|
|
162
|
+
else:
|
|
163
|
+
hdg, dist_m, home_brg = self.navigator.step_with_compass(
|
|
164
|
+
omega_rad_s=float(g[2]),
|
|
165
|
+
speed_m_s=float(speed_m_s),
|
|
166
|
+
dt=float(dt),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
cx_turn = float(self.navigator.steering_cmd(max_turn_rate=1.0)) if return_home else 0.0
|
|
170
|
+
hdg_err = float(neckcompass.wrap_angle(home_brg - hdg)) if return_home else 0.0
|
|
171
|
+
|
|
172
|
+
# 2. Optic Lobe 3-DOF Derotation + LPLC2 / LC4 Looming
|
|
173
|
+
loom_score = 0.0
|
|
174
|
+
bar_score = 0.0
|
|
175
|
+
loom_trig = False
|
|
176
|
+
avoid_x = 0.0
|
|
177
|
+
avoid_y = 0.0
|
|
178
|
+
focus_x_norm = 0.0
|
|
179
|
+
if camera_frame is not None:
|
|
180
|
+
ego = necksight.imu_to_ego_motion(
|
|
181
|
+
yaw_rate_rad_s=float(g[2]),
|
|
182
|
+
pitch_rate_rad_s=float(g[1]),
|
|
183
|
+
roll_rate_rad_s=float(g[0]),
|
|
184
|
+
dt=float(dt),
|
|
185
|
+
grid_width=self.grid_size,
|
|
186
|
+
)
|
|
187
|
+
trig = self.sight.step(np.asarray(camera_frame), dt=float(dt), ego_shift_px=ego)
|
|
188
|
+
loom_score = float(trig.looming_score)
|
|
189
|
+
bar_score = float(trig.bar_score)
|
|
190
|
+
loom_trig = bool(trig.triggered)
|
|
191
|
+
avoid_x = float(trig.avoid_x)
|
|
192
|
+
avoid_y = float(trig.avoid_y)
|
|
193
|
+
# Note: in ConnectomeArbitrator, focus_x < 0 => obstacle on left => evade right (avoid_x > 0)
|
|
194
|
+
focus_x_norm = -avoid_x if abs(avoid_x) > 1e-3 else (float(trig.focus_xy[0]) / max(1.0, self.grid_size - 1.0)) * 2.0 - 1.0
|
|
195
|
+
|
|
196
|
+
# 3. Antennal Lobe, Chordotonal Vibration & Mushroom Body
|
|
197
|
+
nov_score = 0.0
|
|
198
|
+
whiff_rate = 0.0
|
|
199
|
+
pred_cls: Optional[int] = None
|
|
200
|
+
if chemical_vec is not None:
|
|
201
|
+
cvec = np.asarray(chemical_vec, dtype=np.float64).ravel()
|
|
202
|
+
nov_score = float(self.chem_nov.update_and_score(cvec))
|
|
203
|
+
if self.chem_nov.antennal is not None:
|
|
204
|
+
whiff_rate = float(getattr(self.chem_nov.antennal, "whiff_rate", 0.0))
|
|
205
|
+
if self._has_learned_classes:
|
|
206
|
+
pred_cls = int(self.chem_clf.predict(cvec))
|
|
207
|
+
|
|
208
|
+
vib_score = 0.0
|
|
209
|
+
if vibration_window is not None:
|
|
210
|
+
v_feat = self.vib_fe.extract(np.asarray(vibration_window, dtype=np.float64))
|
|
211
|
+
vib_score = float(self.vib_nov.update_and_score(v_feat))
|
|
212
|
+
|
|
213
|
+
# 4. BANC v888 Descending Neck Bottleneck Arbitration
|
|
214
|
+
min_h = float(np.min(self.cord.health))
|
|
215
|
+
desc = self.arbitrator.arbitrate(
|
|
216
|
+
cruise_speed=cruise_speed,
|
|
217
|
+
cx_turn_cmd=cx_turn,
|
|
218
|
+
heading_err_rad=hdg_err,
|
|
219
|
+
loom_score=loom_score,
|
|
220
|
+
thin_loom_score=bar_score,
|
|
221
|
+
loom_focus_x=focus_x_norm,
|
|
222
|
+
mb_novelty=nov_score,
|
|
223
|
+
vib_fault_score=vib_score,
|
|
224
|
+
cord_min_health=min_h,
|
|
225
|
+
dt=dt,
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
dframe = neckcord.DescendingFrame(
|
|
229
|
+
drive_fwd=desc.drive_fwd,
|
|
230
|
+
turn_yaw=desc.turn_yaw,
|
|
231
|
+
strafe_lat=desc.strafe_lat,
|
|
232
|
+
gait_mode=int(neckcord.GaitMode.AUTO),
|
|
233
|
+
freeze_reflex=desc.freeze_reflex,
|
|
234
|
+
seq=self._seq & 0x03,
|
|
235
|
+
)
|
|
236
|
+
self._seq += 1
|
|
237
|
+
wire_4b = neckcord.encode_descending_4b(dframe)
|
|
238
|
+
|
|
239
|
+
# 5. Ventral Nerve Cord Allocation / ESC Proprioception
|
|
240
|
+
fb = feedback_thrust
|
|
241
|
+
if fb is None and esc_rpm is not None and esc_current_a is not None:
|
|
242
|
+
esc_res = self.esc_obs.update(self._last_cmds, esc_rpm, esc_current_a, dt=dt)
|
|
243
|
+
fb = esc_res.estimated_thrust
|
|
244
|
+
|
|
245
|
+
alloc = self.cord.step(wire_4b, feedback_thrust=fb, dt=dt)
|
|
246
|
+
self._last_cmds = alloc.commands.copy()
|
|
247
|
+
|
|
248
|
+
mav_pkt = self.mav.encode_manual_control(
|
|
249
|
+
MavManualControl(
|
|
250
|
+
x=int(round(desc.drive_fwd * 1000.0)),
|
|
251
|
+
y=int(round(desc.strafe_lat * 1000.0)),
|
|
252
|
+
z=500,
|
|
253
|
+
r=int(round(desc.turn_yaw * 1000.0)),
|
|
254
|
+
buttons=(1 if loom_trig else 0),
|
|
255
|
+
target=1,
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
return CNSOutputs(
|
|
260
|
+
heading_rad=float(hdg),
|
|
261
|
+
home_distance_m=float(dist_m),
|
|
262
|
+
home_bearing_rad=float(home_brg),
|
|
263
|
+
loom_score=loom_score,
|
|
264
|
+
thin_loom_score=bar_score,
|
|
265
|
+
loom_triggered=loom_trig,
|
|
266
|
+
avoid_x=avoid_x,
|
|
267
|
+
avoid_y=avoid_y,
|
|
268
|
+
novelty_score=nov_score,
|
|
269
|
+
whiff_rate=whiff_rate,
|
|
270
|
+
vib_novelty_score=vib_score,
|
|
271
|
+
predicted_class=pred_cls,
|
|
272
|
+
descending=desc,
|
|
273
|
+
cervical_wire_4b=wire_4b,
|
|
274
|
+
actuator_commands=alloc.commands,
|
|
275
|
+
actuator_health=alloc.health,
|
|
276
|
+
cord_min_health=float(np.min(alloc.health)),
|
|
277
|
+
mavlink_manual_pkt=mav_pkt,
|
|
278
|
+
)
|