w2t-bkin 0.0.6__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.
w2t_bkin/exceptions.py ADDED
@@ -0,0 +1,426 @@
1
+ """Structured exception hierarchy for W2T-BKIN pipeline.
2
+
3
+ All exceptions inherit from W2TError and include structured metadata:
4
+ - error_code: Stable identifier for programmatic handling
5
+ - message: Human-readable description
6
+ - context: Machine-readable error details (dict)
7
+ - hint: Actionable resolution suggestion
8
+ - stage: Pipeline stage where error occurred
9
+
10
+ Exception Hierarchy:
11
+ -------------------
12
+ W2TError (base)
13
+ ├── ConfigError
14
+ │ ├── ConfigMissingKeyError
15
+ │ ├── ConfigExtraKeyError
16
+ │ └── ConfigValidationError
17
+ ├── SessionError
18
+ │ ├── SessionMissingKeyError
19
+ │ ├── SessionExtraKeyError
20
+ │ └── SessionValidationError
21
+ ├── IngestError
22
+ │ ├── FileNotFoundError
23
+ │ ├── CameraUnverifiableError
24
+ │ └── VerificationError
25
+ │ └── MismatchExceedsToleranceError
26
+ ├── SyncError
27
+ │ ├── TimebaseProviderError
28
+ │ ├── JitterExceedsBudgetError
29
+ │ └── AlignmentError
30
+ ├── EventsError
31
+ │ ├── BpodParseError
32
+ │ └── BpodValidationError
33
+ ├── TranscodeError
34
+ ├── PoseError
35
+ ├── FacemapError
36
+ ├── NWBError
37
+ │ └── ExternalToolError
38
+ ├── ValidationError
39
+ └── QCError
40
+
41
+ Example:
42
+ >>> from w2t_bkin.exceptions import MismatchExceedsToleranceError
43
+ >>> try:
44
+ ... raise MismatchExceedsToleranceError(
45
+ ... camera_id="cam0",
46
+ ... frame_count=8580,
47
+ ... ttl_count=8578,
48
+ ... mismatch=2,
49
+ ... tolerance=1
50
+ ... )
51
+ ... except W2TError as e:
52
+ ... print(e.error_code) # MISMATCH_EXCEEDS_TOLERANCE
53
+ ... print(e.context) # {'camera_id': 'cam0', ...}
54
+ ... print(e.hint) # "Check TTL files..."
55
+ """
56
+
57
+ from typing import Any, Dict, Optional
58
+
59
+
60
+ class W2TError(Exception):
61
+ """Base exception for W2T-BKIN pipeline errors.
62
+
63
+ All exceptions inherit from this base and include structured metadata
64
+ for debugging, logging, and programmatic error handling.
65
+
66
+ Attributes:
67
+ error_code: Stable identifier (e.g., "CONFIG_MISSING_KEY")
68
+ message: Human-readable description
69
+ context: Machine-readable details as dict
70
+ hint: Actionable resolution suggestion
71
+ stage: Pipeline stage (config, ingest, sync, etc.)
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ error_code: str,
77
+ message: str,
78
+ context: Optional[Dict[str, Any]] = None,
79
+ hint: Optional[str] = None,
80
+ stage: Optional[str] = None,
81
+ ):
82
+ self.error_code = error_code
83
+ self.message = message
84
+ self.context = context or {}
85
+ self.hint = hint
86
+ self.stage = stage
87
+ super().__init__(self._format_message())
88
+
89
+ def _format_message(self) -> str:
90
+ """Format error with all structured fields."""
91
+ parts = [f"[{self.error_code}]", self.message]
92
+ if self.stage:
93
+ parts.insert(1, f"(stage: {self.stage})")
94
+ if self.context:
95
+ parts.append(f"Context: {self.context}")
96
+ if self.hint:
97
+ parts.append(f"Hint: {self.hint}")
98
+ return " ".join(parts)
99
+
100
+
101
+ # =============================================================================
102
+ # Configuration Errors
103
+ # =============================================================================
104
+
105
+
106
+ class ConfigError(W2TError):
107
+ """Base for configuration errors."""
108
+
109
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
110
+ super().__init__(error_code="CONFIG_ERROR", message=message, context=context, hint=hint, stage="config")
111
+
112
+
113
+ class ConfigMissingKeyError(ConfigError):
114
+ """Required configuration key not found."""
115
+
116
+ def __init__(self, key: str, file_path: str):
117
+ super().__init__(
118
+ message=f"Required configuration key missing: {key}",
119
+ context={"key": key, "file_path": file_path},
120
+ hint=f"Add '{key}' to {file_path}",
121
+ )
122
+ self.error_code = "CONFIG_MISSING_KEY"
123
+
124
+
125
+ class ConfigExtraKeyError(ConfigError):
126
+ """Unknown configuration key detected."""
127
+
128
+ def __init__(self, key: str, file_path: str, valid_keys: list):
129
+ super().__init__(
130
+ message=f"Unknown configuration key: {key}",
131
+ context={"key": key, "file_path": file_path, "valid_keys": valid_keys},
132
+ hint=f"Remove '{key}' or check for typos. Valid keys: {', '.join(valid_keys)}",
133
+ )
134
+ self.error_code = "CONFIG_EXTRA_KEY"
135
+
136
+
137
+ class ConfigValidationError(ConfigError):
138
+ """Configuration value failed validation."""
139
+
140
+ def __init__(self, key: str, value: Any, expected: str):
141
+ super().__init__(
142
+ message=f"Invalid value for '{key}': {value}",
143
+ context={"key": key, "value": value, "expected": expected},
144
+ hint=f"Expected {expected}",
145
+ )
146
+ self.error_code = "CONFIG_VALIDATION_ERROR"
147
+
148
+
149
+ # =============================================================================
150
+ # Session Errors
151
+ # =============================================================================
152
+
153
+
154
+ class SessionError(W2TError):
155
+ """Base for session configuration errors."""
156
+
157
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
158
+ super().__init__(error_code="SESSION_ERROR", message=message, context=context, hint=hint, stage="session")
159
+
160
+
161
+ class SessionMissingKeyError(SessionError):
162
+ """Required session key not found."""
163
+
164
+ def __init__(self, key: str, file_path: str):
165
+ super().__init__(
166
+ message=f"Required session key missing: {key}",
167
+ context={"key": key, "file_path": file_path},
168
+ hint=f"Add '{key}' to {file_path}",
169
+ )
170
+ self.error_code = "SESSION_MISSING_KEY"
171
+
172
+
173
+ class SessionExtraKeyError(SessionError):
174
+ """Unknown session key detected."""
175
+
176
+ def __init__(self, key: str, file_path: str, valid_keys: list):
177
+ super().__init__(
178
+ message=f"Unknown session key: {key}",
179
+ context={"key": key, "file_path": file_path, "valid_keys": valid_keys},
180
+ hint=f"Remove '{key}' or check for typos. Valid keys: {', '.join(valid_keys)}",
181
+ )
182
+ self.error_code = "SESSION_EXTRA_KEY"
183
+
184
+
185
+ class SessionValidationError(SessionError):
186
+ """Session value failed validation."""
187
+
188
+ def __init__(self, key: str, value: Any, expected: str):
189
+ super().__init__(
190
+ message=f"Invalid value for '{key}': {value}",
191
+ context={"key": key, "value": value, "expected": expected},
192
+ hint=f"Expected {expected}",
193
+ )
194
+ self.error_code = "SESSION_VALIDATION_ERROR"
195
+
196
+
197
+ # =============================================================================
198
+ # Ingest Errors
199
+ # =============================================================================
200
+
201
+
202
+ class IngestError(W2TError):
203
+ """Base for file discovery and ingestion errors."""
204
+
205
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
206
+ super().__init__(error_code="INGEST_ERROR", message=message, context=context, hint=hint, stage="ingest")
207
+
208
+
209
+ class FileNotFoundError(IngestError):
210
+ """Required file not found during discovery."""
211
+
212
+ def __init__(self, pattern: str, search_path: str):
213
+ super().__init__(
214
+ message=f"No files matching pattern: {pattern}",
215
+ context={"pattern": pattern, "search_path": search_path},
216
+ hint=f"Check that files exist in {search_path} and pattern is correct",
217
+ )
218
+ self.error_code = "FILE_NOT_FOUND"
219
+
220
+
221
+ class CameraUnverifiableError(IngestError):
222
+ """Camera references unknown TTL channel."""
223
+
224
+ def __init__(self, camera_id: str, ttl_id: str):
225
+ super().__init__(
226
+ message=f"Camera '{camera_id}' references unknown TTL '{ttl_id}'",
227
+ context={"camera_id": camera_id, "ttl_id": ttl_id},
228
+ hint=f"Add TTL entry for '{ttl_id}' to session.toml or correct camera.ttl_id",
229
+ )
230
+ self.error_code = "CAMERA_UNVERIFIABLE"
231
+
232
+
233
+ class VerificationError(IngestError):
234
+ """Base for frame/TTL verification errors."""
235
+
236
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
237
+ super().__init__(message=message, context=context, hint=hint)
238
+ self.error_code = "VERIFICATION_ERROR"
239
+
240
+
241
+ class MismatchExceedsToleranceError(VerificationError):
242
+ """Frame count vs TTL count mismatch exceeds tolerance."""
243
+
244
+ def __init__(self, camera_id: str, frame_count: int, ttl_count: int, mismatch: int, tolerance: int):
245
+ super().__init__(
246
+ message=f"Camera '{camera_id}' mismatch ({mismatch} frames) exceeds tolerance ({tolerance})",
247
+ context={
248
+ "camera_id": camera_id,
249
+ "frame_count": frame_count,
250
+ "ttl_count": ttl_count,
251
+ "mismatch": mismatch,
252
+ "tolerance": tolerance,
253
+ },
254
+ hint="Check TTL files for missing pulses or video corruption. " "Increase verification.mismatch_tolerance_frames if acceptable.",
255
+ )
256
+ self.error_code = "MISMATCH_EXCEEDS_TOLERANCE"
257
+
258
+
259
+ # =============================================================================
260
+ # Sync Errors
261
+ # =============================================================================
262
+
263
+
264
+ class SyncError(W2TError):
265
+ """Base for timebase synchronization errors."""
266
+
267
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
268
+ super().__init__(error_code="SYNC_ERROR", message=message, context=context, hint=hint, stage="sync")
269
+
270
+
271
+ class TimebaseProviderError(SyncError):
272
+ """Timebase provider initialization failed."""
273
+
274
+ def __init__(self, source: str, reason: str):
275
+ super().__init__(
276
+ message=f"Failed to initialize timebase provider '{source}': {reason}",
277
+ context={"source": source, "reason": reason},
278
+ hint="Check that timebase source files exist and are readable",
279
+ )
280
+ self.error_code = "TIMEBASE_PROVIDER_ERROR"
281
+
282
+
283
+ class JitterExceedsBudgetError(SyncError):
284
+ """Alignment jitter exceeds configured budget."""
285
+
286
+ def __init__(self, max_jitter_s: float, p95_jitter_s: float, budget_s: float):
287
+ super().__init__(
288
+ message=f"Alignment jitter (max={max_jitter_s:.6f}s, p95={p95_jitter_s:.6f}s) exceeds budget ({budget_s}s)",
289
+ context={"max_jitter_s": max_jitter_s, "p95_jitter_s": p95_jitter_s, "budget_s": budget_s},
290
+ hint="Increase timebase.jitter_budget_s or investigate timing quality. " "Check TTL pulse spacing and video framerate stability.",
291
+ )
292
+ self.error_code = "JITTER_EXCEEDS_BUDGET"
293
+
294
+
295
+ class AlignmentError(SyncError):
296
+ """Sample alignment failed."""
297
+
298
+ def __init__(self, reason: str, context: Optional[Dict[str, Any]] = None):
299
+ super().__init__(message=f"Sample alignment failed: {reason}", context=context, hint="Check timebase configuration and data quality")
300
+ self.error_code = "ALIGNMENT_ERROR"
301
+
302
+
303
+ # =============================================================================
304
+ # Events Errors
305
+ # =============================================================================
306
+
307
+
308
+ class EventsError(W2TError):
309
+ """Base for behavioral events parsing errors."""
310
+
311
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
312
+ super().__init__(error_code="EVENTS_ERROR", message=message, context=context, hint=hint, stage="events")
313
+
314
+
315
+ class BpodParseError(EventsError):
316
+ """Bpod .mat file parsing failed."""
317
+
318
+ def __init__(self, reason: str, file_path: Optional[str] = None):
319
+ context = {"reason": reason}
320
+ if file_path:
321
+ context["file_path"] = file_path
322
+ super().__init__(
323
+ message=f"Failed to parse Bpod file: {reason}",
324
+ context=context,
325
+ hint="Check that file is valid Bpod .mat format and not corrupted",
326
+ )
327
+ self.error_code = "BPOD_PARSE_ERROR"
328
+
329
+
330
+ class BpodValidationError(EventsError):
331
+ """Bpod file or data validation failed."""
332
+
333
+ def __init__(self, reason: str, file_path: Optional[str] = None):
334
+ context = {"reason": reason}
335
+ if file_path:
336
+ context["file_path"] = file_path
337
+ super().__init__(
338
+ message=f"Bpod validation failed: {reason}",
339
+ context=context,
340
+ hint="Check file path, size, extension, and data structure",
341
+ )
342
+ self.error_code = "BPOD_VALIDATION_ERROR"
343
+
344
+
345
+ # =============================================================================
346
+ # Transcode Errors
347
+ # =============================================================================
348
+
349
+
350
+ class TranscodeError(W2TError):
351
+ """Base for video transcoding errors."""
352
+
353
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
354
+ super().__init__(error_code="TRANSCODE_ERROR", message=message, context=context, hint=hint, stage="transcode")
355
+
356
+
357
+ # =============================================================================
358
+ # Pose Errors
359
+ # =============================================================================
360
+
361
+
362
+ class PoseError(W2TError):
363
+ """Base for pose estimation errors."""
364
+
365
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
366
+ super().__init__(error_code="POSE_ERROR", message=message, context=context, hint=hint, stage="pose")
367
+
368
+
369
+ # =============================================================================
370
+ # Facemap Errors
371
+ # =============================================================================
372
+
373
+
374
+ class FacemapError(W2TError):
375
+ """Base for facemap processing errors."""
376
+
377
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
378
+ super().__init__(error_code="FACEMAP_ERROR", message=message, context=context, hint=hint, stage="facemap")
379
+
380
+
381
+ # =============================================================================
382
+ # NWB Errors
383
+ # =============================================================================
384
+
385
+
386
+ class NWBError(W2TError):
387
+ """Base for NWB assembly errors."""
388
+
389
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
390
+ super().__init__(error_code="NWB_ERROR", message=message, context=context, hint=hint, stage="nwb")
391
+
392
+
393
+ class ExternalToolError(NWBError):
394
+ """External tool execution failed."""
395
+
396
+ def __init__(self, tool: str, command: str, return_code: int, stderr: str):
397
+ super().__init__(
398
+ message=f"External tool '{tool}' failed with code {return_code}",
399
+ context={"tool": tool, "command": command, "return_code": return_code, "stderr": stderr},
400
+ hint=f"Check that {tool} is installed and accessible in PATH",
401
+ )
402
+ self.error_code = "EXTERNAL_TOOL_ERROR"
403
+
404
+
405
+ # =============================================================================
406
+ # Validation Errors
407
+ # =============================================================================
408
+
409
+
410
+ class ValidationError(W2TError):
411
+ """Base for NWB validation errors."""
412
+
413
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
414
+ super().__init__(error_code="VALIDATION_ERROR", message=message, context=context, hint=hint, stage="validation")
415
+
416
+
417
+ # =============================================================================
418
+ # QC Errors
419
+ # =============================================================================
420
+
421
+
422
+ class QCError(W2TError):
423
+ """Base for QC report generation errors."""
424
+
425
+ def __init__(self, message: str, context: Optional[Dict[str, Any]] = None, hint: Optional[str] = None):
426
+ super().__init__(error_code="QC_ERROR", message=message, context=context, hint=hint, stage="qc")
@@ -0,0 +1,42 @@
1
+ """Facemap motion energy computation and alignment module (Phase 3 - Optional).
2
+
3
+ Provides Facemap ROI handling, signal import/compute, and alignment to reference timebase
4
+ for facial motion analysis.
5
+
6
+ Public API:
7
+ -----------
8
+ All public functions and models are re-exported at the package level:
9
+
10
+ from w2t_bkin.facemap import (
11
+ FacemapBundle,
12
+ FacemapROI,
13
+ FacemapSignal,
14
+ define_rois,
15
+ import_facemap_output,
16
+ compute_facemap_signals,
17
+ align_facemap_to_timebase,
18
+ )
19
+
20
+ See core and models modules for detailed documentation.
21
+ """
22
+
23
+ # Re-export core functions
24
+ from .core import FacemapError, align_facemap_to_timebase, compute_facemap_signals, define_rois, import_facemap_output, validate_facemap_sampling_rate
25
+
26
+ # Re-export models
27
+ from .models import FacemapBundle, FacemapROI, FacemapSignal
28
+
29
+ __all__ = [
30
+ # Models
31
+ "FacemapBundle",
32
+ "FacemapROI",
33
+ "FacemapSignal",
34
+ # Exceptions
35
+ "FacemapError",
36
+ # Core functions
37
+ "define_rois",
38
+ "import_facemap_output",
39
+ "compute_facemap_signals",
40
+ "align_facemap_to_timebase",
41
+ "validate_facemap_sampling_rate",
42
+ ]