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/__init__.py +85 -0
- w2t_bkin/behavior/__init__.py +115 -0
- w2t_bkin/behavior/core.py +1027 -0
- w2t_bkin/bpod/__init__.py +38 -0
- w2t_bkin/bpod/core.py +519 -0
- w2t_bkin/config.py +625 -0
- w2t_bkin/dlc/__init__.py +59 -0
- w2t_bkin/dlc/core.py +448 -0
- w2t_bkin/dlc/models.py +124 -0
- w2t_bkin/exceptions.py +426 -0
- w2t_bkin/facemap/__init__.py +42 -0
- w2t_bkin/facemap/core.py +397 -0
- w2t_bkin/facemap/models.py +134 -0
- w2t_bkin/pipeline.py +665 -0
- w2t_bkin/pose/__init__.py +48 -0
- w2t_bkin/pose/core.py +227 -0
- w2t_bkin/pose/io.py +363 -0
- w2t_bkin/pose/skeleton.py +165 -0
- w2t_bkin/pose/ttl_mock.py +477 -0
- w2t_bkin/session.py +423 -0
- w2t_bkin/sync/__init__.py +72 -0
- w2t_bkin/sync/core.py +678 -0
- w2t_bkin/sync/stats.py +176 -0
- w2t_bkin/sync/timebase.py +311 -0
- w2t_bkin/sync/ttl.py +254 -0
- w2t_bkin/transcode/__init__.py +38 -0
- w2t_bkin/transcode/core.py +303 -0
- w2t_bkin/transcode/models.py +96 -0
- w2t_bkin/ttl/__init__.py +64 -0
- w2t_bkin/ttl/core.py +518 -0
- w2t_bkin/ttl/models.py +19 -0
- w2t_bkin/utils.py +1093 -0
- w2t_bkin-0.0.6.dist-info/METADATA +145 -0
- w2t_bkin-0.0.6.dist-info/RECORD +36 -0
- w2t_bkin-0.0.6.dist-info/WHEEL +4 -0
- w2t_bkin-0.0.6.dist-info/licenses/LICENSE +201 -0
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
|
+
]
|