mesofield 0.3.2b0__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.
- docs/_static/custom.css +40 -0
- docs/_static/favicon.png +0 -0
- docs/_static/logo.png +0 -0
- docs/api/index.md +70 -0
- docs/conf.py +200 -0
- docs/developer_guide.md +303 -0
- docs/index.md +25 -0
- docs/tutorial.md +4 -0
- docs/user_guide.md +172 -0
- examples/teensy_pulse_generator.py +320 -0
- experiments/pipeline_demo/experiment.json +24 -0
- experiments/pipeline_demo/hardware.yaml +23 -0
- experiments/pipeline_demo/procedure.py +50 -0
- experiments/two_cam_demo/experiment.json +24 -0
- experiments/two_cam_demo/hardware.yaml +58 -0
- experiments/two_cam_demo/load_dataset.py +213 -0
- experiments/two_cam_demo/procedure.py +87 -0
- external/video-codecs/openh264-1.8.0-win64.dll +0 -0
- mesofield/__init__.py +45 -0
- mesofield/__main__.py +11 -0
- mesofield/_version.py +24 -0
- mesofield/base.py +750 -0
- mesofield/cli/__init__.py +57 -0
- mesofield/cli/_richhelp.py +100 -0
- mesofield/cli/acquire.py +254 -0
- mesofield/cli/datakit.py +165 -0
- mesofield/cli/process.py +376 -0
- mesofield/cli/rig.py +108 -0
- mesofield/cli/tools.py +347 -0
- mesofield/config.py +751 -0
- mesofield/data/__init__.py +23 -0
- mesofield/data/batch.py +633 -0
- mesofield/data/manager.py +388 -0
- mesofield/data/writer.py +289 -0
- mesofield/datakit/__init__.py +44 -0
- mesofield/datakit/__main__.py +35 -0
- mesofield/datakit/_utils/_logger.py +5 -0
- mesofield/datakit/_version.py +141 -0
- mesofield/datakit/config.py +50 -0
- mesofield/datakit/core.py +783 -0
- mesofield/datakit/datamodel.py +200 -0
- mesofield/datakit/discover.py +124 -0
- mesofield/datakit/explore.py +651 -0
- mesofield/datakit/notebooks/pupil_dlc.ipynb +2445 -0
- mesofield/datakit/profile.py +535 -0
- mesofield/datakit/shell.py +83 -0
- mesofield/datakit/sources/__init__.py +65 -0
- mesofield/datakit/sources/analysis/mesomap.py +194 -0
- mesofield/datakit/sources/analysis/mesoscope.py +77 -0
- mesofield/datakit/sources/analysis/pupil.py +246 -0
- mesofield/datakit/sources/behavior/__init__.py +0 -0
- mesofield/datakit/sources/behavior/dataqueue.py +281 -0
- mesofield/datakit/sources/behavior/psychopy.py +364 -0
- mesofield/datakit/sources/behavior/treadmill.py +323 -0
- mesofield/datakit/sources/behavior/wheel.py +277 -0
- mesofield/datakit/sources/camera/mesoscope.py +32 -0
- mesofield/datakit/sources/camera/metadata_json.py +130 -0
- mesofield/datakit/sources/camera/pupil.py +28 -0
- mesofield/datakit/sources/camera/suite2p.py +547 -0
- mesofield/datakit/sources/register.py +204 -0
- mesofield/datakit/sources/session/config.py +130 -0
- mesofield/datakit/sources/session/notes.py +63 -0
- mesofield/datakit/sources/session/timestamps.py +58 -0
- mesofield/datakit/timeline.py +306 -0
- mesofield/devices/__init__.py +42 -0
- mesofield/devices/base.py +498 -0
- mesofield/devices/base_camera.py +295 -0
- mesofield/devices/cameras.py +740 -0
- mesofield/devices/daq.py +151 -0
- mesofield/devices/encoder.py +384 -0
- mesofield/devices/mocks.py +275 -0
- mesofield/devices/psychopy_device.py +455 -0
- mesofield/devices/subprocesses/__init__.py +0 -0
- mesofield/devices/subprocesses/psychopy.py +133 -0
- mesofield/devices/treadmill.py +318 -0
- mesofield/engines.py +380 -0
- mesofield/gui/Mesofield_icon.png +0 -0
- mesofield/gui/__init__.py +76 -0
- mesofield/gui/config_wizard.py +724 -0
- mesofield/gui/controller.py +535 -0
- mesofield/gui/dynamic_controller.py +78 -0
- mesofield/gui/maingui.py +427 -0
- mesofield/gui/mdagui.py +285 -0
- mesofield/gui/qt_device_adapter.py +109 -0
- mesofield/gui/speedplotter.py +152 -0
- mesofield/gui/theme.py +445 -0
- mesofield/gui/tiff_viewer.py +1050 -0
- mesofield/gui/viewer.py +691 -0
- mesofield/hardware.py +549 -0
- mesofield/playback.py +1298 -0
- mesofield/processing/__init__.py +12 -0
- mesofield/processing/runner.py +237 -0
- mesofield/processors/__init__.py +13 -0
- mesofield/processors/base.py +287 -0
- mesofield/processors/frame_mean.py +19 -0
- mesofield/protocols.py +378 -0
- mesofield/scaffold/__init__.py +34 -0
- mesofield/scaffold/experiment.py +400 -0
- mesofield/scaffold/rigs.py +121 -0
- mesofield/signals.py +85 -0
- mesofield/utils/__init__.py +0 -0
- mesofield/utils/_logger.py +156 -0
- mesofield/utils/retrofit.py +309 -0
- mesofield/utils/utils.py +217 -0
- mesofield-0.3.2b0.dist-info/METADATA +178 -0
- mesofield-0.3.2b0.dist-info/RECORD +111 -0
- mesofield-0.3.2b0.dist-info/WHEEL +5 -0
- mesofield-0.3.2b0.dist-info/entry_points.txt +2 -0
- mesofield-0.3.2b0.dist-info/licenses/LICENSE +21 -0
- mesofield-0.3.2b0.dist-info/top_level.txt +6 -0
- scripts/bench_frame_processor.py +103 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
"""Teensy treadmill encoder over USB-serial.
|
|
2
|
+
|
|
3
|
+
Firmware Data Format:
|
|
4
|
+
- With SHOW_MICROS defined: ``"micros,distance(mm),speed(mm/s)"``
|
|
5
|
+
- Without SHOW_MICROS: ``"distance(mm),speed(mm/s)"``
|
|
6
|
+
|
|
7
|
+
Supported Commands:
|
|
8
|
+
| Command | Description |
|
|
9
|
+
|---------|----------------------------------------------|
|
|
10
|
+
| '?' | Print version and header info |
|
|
11
|
+
| 'c' | Initiate speed output calibration |
|
|
12
|
+
|
|
13
|
+
Built on :class:`mesofield.devices.base.BaseSerialDevice`. Each parsed
|
|
14
|
+
line is recorded as a dict ``{"distance": float, "speed": float,
|
|
15
|
+
"device_us": int|None}`` so that the default
|
|
16
|
+
:meth:`BaseDataProducer.save_data` writes a 4-column CSV
|
|
17
|
+
(``timestamp,distance,speed,device_us``).
|
|
18
|
+
|
|
19
|
+
The producer (`EncoderSerialInterface`) writes the CSV; the matching
|
|
20
|
+
ingest-side parser (`TreadmillSource`) lives at the bottom of this
|
|
21
|
+
module so producer and parser sit in the same file.
|
|
22
|
+
`EncoderSerialInterface.Parser` resolves to `TreadmillSource` for
|
|
23
|
+
manifest-driven dispatch.
|
|
24
|
+
"""
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
import re
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
from typing import Any, ClassVar, Dict, Optional, Tuple
|
|
30
|
+
|
|
31
|
+
import numpy as np
|
|
32
|
+
import pandas as pd
|
|
33
|
+
|
|
34
|
+
from mesofield import DeviceRegistry
|
|
35
|
+
from mesofield.devices.base import BaseSerialDevice
|
|
36
|
+
from mesofield.datakit.config import settings
|
|
37
|
+
from mesofield.datakit.sources.register import TimeseriesSource
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@DeviceRegistry.register("encoder")
|
|
41
|
+
class EncoderSerialInterface(BaseSerialDevice):
|
|
42
|
+
"""Teensy encoder/treadmill device.
|
|
43
|
+
|
|
44
|
+
Constructor accepts either a cfg dict (``BaseSerialDevice``-style) or
|
|
45
|
+
legacy positional/keyword args ``(port, baudrate)`` for backward
|
|
46
|
+
compatibility with :mod:`mesofield.hardware`.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
device_type: ClassVar[str] = "encoder"
|
|
50
|
+
file_type: ClassVar[str] = "csv"
|
|
51
|
+
bids_type: ClassVar[Optional[str]] = "beh"
|
|
52
|
+
default_baudrate: ClassVar[int] = 192000
|
|
53
|
+
|
|
54
|
+
# Typed contract for the parser's dataqueue lookup. parse_line returns a
|
|
55
|
+
# dict {distance, speed, device_us}, so the dataqueue payload column holds
|
|
56
|
+
# a dict-repr. In Step 4.6 the parser will read payload_fields off the
|
|
57
|
+
# manifest instead of hardcoding the "EncoderData timestamp=N" regex.
|
|
58
|
+
dataqueue_payload_schema: ClassVar[Optional[dict]] = {
|
|
59
|
+
"device_id": "treadmill",
|
|
60
|
+
"payload_format": "dict",
|
|
61
|
+
"payload_fields": {
|
|
62
|
+
"distance": "float",
|
|
63
|
+
"speed": "float",
|
|
64
|
+
"device_us": "int",
|
|
65
|
+
},
|
|
66
|
+
"description": "Dict payload pushed by parse_line(); device_us is the master clock anchor.",
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
def __init__(
|
|
70
|
+
self,
|
|
71
|
+
cfg: Optional[Dict[str, Any]] = None,
|
|
72
|
+
port: Optional[str] = None,
|
|
73
|
+
baudrate: Optional[int] = None,
|
|
74
|
+
**kwargs: Any,
|
|
75
|
+
) -> None:
|
|
76
|
+
if cfg is None:
|
|
77
|
+
cfg = {}
|
|
78
|
+
else:
|
|
79
|
+
cfg = dict(cfg)
|
|
80
|
+
if port is not None:
|
|
81
|
+
cfg.setdefault("port", port)
|
|
82
|
+
if baudrate is not None:
|
|
83
|
+
cfg.setdefault("baudrate", baudrate)
|
|
84
|
+
cfg.setdefault("id", "treadmill")
|
|
85
|
+
|
|
86
|
+
super().__init__(cfg, **kwargs)
|
|
87
|
+
|
|
88
|
+
# Optional Qt adapter for GUI live-preview signals. Lazy import
|
|
89
|
+
# so headless sessions don't require PyQt6.
|
|
90
|
+
self._qt_adapter = None
|
|
91
|
+
self.serialDataReceived = None
|
|
92
|
+
self.serialSpeedUpdated = None
|
|
93
|
+
try:
|
|
94
|
+
from mesofield.gui.qt_device_adapter import QtDeviceAdapter
|
|
95
|
+
|
|
96
|
+
self._qt_adapter = QtDeviceAdapter(self)
|
|
97
|
+
self.serialDataReceived = self._qt_adapter.serialDataReceived
|
|
98
|
+
self.serialSpeedUpdated = self._qt_adapter.serialSpeedUpdated
|
|
99
|
+
except Exception:
|
|
100
|
+
self.logger.debug("Qt adapter unavailable; running headless.")
|
|
101
|
+
|
|
102
|
+
# -- BaseSerialDevice hooks ----------------------------------------
|
|
103
|
+
def parse_line(
|
|
104
|
+
self, line: bytes
|
|
105
|
+
) -> Optional[Tuple[Dict[str, Any], Optional[float]]]:
|
|
106
|
+
text = line.decode("utf-8", errors="replace").strip()
|
|
107
|
+
if not text:
|
|
108
|
+
return None
|
|
109
|
+
parts = text.split(",")
|
|
110
|
+
try:
|
|
111
|
+
if len(parts) == 3:
|
|
112
|
+
device_us = int(parts[0].strip())
|
|
113
|
+
distance = float(parts[1].strip())
|
|
114
|
+
speed = float(parts[2].strip())
|
|
115
|
+
elif len(parts) == 2:
|
|
116
|
+
device_us = None
|
|
117
|
+
distance = float(parts[0].strip())
|
|
118
|
+
speed = float(parts[1].strip())
|
|
119
|
+
else:
|
|
120
|
+
self.logger.debug("Ignored non-data line: %r", text)
|
|
121
|
+
return None
|
|
122
|
+
except ValueError:
|
|
123
|
+
self.logger.debug("Failed to parse line: %r", text)
|
|
124
|
+
return None
|
|
125
|
+
|
|
126
|
+
return {"distance": distance, "speed": speed, "device_us": device_us}, None
|
|
127
|
+
|
|
128
|
+
# -- Convenience wrapper for firmware commands ---------------------
|
|
129
|
+
def send_command(self, command: str) -> bytes:
|
|
130
|
+
"""Send a single-character firmware command (no newline)."""
|
|
131
|
+
return self.send_line(command, newline=b"")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# ---------------------------------------------------------------------------
|
|
135
|
+
# Parser (the ingest-side counterpart). Lives in the same file so producer-
|
|
136
|
+
# side payload changes are forced to confront parser-side regexes.
|
|
137
|
+
# Bound below as `EncoderSerialInterface.Parser`.
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
class TreadmillSource(TimeseriesSource):
|
|
141
|
+
"""Load treadmill samples aligned to the experiment window."""
|
|
142
|
+
tag = "treadmill"
|
|
143
|
+
patterns = ("**/*_treadmill.csv", "**/*_treadmill_data.csv")
|
|
144
|
+
camera_tag = None
|
|
145
|
+
|
|
146
|
+
timestamp_column = "timestamp"
|
|
147
|
+
distance_columns = ("distance_mm", "distance")
|
|
148
|
+
speed_columns = ("speed_mm", "speed_mm_s", "speed")
|
|
149
|
+
output_distance_column = "distance_mm"
|
|
150
|
+
output_speed_column = "speed_mm"
|
|
151
|
+
|
|
152
|
+
dataqueue_queue_column = settings.timeline.queue_column
|
|
153
|
+
dataqueue_device_id_column = "device_id"
|
|
154
|
+
dataqueue_payload_column = "payload"
|
|
155
|
+
|
|
156
|
+
dataqueue_payload_prefix = "EncoderData"
|
|
157
|
+
timeline_master_patterns = ("dhyana", "mesoscope")
|
|
158
|
+
_encoder_ts_re = re.compile(r"timestamp\s*=\s*(\d+)", re.IGNORECASE)
|
|
159
|
+
|
|
160
|
+
def build_timeseries(
|
|
161
|
+
self,
|
|
162
|
+
path: Path,
|
|
163
|
+
*,
|
|
164
|
+
context: SourceContext | None = None,
|
|
165
|
+
) -> tuple[np.ndarray, pd.DataFrame, dict]:
|
|
166
|
+
context = self._require_context(context)
|
|
167
|
+
df = pd.read_csv(path)
|
|
168
|
+
dist = next((c for c in self.distance_columns if c in df.columns), None)
|
|
169
|
+
speed = next((c for c in self.speed_columns if c in df.columns), None)
|
|
170
|
+
if self.timestamp_column not in df.columns or dist is None or speed is None:
|
|
171
|
+
raise ValueError(f"Expected timestamp/distance/speed columns in {path}")
|
|
172
|
+
df = df.rename(columns={dist: self.output_distance_column, speed: self.output_speed_column})
|
|
173
|
+
|
|
174
|
+
dq_path = context.path_for("dataqueue")
|
|
175
|
+
aligned, meta = self.extract_treadmill_aligned(
|
|
176
|
+
df,
|
|
177
|
+
dq_path,
|
|
178
|
+
window=context.experiment_window,
|
|
179
|
+
dataqueue_frame=context.dataqueue_frame,
|
|
180
|
+
)
|
|
181
|
+
t = aligned["time_elapsed_s"].to_numpy(dtype=np.float64)
|
|
182
|
+
meta = {
|
|
183
|
+
"source_file": str(path),
|
|
184
|
+
"dataqueue_file": str(dq_path) if dq_path is not None else None,
|
|
185
|
+
"n_samples": len(aligned),
|
|
186
|
+
**meta,
|
|
187
|
+
}
|
|
188
|
+
return t, aligned, meta
|
|
189
|
+
|
|
190
|
+
# --- alignment --------------------------------
|
|
191
|
+
|
|
192
|
+
def extract_treadmill_aligned(
|
|
193
|
+
self,
|
|
194
|
+
treadmill_df: pd.DataFrame,
|
|
195
|
+
dq_path: Path | None,
|
|
196
|
+
*,
|
|
197
|
+
window: tuple[float, float] | None = None,
|
|
198
|
+
dataqueue_frame: pd.DataFrame | None = None,
|
|
199
|
+
) -> tuple[pd.DataFrame, dict]:
|
|
200
|
+
t0, t1 = window or self._window(dq_path, dataqueue_frame)
|
|
201
|
+
duration = float(t1 - t0)
|
|
202
|
+
if not np.isfinite(duration) or duration <= 0:
|
|
203
|
+
raise ValueError(f"Invalid camera window duration from dataqueue: start={t0}, end={t1}")
|
|
204
|
+
|
|
205
|
+
enc = self._encoder_rows(dq_path, dataqueue_frame)
|
|
206
|
+
a, b = self._fit(enc["encoder_ts"].to_numpy(), enc["queue_elapsed"].to_numpy())
|
|
207
|
+
|
|
208
|
+
ts = pd.to_numeric(treadmill_df[self.timestamp_column], errors="coerce").to_numpy(dtype=np.float64)
|
|
209
|
+
time_s = a * ts + b - t0
|
|
210
|
+
|
|
211
|
+
mask = np.isfinite(time_s) & (time_s >= 0.0) & (time_s <= duration)
|
|
212
|
+
if not np.any(mask):
|
|
213
|
+
raise ValueError("No treadmill samples fall inside the Dhyana window")
|
|
214
|
+
|
|
215
|
+
aligned = treadmill_df.loc[mask].copy()
|
|
216
|
+
aligned["time_elapsed_s"] = time_s[mask]
|
|
217
|
+
aligned.sort_values("time_elapsed_s", inplace=True)
|
|
218
|
+
aligned.reset_index(drop=True, inplace=True)
|
|
219
|
+
|
|
220
|
+
return aligned, {
|
|
221
|
+
"source_method": "dataqueue_align_mvp",
|
|
222
|
+
"experiment_window": {"start": float(t0), "end": float(t1)},
|
|
223
|
+
"alignment": {"a": float(a), "b": float(b)},
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
# --- dataqueue utilities -----------------------------------------------------
|
|
227
|
+
|
|
228
|
+
def _window(
|
|
229
|
+
self,
|
|
230
|
+
dq_path: Path | None,
|
|
231
|
+
dataqueue_frame: pd.DataFrame | None,
|
|
232
|
+
) -> tuple[float, float]:
|
|
233
|
+
dq = dataqueue_frame
|
|
234
|
+
if dq is None:
|
|
235
|
+
if dq_path is None:
|
|
236
|
+
raise FileNotFoundError("TreadmillSource: dataqueue path not available")
|
|
237
|
+
dq = pd.read_csv(
|
|
238
|
+
dq_path,
|
|
239
|
+
usecols=[self.dataqueue_queue_column, self.dataqueue_device_id_column],
|
|
240
|
+
low_memory=False,
|
|
241
|
+
)
|
|
242
|
+
device = dq.get(self.dataqueue_device_id_column, pd.Series(dtype=str)).astype(str)
|
|
243
|
+
|
|
244
|
+
mask = np.zeros(len(dq), dtype=bool)
|
|
245
|
+
for pattern in self.timeline_master_patterns:
|
|
246
|
+
mask |= device.str.contains(pattern, case=False, na=False, regex=False).to_numpy()
|
|
247
|
+
|
|
248
|
+
rows = pd.to_numeric(dq.loc[mask, self.dataqueue_queue_column], errors="coerce").dropna()
|
|
249
|
+
if len(rows) < 2:
|
|
250
|
+
raise ValueError("Could not find >=2 master camera rows in dataqueue")
|
|
251
|
+
return float(rows.iloc[0]), float(rows.iloc[-1])
|
|
252
|
+
|
|
253
|
+
def _encoder_rows(
|
|
254
|
+
self,
|
|
255
|
+
dq_path: Path | None,
|
|
256
|
+
dataqueue_frame: pd.DataFrame | None,
|
|
257
|
+
) -> pd.DataFrame:
|
|
258
|
+
dq = dataqueue_frame
|
|
259
|
+
if dq is None:
|
|
260
|
+
if dq_path is None:
|
|
261
|
+
raise FileNotFoundError("TreadmillSource: dataqueue path not available")
|
|
262
|
+
dq = pd.read_csv(
|
|
263
|
+
dq_path,
|
|
264
|
+
usecols=[self.dataqueue_queue_column, self.dataqueue_payload_column],
|
|
265
|
+
low_memory=False,
|
|
266
|
+
)
|
|
267
|
+
payload = dq[self.dataqueue_payload_column].astype(str)
|
|
268
|
+
|
|
269
|
+
# strict parse only from EncoderData payloads with timestamp=...
|
|
270
|
+
mask = payload.str.contains(self.dataqueue_payload_prefix, na=False)
|
|
271
|
+
if not np.any(mask):
|
|
272
|
+
raise ValueError("No EncoderData payloads found in dataqueue")
|
|
273
|
+
|
|
274
|
+
enc = dq.loc[mask, [self.dataqueue_queue_column, self.dataqueue_payload_column]].copy()
|
|
275
|
+
enc["encoder_ts"] = enc[self.dataqueue_payload_column].apply(self._parse_encoder_ts)
|
|
276
|
+
|
|
277
|
+
enc = enc.dropna(subset=["encoder_ts", self.dataqueue_queue_column])
|
|
278
|
+
enc = enc.rename(columns={self.dataqueue_queue_column: "queue_elapsed"})
|
|
279
|
+
enc["queue_elapsed"] = pd.to_numeric(enc["queue_elapsed"], errors="coerce")
|
|
280
|
+
enc = enc.dropna(subset=["queue_elapsed"])
|
|
281
|
+
|
|
282
|
+
if len(enc) < 2:
|
|
283
|
+
raise ValueError("Insufficient encoder samples for alignment")
|
|
284
|
+
return enc
|
|
285
|
+
|
|
286
|
+
def _parse_encoder_ts(self, payload: str) -> int | None:
|
|
287
|
+
m = self._encoder_ts_re.search(payload or "")
|
|
288
|
+
return int(m.group(1)) if m else None
|
|
289
|
+
|
|
290
|
+
def _fit(self, encoder_ts: np.ndarray, queue_elapsed: np.ndarray) -> tuple[float, float]:
|
|
291
|
+
x = np.asarray(encoder_ts, dtype=np.float64)
|
|
292
|
+
y = np.asarray(queue_elapsed, dtype=np.float64)
|
|
293
|
+
mask = np.isfinite(x) & np.isfinite(y)
|
|
294
|
+
x = x[mask]
|
|
295
|
+
y = y[mask]
|
|
296
|
+
if x.size < 2:
|
|
297
|
+
raise ValueError("Insufficient encoder samples for alignment")
|
|
298
|
+
|
|
299
|
+
# Centered affine fit (stable for large microsecond timestamps)
|
|
300
|
+
x0 = float(x.mean())
|
|
301
|
+
y0 = float(y.mean())
|
|
302
|
+
xc = x - x0
|
|
303
|
+
yc = y - y0
|
|
304
|
+
denom = float(np.dot(xc, xc))
|
|
305
|
+
if denom <= 0:
|
|
306
|
+
raise ValueError("Degenerate encoder timestamps for alignment")
|
|
307
|
+
|
|
308
|
+
a = float(np.dot(xc, yc) / denom)
|
|
309
|
+
b = float(y0 - a * x0)
|
|
310
|
+
|
|
311
|
+
if not np.isfinite(a) or not np.isfinite(b):
|
|
312
|
+
raise ValueError("Encoder alignment fit returned non-finite coefficients")
|
|
313
|
+
return a, b
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
# Manifest-driven dispatch: SOURCE_REGISTRY["treadmill"] resolves to
|
|
317
|
+
# EncoderSerialInterface.Parser.
|
|
318
|
+
EncoderSerialInterface.Parser = TreadmillSource
|