Skip to content
43 changes: 38 additions & 5 deletions behaviors/openfield.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 "<none>"
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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -423,7 +456,7 @@ class ConfigurationArena(dj.Manual):
definition = """
# Camera information
arena_idx : tinyint
-> behavior.Configuration
-> interface.Configuration
---
size : int
discription : varchar(256)
Expand Down
7 changes: 2 additions & 5 deletions behaviors/vr_ball.py
Original file line number Diff line number Diff line change
@@ -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



Expand Down
4 changes: 2 additions & 2 deletions experiments/approach.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -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):
Expand Down
9 changes: 4 additions & 5 deletions stimuli/olfactory.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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),
Expand Down
24 changes: 21 additions & 3 deletions stimuli/openfield_panda.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down
2 changes: 1 addition & 1 deletion stimuli/psycho_grating.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion stimuli/vr_odors.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 2 additions & 3 deletions tasks/openfield_task.py
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -37,7 +36,7 @@


exp = Experiment()
exp.setup(logger, OpenField, session_params)

Check failure on line 39 in tasks/openfield_task.py

View workflow job for this annotation

GitHub Actions / Lint (ruff)

ruff (F821)

tasks/openfield_task.py:39:11: F821 Undefined name `logger`
conditions = []
rot_f = lambda: interp((np.random.rand(30) - 0.5) * 10)

Expand Down
Loading