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/session.py ADDED
@@ -0,0 +1,423 @@
1
+ """Session metadata loading and NWBFile creation.
2
+
3
+ This module provides functionality to load session.toml files and create
4
+ pynwb.NWBFile objects with complete metadata. It bridges session configuration
5
+ files and NWB file generation.
6
+
7
+ Key Features:
8
+ -------------
9
+ - **TOML Loading**: Parse session.toml with validation
10
+ - **NWBFile Creation**: Convert session metadata to pynwb.NWBFile
11
+ - **Subject Metadata**: Full pynwb.file.Subject object creation
12
+ - **Device Management**: Create Device objects from config
13
+ - **Flexible Input**: Accept Path, str, or dict
14
+ - **ISO 8601 Support**: Parse datetime strings correctly
15
+
16
+ Main Functions:
17
+ ---------------
18
+ - load_session_metadata: Load and parse session.toml file
19
+ - create_nwb_file: Create pynwb.NWBFile from session metadata
20
+ - create_subject: Create pynwb.file.Subject from metadata
21
+ - create_devices: Create Device objects from device list
22
+
23
+ Requirements:
24
+ -------------
25
+ - FR-7: NWB file assembly with metadata
26
+ - FR-10: Configuration management
27
+ - NFR-1: Reproducibility (complete metadata)
28
+ - NFR-11: Provenance tracking
29
+
30
+ Example:
31
+ --------
32
+ >>> from w2t_bkin.session import load_session_metadata, create_nwb_file
33
+ >>> from pathlib import Path
34
+ >>>
35
+ >>> # Load session metadata from TOML
36
+ >>> session_path = Path("data/raw/Session-000001/session.toml")
37
+ >>> metadata = load_session_metadata(session_path)
38
+ >>>
39
+ >>> # Create NWBFile object
40
+ >>> nwbfile = create_nwb_file(metadata)
41
+ >>> print(f"Session: {nwbfile.session_id}")
42
+ >>> print(f"Subject: {nwbfile.subject.subject_id}")
43
+ """
44
+
45
+ # TODO: lab_meta_data create NWB extension to hold lab-specific metadata
46
+
47
+ from pathlib import Path
48
+ from typing import Any, Dict, List, Optional, Union
49
+
50
+ from pynwb import NWBHDF5IO, NWBFile
51
+ from pynwb.device import Device, DeviceModel
52
+ from pynwb.file import Subject
53
+ from pynwb.image import ImageSeries
54
+ import tomli
55
+
56
+ from w2t_bkin import utils
57
+
58
+
59
+ def load_session_metadata(session_path: Union[str, Path]) -> Dict[str, Any]:
60
+ """Load session metadata from TOML file.
61
+
62
+ Reads and parses a session.toml file containing NWB metadata.
63
+
64
+ Parameters
65
+ ----------
66
+ session_path : Union[str, Path]
67
+ Path to session.toml file
68
+
69
+ Returns
70
+ -------
71
+ Dict[str, Any]
72
+ Parsed session metadata dictionary
73
+
74
+ Raises
75
+ ------
76
+ FileNotFoundError
77
+ If session.toml does not exist
78
+ ValueError
79
+ If TOML is invalid or malformed
80
+
81
+ Example
82
+ -------
83
+ >>> metadata = load_session_metadata("Session-000001/session.toml")
84
+ >>> print(metadata["identifier"])
85
+ Session-000001
86
+ """
87
+ session_path = Path(session_path)
88
+
89
+ if not session_path.exists():
90
+ raise FileNotFoundError(f"Session file not found: {session_path}")
91
+
92
+ with open(session_path, "rb") as f:
93
+ metadata = tomli.load(f)
94
+
95
+ return metadata
96
+
97
+
98
+ def create_nwb_file(session_file: Union[str, Path, Dict[str, Any]]) -> NWBFile:
99
+ """Create NWBFile object from session metadata.
100
+
101
+ Example
102
+ -------
103
+ >>> # From file path
104
+ >>> nwbfile = create_nwb_file("Session-000001/session.toml")
105
+ >>>
106
+ >>> # From metadata dict
107
+ >>> metadata = {"identifier": "S001", "session_start_time": "2025-01-15T14:30:00", ...}
108
+ >>> nwbfile = create_nwb_file(metadata)
109
+ """
110
+ # Load metadata if path provided, otherwise use dict directly
111
+ if isinstance(session_file, dict):
112
+ metadata = session_file
113
+ else:
114
+ metadata = load_session_metadata(session_file)
115
+
116
+ # Create NWBFile with all metadata
117
+ nwbfile = NWBFile(
118
+ session_description=metadata.get("session_description"),
119
+ identifier=metadata.get("identifier"),
120
+ session_start_time=utils.parse_datetime(metadata["session_start_time"]) if "session_start_time" in metadata else None,
121
+ timestamps_reference_time=utils.parse_datetime(metadata["timestamps_reference_time"]) if "timestamps_reference_time" in metadata else None,
122
+ experimenter=metadata.get("experimenter", None),
123
+ experiment_description=metadata.get("experiment_description", None),
124
+ session_id=metadata.get("session_id", None),
125
+ institution=metadata.get("institution", None),
126
+ keywords=metadata.get("keywords", None),
127
+ notes=metadata.get("notes", None),
128
+ pharmacology=metadata.get("pharmacology", None),
129
+ protocol=metadata.get("protocol", None),
130
+ related_publications=metadata.get("related_publications", None),
131
+ slices=metadata.get("slices", None),
132
+ source_script=utils.get_source_script(),
133
+ source_script_file_name=utils.get_source_script_file_name(),
134
+ was_generated_by=utils.get_software_packages(),
135
+ data_collection=metadata.get("data_collection", None),
136
+ surgery=metadata.get("surgery", None),
137
+ virus=metadata.get("virus", None),
138
+ stimulus_notes=metadata.get("stimulus_notes", None),
139
+ lab=metadata.get("lab", None),
140
+ )
141
+
142
+ # Add subject if provided
143
+ if "subject" in metadata:
144
+ nwbfile.subject = create_subject(metadata["subject"])
145
+
146
+ # Add devices if provided
147
+ if "devices" in metadata:
148
+ for device_info in metadata["devices"]:
149
+ nwbfile.add_device(create_device(device_info))
150
+
151
+ # Add electrode groups if provided
152
+ if "electrode_groups" in metadata:
153
+ for eg_info in metadata["electrode_groups"]:
154
+ pass
155
+ # TODO: implement create_electrode_group function
156
+
157
+ if "imaging_planes" in metadata:
158
+ for ip_info in metadata["imaging_planes"]:
159
+ pass
160
+ # TODO: implement create_imaging_plane function
161
+
162
+ if "ogen_sites" in metadata:
163
+ for os_info in metadata["ogen_sites"]:
164
+ pass
165
+ # TODO: implement create_ogen_site function
166
+
167
+ if "processing_modules" in metadata:
168
+ for pm_info in metadata["processing_modules"]:
169
+ pass
170
+ # TODO: implement create_processing_module function
171
+
172
+ return nwbfile
173
+
174
+
175
+ def create_subject(subject_data: Dict[str, Any]) -> Subject:
176
+ """Create pynwb.file.Subject object from metadata dictionary.
177
+
178
+ Constructs a Subject object with all standard NWB subject fields including
179
+ demographics, genotype information, and date of birth.
180
+
181
+ Parameters
182
+ ----------
183
+ subject_data : Dict[str, Any]
184
+ Dictionary containing subject metadata with optional fields:
185
+ - age: Age of subject (e.g., "P90D" for 90 days)
186
+ - age__reference: Reference point for age (default: "birth")
187
+ - description: Free-form text description
188
+ - genotype: Genetic strain designation
189
+ - sex: Sex of subject (e.g., "M", "F", "U")
190
+ - species: Species name (e.g., "Mus musculus")
191
+ - subject_id: Unique identifier for subject
192
+ - weight: Weight of subject
193
+ - date_of_birth: ISO 8601 datetime string
194
+ - strain: Genetic strain name
195
+
196
+ Returns
197
+ -------
198
+ Subject
199
+ Configured pynwb.file.Subject object
200
+
201
+ Example
202
+ -------
203
+ >>> subject_data = {
204
+ ... "subject_id": "M001",
205
+ ... "species": "Mus musculus",
206
+ ... "sex": "M",
207
+ ... "age": "P90D",
208
+ ... "date_of_birth": "2024-10-15T00:00:00"
209
+ ... }
210
+ >>> subject = create_subject(subject_data)
211
+ >>> print(subject.subject_id)
212
+ M001
213
+ """
214
+ return Subject(
215
+ age=subject_data.get("age", None),
216
+ age__reference=subject_data.get("age__reference", "birth"),
217
+ description=subject_data.get("description", None),
218
+ genotype=subject_data.get("genotype", None),
219
+ sex=subject_data.get("sex", None),
220
+ species=subject_data.get("species", None),
221
+ subject_id=subject_data.get("subject_id", None),
222
+ weight=subject_data.get("weight", None),
223
+ date_of_birth=utils.parse_datetime(subject_data["date_of_birth"]) if "date_of_birth" in subject_data else None,
224
+ strain=subject_data.get("strain", None),
225
+ )
226
+
227
+
228
+ def create_device(device_info: Dict[str, Any]) -> Device:
229
+ """Create pynwb.device.Device object from metadata dictionary.
230
+
231
+ Constructs a Device object representing physical hardware used in the experiment,
232
+ such as cameras, behavioral apparatus, or recording equipment.
233
+
234
+ Parameters
235
+ ----------
236
+ device_info : Dict[str, Any]
237
+ Dictionary containing device metadata:
238
+ - name: Unique device name (required)
239
+ - description: Text description of device (optional)
240
+ - serial_number: Serial number or identifier (optional)
241
+ - manufacturer: Manufacturer name (optional)
242
+ - model: Dictionary with model information (optional):
243
+ - model_name: Name of device model (required if model provided)
244
+ - manufacturer: Model manufacturer (optional)
245
+ - model_number: Model number/version (optional)
246
+ - description: Model description (optional)
247
+
248
+ Returns
249
+ -------
250
+ Device
251
+ Configured pynwb.device.Device object
252
+
253
+ Example
254
+ -------
255
+ >>> device_info = {
256
+ ... "name": "Camera_Top",
257
+ ... "description": "Top-view behavioral camera",
258
+ ... "manufacturer": "FLIR",
259
+ ... "serial_number": "FL-12345",
260
+ ... "model": {
261
+ ... "model_name": "Blackfly S BFS-U3-31S4M",
262
+ ... "model_number": "BFS-U3-31S4M-C"
263
+ ... }
264
+ ... }
265
+ >>> device = create_device(device_info)
266
+ >>> print(device.name)
267
+ Camera_Top
268
+ """
269
+ return Device(
270
+ name=device_info["name"],
271
+ description=device_info.get("description", None),
272
+ serial_number=device_info.get("serial_number", None),
273
+ manufacturer=device_info.get("manufacturer", None),
274
+ model=device_model(device_info["model"]) if "model" in device_info else None,
275
+ )
276
+
277
+
278
+ def device_model(device_info: Dict[str, Any]) -> DeviceModel:
279
+ """Create pynwb.device.DeviceModel object from metadata dictionary.
280
+
281
+ Constructs a DeviceModel object representing the specific model/version
282
+ of a hardware device used in the experiment.
283
+
284
+ Parameters
285
+ ----------
286
+ device_info : Dict[str, Any]
287
+ Dictionary containing device model metadata:
288
+ - model_name: Name of device model (required)
289
+ - manufacturer: Manufacturer name (optional)
290
+ - model_number: Model number or version identifier (optional)
291
+ - description: Text description of the model (optional)
292
+
293
+ Returns
294
+ -------
295
+ DeviceModel
296
+ Configured pynwb.device.DeviceModel object
297
+
298
+ Example
299
+ -------
300
+ >>> model_info = {
301
+ ... "model_name": "Blackfly S BFS-U3-31S4M",
302
+ ... "manufacturer": "FLIR",
303
+ ... "model_number": "BFS-U3-31S4M-C",
304
+ ... "description": "3.1 MP USB3 monochrome camera"
305
+ ... }
306
+ >>> model = device_model(model_info)
307
+ >>> print(model.name)
308
+ Blackfly S BFS-U3-31S4M
309
+ """
310
+ return DeviceModel(
311
+ name=device_info["model_name"],
312
+ manufacturer=device_info.get("manufacturer", None),
313
+ model_number=device_info.get("model_number", None),
314
+ description=device_info.get("description", None),
315
+ )
316
+
317
+
318
+ # =============================================================================
319
+ # NWB File Writing and Acquisition
320
+ # =============================================================================
321
+
322
+
323
+ def add_video_acquisition(
324
+ nwbfile: NWBFile,
325
+ camera_id: str,
326
+ video_files: List[str],
327
+ frame_rate: float = 30.0,
328
+ device: Optional[Device] = None,
329
+ ) -> NWBFile:
330
+ """Add video ImageSeries to NWBFile acquisition.
331
+
332
+ Creates an ImageSeries object with external video file links (videos are not
333
+ embedded in the NWB file) and adds it to the acquisition section. Uses
334
+ rate-based timing for efficiency.
335
+
336
+ Parameters
337
+ ----------
338
+ nwbfile : NWBFile
339
+ NWBFile object to add acquisition to
340
+ camera_id : str
341
+ Camera identifier (becomes ImageSeries name)
342
+ video_files : List[str]
343
+ List of absolute paths to video files
344
+ frame_rate : float, optional
345
+ Video frame rate in Hz (default: 30.0)
346
+ device : Device, optional
347
+ pynwb Device object representing the camera
348
+
349
+ Returns
350
+ -------
351
+ NWBFile
352
+ Updated NWBFile object (same as input, modified in place)
353
+
354
+ Example
355
+ -------
356
+ >>> from w2t_bkin.session import create_nwb_file, add_video_acquisition
357
+ >>> nwbfile = create_nwb_file("session.toml")
358
+ >>> nwbfile = add_video_acquisition(
359
+ ... nwbfile,
360
+ ... camera_id="camera_0",
361
+ ... video_files=["/path/to/video1.avi", "/path/to/video2.avi"],
362
+ ... frame_rate=30.0
363
+ ... )
364
+ >>> print(nwbfile.acquisition["camera_0"])
365
+ """
366
+
367
+ image_series = ImageSeries(
368
+ name=camera_id,
369
+ external_file=video_files,
370
+ format="external",
371
+ rate=frame_rate,
372
+ starting_time=0.0,
373
+ unit="n/a",
374
+ device=device,
375
+ )
376
+
377
+ nwbfile.add_acquisition(image_series)
378
+ return nwbfile
379
+
380
+
381
+ def write_nwb_file(nwbfile: NWBFile, output_path: Path) -> Path:
382
+ """Write NWBFile to disk using NWBHDF5IO.
383
+
384
+ Serializes an in-memory NWBFile object to an HDF5 file on disk.
385
+ Creates parent directories if needed.
386
+
387
+ Parameters
388
+ ----------
389
+ nwbfile : NWBFile
390
+ NWBFile object to write
391
+ output_path : Path
392
+ Output file path (should end with .nwb)
393
+
394
+ Returns
395
+ -------
396
+ Path
397
+ Path to written NWB file (same as output_path)
398
+
399
+ Raises
400
+ ------
401
+ IOError
402
+ If file cannot be written
403
+
404
+ Example
405
+ -------
406
+ >>> from w2t_bkin.session import create_nwb_file, write_nwb_file
407
+ >>> from pathlib import Path
408
+ >>>
409
+ >>> nwbfile = create_nwb_file("session.toml")
410
+ >>> output_path = Path("output/session.nwb")
411
+ >>> write_nwb_file(nwbfile, output_path)
412
+ >>> print(f"Written: {output_path}")
413
+ """
414
+
415
+ # Ensure parent directory exists
416
+ output_path = Path(output_path)
417
+ output_path.parent.mkdir(parents=True, exist_ok=True)
418
+
419
+ # Write NWBFile
420
+ with NWBHDF5IO(str(output_path), "w") as io:
421
+ io.write(nwbfile)
422
+
423
+ return output_path
@@ -0,0 +1,72 @@
1
+ """Temporal synchronization utilities.
2
+
3
+ Provides timebase providers, sample alignment, and stream synchronization
4
+ for video, pose, facemap, and behavioral data.
5
+
6
+ Note: TTL pulse loading has been moved to the ttl module.
7
+
8
+ Example:
9
+ >>> from w2t_bkin.sync import create_timebase_provider, align_samples
10
+ >>> provider = create_timebase_provider(source="nominal_rate", rate=30.0)
11
+ >>> timestamps = provider.get_timestamps(n_samples=100)
12
+ """
13
+
14
+ # Exceptions
15
+ from ..exceptions import JitterExceedsBudgetError, SyncError
16
+
17
+ # Core synchronization (formerly primitives, streams, protocols)
18
+ from .core import (
19
+ TimebaseConfigProtocol,
20
+ align_pose_frames_to_reference,
21
+ align_samples,
22
+ compute_jitter_stats,
23
+ enforce_jitter_budget,
24
+ fit_robust_linear_model,
25
+ map_linear,
26
+ map_nearest,
27
+ sync_stream_to_timebase,
28
+ )
29
+
30
+ # Alignment statistics (formerly models, stats)
31
+ from .stats import AlignmentStats, compute_alignment, create_alignment_stats, load_alignment_manifest, write_alignment_stats
32
+
33
+ # Timebase providers
34
+ from .timebase import NeuropixelsProvider, NominalRateProvider, TimebaseProvider, TTLProvider, create_timebase_provider, create_timebase_provider_from_config
35
+
36
+ # TTL synchronization
37
+ from .ttl import align_bpod_trials_to_ttl, get_sync_time_from_bpod_trial
38
+
39
+ __all__ = [
40
+ # Exceptions
41
+ "SyncError",
42
+ "JitterExceedsBudgetError",
43
+ # Models
44
+ "AlignmentStats",
45
+ # Timebase
46
+ "TimebaseProvider",
47
+ "NominalRateProvider",
48
+ "TTLProvider",
49
+ "NeuropixelsProvider",
50
+ "create_timebase_provider",
51
+ "create_timebase_provider_from_config",
52
+ # Mapping (Primitives)
53
+ "map_nearest",
54
+ "map_linear",
55
+ "compute_jitter_stats",
56
+ "enforce_jitter_budget",
57
+ "align_samples",
58
+ # TTL
59
+ "align_bpod_trials_to_ttl",
60
+ "get_sync_time_from_bpod_trial",
61
+ # Streams
62
+ "sync_stream_to_timebase",
63
+ "align_pose_frames_to_reference",
64
+ "fit_robust_linear_model",
65
+ # Stats
66
+ "create_alignment_stats",
67
+ "write_alignment_stats",
68
+ "load_alignment_manifest",
69
+ "compute_alignment",
70
+ # Protocols
71
+ "TimebaseConfigProtocol",
72
+ ]