diff --git a/src/aind_metadata_mapper/fip/README.md b/src/aind_metadata_mapper/fip/README.md index 46e4cec9..0e0f78a6 100644 --- a/src/aind_metadata_mapper/fip/README.md +++ b/src/aind_metadata_mapper/fip/README.md @@ -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_` 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: diff --git a/src/aind_metadata_mapper/fip/constants.py b/src/aind_metadata_mapper/fip/constants.py index a7f1d6de..55055a8f 100644 --- a/src/aind_metadata_mapper/fip/constants.py +++ b/src/aind_metadata_mapper/fip/constants.py @@ -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 diff --git a/src/aind_metadata_mapper/fip/mapper.py b/src/aind_metadata_mapper/fip/mapper.py index b5083943..35c0ffad 100644 --- a/src/aind_metadata_mapper/fip/mapper.py +++ b/src/aind_metadata_mapper/fip/mapper.py @@ -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, @@ -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 ------- @@ -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" @@ -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}" + 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 diff --git a/tests/fip/test_mapper.py b/tests/fip/test_mapper.py index 2e47701e..575719e3 100644 --- a/tests/fip/test_mapper.py +++ b/tests/fip/test_mapper.py @@ -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.