Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/aind_metadata_mapper/fip/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,9 @@ The mapper expects input JSON with this structure:
- `rig.camera_green_iso` → Green/Isosbestic detector
- `rig.camera_red` → Red detector
- `rig.cuttlefish_fip` → Controller device
- `rig.roi_settings` → Determines fiber count and creates patch cord configurations
- `rig.roi_settings` → Not currently read; may be null. Camera identifiers come from `rig.camera_*` and patch cords from the implanted fibers in the procedures record. The per-stream `data_stream_metadata[*].regions` recorded by aind-physiology-fip 0.3.5 and later are accepted but not read yet either.

The mapper assumes each ROI is drawn over its index-matched patch cord (ROI 0 over Patch Cord 0, and so on), that both cameras are in use, and that every implanted fiber has an active patch cord. The implanted fiber list comes from the subject's procedures record via the metadata service (see `_parse_implanted_fibers`), which describes the surgery rather than what was plugged in during the session. If the rig recorded which patch cords were in use and which fiber each was connected to, patch cord configurations and connections could be built from that record instead. That would be more accurate, and simpler: the procedures lookup (`_parse_implanted_fibers`, `get_procedures`), the `Fiber_<n>` name parsing (`_extract_fiber_index`), the implanted-fiber error handling in `transform`, and the index-matching assumption behind `_build_connections` and `_get_active_devices` would all go away, with connections read from the rig record rather than synthesized. The per-stream `regions` recorded by aind-physiology-fip 0.3.5 and later do not yet carry that mapping (see [Aind.Physiology.Fip #40](https://github.com/AllenNeuralDynamics/Aind.Physiology.Fip/issues/40)).

### Channels
Each fiber gets three channels:
Expand Down
2 changes: 0 additions & 2 deletions src/aind_metadata_mapper/fip/constants.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,6 @@
ROI_KEYWORD_GREEN = "green"
ROI_KEYWORD_ISO = "iso"
ROI_KEYWORD_RED = "red"
ROI_KEYWORD_ROI = "_roi"
ROI_KEYWORD_BACKGROUND = "_background"

# ==============================================================================
# Default Values
Expand Down
103 changes: 48 additions & 55 deletions src/aind_metadata_mapper/fip/mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,9 @@
EXCITATION_UV,
EXCITATION_YELLOW,
FIP_CAMERA_COMPRESSION,
ROI_KEYWORD_BACKGROUND,
ROI_KEYWORD_GREEN,
ROI_KEYWORD_ISO,
ROI_KEYWORD_RED,
ROI_KEYWORD_ROI,
)
from aind_metadata_mapper.utils import (
ensure_timezone,
Expand Down Expand Up @@ -495,16 +493,16 @@ def _extract_camera_exposure_time(self, rig_config: Dict) -> float:
)
return 10000.0 # Default exposure time in microseconds

def _get_camera_names_from_roi(self, roi_settings: Dict) -> Dict[str, str]:
"""Get camera device identifiers from ROI settings.
def _get_camera_names(self, rig_config: Dict) -> Dict[str, str]:
"""Get camera device identifiers from the rig configuration.

Extracts camera config keys from ROI settings and transforms them to
Classifies the rig's camera entries as green or red and transforms them to
historical standard device names using DEVICE_NAME_MAP.

Parameters
----------
roi_settings : Dict
ROI settings from rig configuration.
rig_config : Dict
Rig configuration dictionary from metadata.

