Skip to content

Commit 235ae7f

Browse files
fix: if multiple generators, use coupled trial type generator
1 parent f265875 commit 235ae7f

2 files changed

Lines changed: 78 additions & 2 deletions

File tree

src/dynamic_foraging_processing/processing/_trial_table.py

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
import numpy as np
1212
import pandas as pd
13+
from aind_behavior_dynamic_foraging.task_logic import AindDynamicForagingTaskLogic
14+
from aind_behavior_dynamic_foraging.task_logic.trial_generators import TrialGeneratorSpec
1315
from aind_behavior_dynamic_foraging.task_logic.trial_models import Trial, TrialOutcome
1416
from aind_behavior_services.task.distributions import Distribution, DistributionFamily
1517
from contraqctor.contract import Dataset
@@ -274,7 +276,42 @@ def _auto_water(trial: t.Optional[Trial], *, side: bool) -> t.Optional[int]:
274276
# ------------------------------------------------------------------ #
275277
# Session-level (constant across trials) columns
276278
# ------------------------------------------------------------------ #
277-
def _session_columns(self, task_logic: t.Optional[t.Any]) -> t.Dict[str, t.Any]:
279+
@staticmethod
280+
def _coupled_generator(
281+
generator: TrialGeneratorSpec,
282+
) -> t.Optional[TrialGeneratorSpec]:
283+
"""Resolve the ``CoupledTrialGenerator`` carrying the session parameters.
284+
285+
``task_parameters.trial_generator`` may be a single generator spec or a
286+
``TrialGeneratorCompositeSpec`` wrapping several sub-generators in
287+
``.generators``. The block-length, ITI, delay, and reward-probability
288+
summaries come from the coupled generator; this unwraps the composite
289+
and returns it, or returns ``generator`` unchanged when it is not a
290+
composite.
291+
292+
Parameters
293+
----------
294+
generator : TrialGeneratorSpec
295+
The ``trial_generator`` from the task parameters.
296+
297+
Returns
298+
-------
299+
TrialGeneratorSpec or None
300+
The coupled generator, or ``None`` if a composite contains none.
301+
"""
302+
sub_generators = getattr(generator, "generators", None)
303+
if sub_generators is None:
304+
return generator
305+
for sub in sub_generators:
306+
if getattr(sub, "type", None) == "CoupledTrialGenerator":
307+
return sub
308+
logger.warning(
309+
"No CoupledTrialGenerator among composite trial generators; "
310+
"session columns will be null."
311+
)
312+
return None
313+
314+
def _session_columns(self, task_logic: AindDynamicForagingTaskLogic) -> t.Dict[str, t.Any]:
278315
"""Return the per-session trial columns derived from the task logic.
279316
280317
These (block/ITI/delay distribution summaries and reward structure) are
@@ -284,7 +321,9 @@ def _session_columns(self, task_logic: t.Optional[t.Any]) -> t.Dict[str, t.Any]:
284321
if task_logic is None:
285322
return columns
286323

287-
generator = task_logic.task_parameters.trial_generator
324+
generator = self._coupled_generator(task_logic.task_parameters.trial_generator)
325+
if generator is None:
326+
return columns
288327

289328
block_beta, block_min, block_max = self._distribution_stats(generator.block_length)
290329
iti_beta, iti_min, iti_max = self._distribution_stats(

tests/test_processing/test_trial_table.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
)
1010
from aind_behavior_dynamic_foraging.task_logic.trial_generators import (
1111
CoupledTrialGeneratorSpec,
12+
TrialGeneratorCompositeSpec,
13+
UncoupledTrialGeneratorSpec,
1214
)
1315
from aind_behavior_dynamic_foraging.task_logic.trial_models import TrialOutcome
1416
from aind_behavior_services.task.distributions import (
@@ -224,6 +226,41 @@ def test_build_exponential_quiescent_sets_delay_beta():
224226
assert table.iloc[0]["delay_max"] == 1.0
225227

226228

229+
# --------------------------------------------------------------------------- #
230+
# _coupled_generator — composite trial generators
231+
# --------------------------------------------------------------------------- #
232+
def test_coupled_generator_returns_single_generator_unchanged():
233+
"""A non-composite generator (no ``.generators``) is returned as-is."""
234+
spec = _task_logic().task_parameters.trial_generator
235+
assert TrialTableBuilder._coupled_generator(spec) is spec
236+
237+
238+
def test_coupled_generator_unwraps_composite():
239+
"""The coupled sub-generator is selected from a composite."""
240+
coupled = _task_logic().task_parameters.trial_generator
241+
composite = TrialGeneratorCompositeSpec(generators=[UncoupledTrialGeneratorSpec(), coupled])
242+
resolved = TrialTableBuilder._coupled_generator(composite)
243+
assert resolved.type == "CoupledTrialGenerator"
244+
assert resolved.min_block_reward == coupled.min_block_reward
245+
246+
247+
def test_coupled_generator_none_when_composite_has_no_coupled(caplog):
248+
"""A composite without a coupled generator yields ``None`` and warns."""
249+
composite = TrialGeneratorCompositeSpec(generators=[UncoupledTrialGeneratorSpec()])
250+
assert TrialTableBuilder._coupled_generator(composite) is None
251+
assert "No CoupledTrialGenerator" in caplog.text
252+
253+
254+
def test_session_columns_empty_when_no_coupled_generator():
255+
"""A composite task logic with no coupled generator yields no session columns."""
256+
task_logic = AindDynamicForagingTaskLogic(
257+
task_parameters=AindDynamicForagingTaskParameters(
258+
trial_generator=TrialGeneratorCompositeSpec(generators=[UncoupledTrialGeneratorSpec()])
259+
)
260+
)
261+
assert TrialTableBuilder(_Node({}))._session_columns(task_logic) == {}
262+
263+
227264
# --------------------------------------------------------------------------- #
228265
# _load — error handling
229266
# --------------------------------------------------------------------------- #

0 commit comments

Comments
 (0)