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.
@@ -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
+ )