Returns
-------
Expand All @@ -514,11 +512,8 @@ def _get_camera_names_from_roi(self, roi_settings: Dict) -> Dict[str, str]:
"""
camera_names = {}

for roi_key in roi_settings.keys():
if ROI_KEYWORD_ROI in roi_key and ROI_KEYWORD_BACKGROUND not in roi_key:
# Extract camera key from roi_key (e.g., "camera_green_iso_roi" -> "camera_green_iso")
camera_key = roi_key.replace(ROI_KEYWORD_ROI, "")

for camera_key in rig_config.keys():
if camera_key.startswith("camera_"):
# Determine camera type
if ROI_KEYWORD_GREEN in camera_key or ROI_KEYWORD_ISO in camera_key:
camera_type = "green"
Expand Down Expand Up @@ -664,51 +659,49 @@ def _build_configurations(
# 1. Green: 470nm excitation, ~520nm emission, green camera
# 2. Isosbestic: 415nm excitation, ~520nm emission, green camera (same detector!)
# 3. Red: 565nm excitation, ~590nm emission, red camera
roi_settings = rig_config.get("roi_settings", {})
if roi_settings:
# Get camera identifiers from ROI settings
camera_names = self._get_camera_names_from_roi(roi_settings)
green_camera_name = camera_names.get("green")
red_camera_name = camera_names.get("red")

# Use implanted fibers directly
fiber_indices = implanted_fibers

# Create patch cord for each implanted fiber
for fiber_idx in fiber_indices:
channels = []
fiber_name = f"Fiber_{fiber_idx}"
fiber_measurements = intended_measurements.get(fiber_name) if intended_measurements else None

# Channel definitions: (channel_type, measurement_key, led_wavelength, camera_name, emission_wavelength)
channel_defs = [
("Green", "G", EXCITATION_BLUE, green_camera_name, EMISSION_GREEN),
("Isosbestic", "Iso", EXCITATION_UV, green_camera_name, EMISSION_GREEN),
("Red", "R", EXCITATION_YELLOW, red_camera_name, EMISSION_RED),
]

for channel_type, measurement_key, led_wavelength, camera_name, emission_wavelength in channel_defs:
if camera_name:
measurement = fiber_measurements.get(measurement_key) if fiber_measurements else None
channels.append(
self._create_channel(
fiber_idx,
channel_type,
led_configs_by_wavelength.get(led_wavelength),
measurement,
camera_name,
emission_wavelength,
exposure_time_ms,
)
)
# Get camera identifiers from the rig configuration
camera_names = self._get_camera_names(rig_config)
green_camera_name = camera_names.get("green")
red_camera_name = camera_names.get("red")

# Use implanted fibers directly
fiber_indices = implanted_fibers

# Create patch cord if we have channels
if channels:
patch_cord = PatchCordConfig(
device_name=f"Patch Cord {fiber_idx}",
channels=channels,
# Create patch cord for each implanted fiber
for fiber_idx in fiber_indices:
channels = []
fiber_name = f"Fiber_{fiber_idx}"
Comment thread
dougollerenshaw marked this conversation as resolved.
Resolved
fiber_measurements = intended_measurements.get(fiber_name) if intended_measurements else None

# Channel definitions: (channel_type, measurement_key, led_wavelength, camera_name, emission_wavelength)
channel_defs = [
("Green", "G", EXCITATION_BLUE, green_camera_name, EMISSION_GREEN),
("Isosbestic", "Iso", EXCITATION_UV, green_camera_name, EMISSION_GREEN),
("Red", "R", EXCITATION_YELLOW, red_camera_name, EMISSION_RED),
]

for channel_type, measurement_key, led_wavelength, camera_name, emission_wavelength in channel_defs:
if camera_name:
measurement = fiber_measurements.get(measurement_key) if fiber_measurements else None
channels.append(
self._create_channel(
fiber_idx,
channel_type,
led_configs_by_wavelength.get(led_wavelength),
measurement,
camera_name,
emission_wavelength,
exposure_time_ms,
)
)
configurations.append(patch_cord)

# Create patch cord if we have channels
if channels:
patch_cord = PatchCordConfig(
device_name=f"Patch Cord {fiber_idx}",
channels=channels,
)
configurations.append(patch_cord)

return configurations

Expand Down
34 changes: 34 additions & 0 deletions tests/fip/test_mapper.py
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,40 @@ def test_configurations_built(self):
self.assertGreater(len(patch_cords[0].channels), 0)
self.assertIsNotNone(patch_cords[0].channels[0].detector)

def test_roi_settings_not_used(self):
"""Test that ROI settings do not affect the acquisition.

Camera identifiers come from the rig configuration and patch cords from the implanted
fibers, so rig.roi_settings may be null or absent (#498), and the per-stream regions
recorded by aind-physiology-fip 0.3.5 and later are accepted but not yet read. All
variants are run with schema validation on.
"""
data = self.example_intermediate_data
rig, stream = data["rig"], data["data_stream_metadata"][0]
variants = {
"baseline": data,
"rig.roi_settings null": {**data, "rig": {**rig, "roi_settings": None}},
"rig.roi_settings absent": {**data, "rig": {k: v for k, v in rig.items() if k != "roi_settings"}},
"stream regions present": {**data, "data_stream_metadata": [{**stream, "regions": rig["roi_settings"]}]},
}
acquisitions = {}
for label, variant in variants.items():
with self.subTest(variant=label):
acquisitions[label] = self.mapper.transform(
variant,
skip_validation=False,
intended_measurements=self.test_intended_measurements,
implanted_fibers=self.test_implanted_fibers,
ethics_review_id=self.test_ethics_review_id,
)
for label, acquisition in acquisitions.items():
self.assertEqual(acquisition, acquisitions["baseline"], label)

def test_get_camera_names_ignores_unclassified_cameras(self):
"""Test that camera entries that are neither green nor red are skipped."""
names = self.mapper._get_camera_names({"camera_green_iso": {}, "camera_red": {}, "camera_other": {}})
self.assertEqual(names, {"green": "Green CMOS", "red": "Red CMOS"})

def test_timezone_handling(self):
"""Test that session times are properly timezone-aware after processing.

Expand Down
Loading