diff --git a/behaviors/openfield.py b/behaviors/openfield.py index b1d9dac..f86c850 100644 --- a/behaviors/openfield.py +++ b/behaviors/openfield.py @@ -138,17 +138,49 @@ def setup(self, exp) -> None: self._initialize_dlc() + def _get_openfield_camera(self): + """Return the openfield camera (the one DLC consumes frames from). + + DLC's corner detector and pose estimator both pull from a single + camera — the one with ``video_aim='openfield'``. Other cameras + configured for this setup (e.g. a passive ``'eye'`` camera) just + record to disk and must not be confused with the openfield camera. + + Cameras are keyed as ``f"{video_aim}_{camera_idx}"`` in + ``Interface.cameras`` (stable per-physical-camera identity across + sessions), so we filter by the ``openfield_`` prefix. + + Raises: + ValueError: if no camera with ``video_aim='openfield'`` is + configured, or if more than one is (DLC requires exactly one). + """ + cameras = self.interface.cameras + matches = {k: c for k, c in cameras.items() if k.startswith("openfield_")} + if not matches: + configured = list(cameras.keys()) or "" + raise ValueError( + "No camera with video_aim='openfield' is configured for this " + f"setup. Configured keys: {configured}. Add a row to " + "SetupConfiguration.Camera with video_aim='openfield' for the " + "camera DLC should track." + ) + if len(matches) > 1: + raise ValueError( + f"Multiple openfield cameras configured: {list(matches.keys())}. " + "DLC requires exactly one camera with video_aim='openfield'." + ) + return next(iter(matches.values())) + def _initialize_dlc(self) -> None: """Initialize the DeepLabCut (DLC) object for pose estimation.""" - if self.interface.camera is None: - raise ValueError("Camera is not initialized") + openfield_cam = self._get_openfield_camera() corners, affine_matrix = self.get_corners() dlc_body_path = self.logger.get(schema='interface', table='SetupConfigurationArena.Models', fields=['path'], key={'setup_conf_idx': self.exp.session_params['setup_conf_idx'], 'target': 'bodyparts'})[0] - self.dlc = DLCContinuousPoseEstimator(frame_queue=self.interface.camera.process_queue, + self.dlc = DLCContinuousPoseEstimator(frame_queue=openfield_cam.process_queue, model_path=dlc_body_path, logger=self.logger, shared_memory_conf=self.shm_conf, @@ -362,7 +394,8 @@ def get_corners(self): fields=['path'], key={'setup_conf_idx': self.exp.session_params['setup_conf_idx'], 'target': 'corners'})[0] - dlcCorners = DLCCornerDetector(frame_queue=self.interface.camera.process_queue, + openfield_cam = self._get_openfield_camera() + dlcCorners = DLCCornerDetector(frame_queue=openfield_cam.process_queue, model_path=dlc_corners_path, arena_size=self.arena_size, result=corners_dict, @@ -423,7 +456,7 @@ class ConfigurationArena(dj.Manual): definition = """ # Camera information arena_idx : tinyint - -> behavior.Configuration + -> interface.Configuration --- size : int discription : varchar(256) diff --git a/behaviors/vr_ball.py b/behaviors/vr_ball.py index 98f6b77..abbe297 100644 --- a/behaviors/vr_ball.py +++ b/behaviors/vr_ball.py @@ -1,12 +1,9 @@ -from core.Behavior import * - - import datajoint as dj import numpy as np -from ethopy.core.behavior import Behavior +from ethopy.core.behavior import Behavior, BehCondition from ethopy.core.logger import behavior -from ethopy.interfaces.ball import Ball +from ethopy.interfaces.Ball import Ball diff --git a/experiments/approach.py b/experiments/approach.py index 179e9cc..5ca04fc 100644 --- a/experiments/approach.py +++ b/experiments/approach.py @@ -176,7 +176,7 @@ def entry(self): self.beh.punish() super().entry() self.punish_period = self.curr_cond["punish_duration"] - if self.params.get("incremental_punishment"): + if self.curr_cond["incremental_punishment"]: self.punish_period *= self.beh.get_false_history() def run(self): @@ -196,7 +196,7 @@ def exit(self): class InterTrial(Experiment): def run(self): - if self.beh.is_licking() and self.params.get("noresponse_intertrial"): + if self.beh.is_licking() and self.curr_cond["noresponse_intertrial"]: self.state_timer.start() def next(self): diff --git a/stimuli/olfactory.py b/stimuli/olfactory.py index 687411a..ca8bd5c 100644 --- a/stimuli/olfactory.py +++ b/stimuli/olfactory.py @@ -1,7 +1,7 @@ import datajoint as dj from ethopy.core.logger import stimulus -from ethopy.core.stimulus import Stimulus +from ethopy.core.stimulus import Stimulus, StimCondition @stimulus.schema @@ -36,12 +36,11 @@ class Channel(dj.Part): dutycycle : int # odor dutycycle """ - cond_tables = ['Olfactory', 'Olfactory.Channel'] - required_fields = ['odor_duration', 'odorant_id', 'delivery_port'] - default_key = {'dutycycle': 50} - def __init__(self): super().__init__() + self.cond_tables = ['Olfactory', 'Olfactory.Channel'] + self.required_fields = ['odor_duration', 'odorant_id', 'delivery_port'] + self.default_key = {'dutycycle': 50} self.fill_colors.set({'background': (0, 0, 0), 'start': (0.2, 0.2, 0.2), 'ready': (0.3, 0.3, 0.3), diff --git a/stimuli/openfield_panda.py b/stimuli/openfield_panda.py index 4cb7e7d..8d2265f 100644 --- a/stimuli/openfield_panda.py +++ b/stimuli/openfield_panda.py @@ -361,10 +361,28 @@ def create_movies(self): os.remove(f) def get_cond(self, cond_name, idx=0): + """Extract the cond_name prefix conditions. + + ``self.curr_cond`` is a flat dict holding the whole trial (lights, + background, and every object field prefixed with ``cond_name``, e.g. + ``"obj_"``). This returns just the fields for object ``idx``, with the + prefix stripped, so an ``Agent`` can read ``cond["mag"]`` instead of + ``cond["obj_mag"]``. + + For each matching field: + - a scalar value is shared by every object (returned as-is); + - a sequence (tuple/list/array) is treated as one value per object, + so object ``idx`` gets ``v[idx]``. + + Args: + cond_name: Field prefix to select and strip (e.g. ``"obj_"``). + idx: Index of the object whose values to pick from sequence fields. + + Returns: + A dict of de-prefixed field names mapped to this object's value. + """ return { - k.split(cond_name, 1)[1]: v - if type(v) is int or type(v) is float - else v[idx] + k.split(cond_name, 1)[1]: (v if np.ndim(v) == 0 else v[idx]) for k, v in self.curr_cond.items() if k.startswith(cond_name) } diff --git a/stimuli/psycho_grating.py b/stimuli/psycho_grating.py index 2ae9697..9c50675 100644 --- a/stimuli/psycho_grating.py +++ b/stimuli/psycho_grating.py @@ -2,7 +2,7 @@ import datajoint as dj from ethopy.core.logger import stimulus -from ethopy.core.stimulus import Stimulus +from ethopy.core.stimulus import Stimulus, StimCondition from ethopy.stimuli.psycho_presenter import Presenter from ethopy.utils.helper_functions import iterable diff --git a/stimuli/vr_odors.py b/stimuli/vr_odors.py index c5de9a4..0989aa3 100644 --- a/stimuli/vr_odors.py +++ b/stimuli/vr_odors.py @@ -3,7 +3,7 @@ from ethopy.stimuli.olfactory import Odorants, Olfactory from ethopy.core.logger import stimulus -from ethopy.core.stimulus import Stimulus +from ethopy.core.stimulus import Stimulus, StimCondition @stimulus.schema diff --git a/tasks/openfield_task.py b/tasks/openfield_task.py index 009f3f6..223f8e7 100644 --- a/tasks/openfield_task.py +++ b/tasks/openfield_task.py @@ -1,10 +1,9 @@ import numpy as np from scipy import interpolate -from ethopy.core import logger -from ethopy.behaviors.openField import OpenField +from ethopy.behaviors.openfield import OpenField from ethopy.experiments.approach import Experiment -from ethopy.stimuli.panda import Panda +from ethopy.stimuli.openfield_panda import Panda def interp(x):