From 0ab286d2025dfa08148b2d0f0a2df634fe8ef593 Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 2 Jul 2026 14:07:20 -0700 Subject: [PATCH 1/4] refactors auto response to auto reward --- schema/aind_behavior_dynamic_foraging.json | 6 +- .../AindBehaviorDynamicForaging.Generated.cs | 20 ++-- .../ModifyTrialWithAutoWaterState.cs | 4 +- src/Extensions/TaskEngine.bonsai | 105 +++++++----------- src/Extensions/TrialOutcomeVisualizer.cs | 2 +- .../interventions/bias_intervention.py | 2 +- .../block_based_trial_generator.py | 18 +-- .../integration_test_trial_generator.py | 16 +-- .../task_logic/trial_models.py | 4 +- src/test_visualizer.bonsai | 4 +- .../test_block_based_trial_generator.py | 8 +- .../metrics.py | 2 +- .../tests/test_metrics.py | 4 +- 13 files changed, 84 insertions(+), 111 deletions(-) diff --git a/schema/aind_behavior_dynamic_foraging.json b/schema/aind_behavior_dynamic_foraging.json index b887ffa7..07b258fd 100644 --- a/schema/aind_behavior_dynamic_foraging.json +++ b/schema/aind_behavior_dynamic_foraging.json @@ -3454,9 +3454,9 @@ "title": "Inter Trial Interval Duration", "type": "number" }, - "is_auto_response_right": { + "is_auto_reward_right": { "default": null, - "description": "If set, the trial will automatically (and immediately) register a response to the right (True) or left (False).", + "description": "If set, the trial will automatically (and immediately) trigger a reward to the right (True) or left (False). The trial will continue normally after delivery.", "oneOf": [ { "type": "boolean" @@ -3465,7 +3465,7 @@ "type": "null" } ], - "title": "Is Auto Response Right" + "title": "Is Auto Reward Right" }, "lickspout_offset_delta": { "default": 0.0, diff --git a/src/Extensions/AindBehaviorDynamicForaging.Generated.cs b/src/Extensions/AindBehaviorDynamicForaging.Generated.cs index 52a7c6c6..cebc5012 100644 --- a/src/Extensions/AindBehaviorDynamicForaging.Generated.cs +++ b/src/Extensions/AindBehaviorDynamicForaging.Generated.cs @@ -6091,7 +6091,7 @@ public partial class Trial private double _interTrialIntervalDuration; - private bool? _isAutoResponseRight; + private bool? _isAutoRewardRight; private double _lickspoutOffsetDelta; @@ -6120,7 +6120,7 @@ protected Trial(Trial other) _quickRetractSettings = other._quickRetractSettings; _quiescencePeriodDuration = other._quiescencePeriodDuration; _interTrialIntervalDuration = other._interTrialIntervalDuration; - _isAutoResponseRight = other._isAutoResponseRight; + _isAutoRewardRight = other._isAutoRewardRight; _lickspoutOffsetDelta = other._lickspoutOffsetDelta; _metadata = other._metadata; } @@ -6283,20 +6283,20 @@ public double InterTrialIntervalDuration } /// - /// If set, the trial will automatically (and immediately) register a response to the right (True) or left (False). + /// If set, the trial will automatically (and immediately) trigger a reward to the right (True) or left (False). The trial will continue normally after delivery. /// - [Newtonsoft.Json.JsonPropertyAttribute("is_auto_response_right")] - [System.ComponentModel.DescriptionAttribute("If set, the trial will automatically (and immediately) register a response to the" + - " right (True) or left (False).")] - public bool? IsAutoResponseRight + [Newtonsoft.Json.JsonPropertyAttribute("is_auto_reward_right")] + [System.ComponentModel.DescriptionAttribute("If set, the trial will automatically (and immediately) trigger a reward to the ri" + + "ght (True) or left (False). The trial will continue normally after delivery.")] + public bool? IsAutoRewardRight { get { - return _isAutoResponseRight; + return _isAutoRewardRight; } set { - _isAutoResponseRight = value; + _isAutoRewardRight = value; } } @@ -6357,7 +6357,7 @@ protected virtual bool PrintMembers(System.Text.StringBuilder stringBuilder) stringBuilder.Append("QuickRetractSettings = " + _quickRetractSettings + ", "); stringBuilder.Append("QuiescencePeriodDuration = " + _quiescencePeriodDuration + ", "); stringBuilder.Append("InterTrialIntervalDuration = " + _interTrialIntervalDuration + ", "); - stringBuilder.Append("IsAutoResponseRight = " + _isAutoResponseRight + ", "); + stringBuilder.Append("IsAutoRewardRight = " + _isAutoRewardRight + ", "); stringBuilder.Append("LickspoutOffsetDelta = " + _lickspoutOffsetDelta + ", "); stringBuilder.Append("Metadata = " + _metadata); return true; diff --git a/src/Extensions/ModifyTrialWithAutoWaterState.cs b/src/Extensions/ModifyTrialWithAutoWaterState.cs index 66f22a6b..29e0eb10 100644 --- a/src/Extensions/ModifyTrialWithAutoWaterState.cs +++ b/src/Extensions/ModifyTrialWithAutoWaterState.cs @@ -24,13 +24,13 @@ public IObservable Process(IObservable> sour if (autoWaterState.IsRight) { newTrial.PRewardRight = 1.0f; - newTrial.IsAutoResponseRight = true; + newTrial.IsAutoRewardRight = true; return newTrial; } if (autoWaterState.IsLeft) { newTrial.PRewardLeft = 1.0f; - newTrial.IsAutoResponseRight = false; + newTrial.IsAutoRewardRight = false; return newTrial; } throw new InvalidOperationException("AutoWaterState must be either right or left."); diff --git a/src/Extensions/TaskEngine.bonsai b/src/Extensions/TaskEngine.bonsai index 567569bb..ddd4736a 100644 --- a/src/Extensions/TaskEngine.bonsai +++ b/src/Extensions/TaskEngine.bonsai @@ -384,50 +384,6 @@ ResponsePeriod - - ThisTrial - - - IsAutoResponseRight - - - - - - Source1 - - - HasValue - - - - - - - - - - - - - - - - - - - - - HarpTimestampSource - - - - ThreadPoolScheduler - - - - - ThisTrial @@ -548,9 +504,31 @@ SoftwareEvent - - TODO - + + ThisTrial + + + IsAutoRewardRight + + + + + + Source1 + + + HasValue + + + + + + + + + + + GiveRewardRight @@ -558,33 +536,28 @@ - - - - - + + + + + + - - - + + + - - + + - - - - - + + - + - - - diff --git a/src/Extensions/TrialOutcomeVisualizer.cs b/src/Extensions/TrialOutcomeVisualizer.cs index 8247f312..fef88151 100644 --- a/src/Extensions/TrialOutcomeVisualizer.cs +++ b/src/Extensions/TrialOutcomeVisualizer.cs @@ -430,7 +430,7 @@ private static void CategorizeTrials( { var trial = trialList[i]; float x = i; - bool isAutoResponse = trial.Trial.IsAutoResponseRight.HasValue; + bool isAutoResponse = trial.Trial.IsAutoRewardRight.HasValue; if (!trial.IsRightChoice.HasValue) { diff --git a/src/aind_behavior_dynamic_foraging/task_logic/interventions/bias_intervention.py b/src/aind_behavior_dynamic_foraging/task_logic/interventions/bias_intervention.py index 795d56c1..35670182 100644 --- a/src/aind_behavior_dynamic_foraging/task_logic/interventions/bias_intervention.py +++ b/src/aind_behavior_dynamic_foraging/task_logic/interventions/bias_intervention.py @@ -100,7 +100,7 @@ def determine_antibias_intervention(self, bias: float) -> tuple[Optional[bool], ``parameters.lickspout_offset_delta`` mm. Returns: - Tuple dictating is_auto_response_right and lickspout_offset_delta of trial + Tuple dictating is_auto_reward_right and lickspout_offset_delta of trial """ if self.parameters is None: diff --git a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py index 0f4267a6..273ac14f 100644 --- a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py +++ b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/block_based_trial_generator.py @@ -194,33 +194,33 @@ def next(self) -> Trial | None: self.is_right_baited = self.block.p_right_reward > random_numbers[1] or self.is_right_baited logger.debug("Right baited: %s" % self.is_right_baited) - is_auto_response_right = None + is_auto_reward_right = None # determine autowater if is_autowater := self._are_autowater_conditions_met(): - is_auto_response_right = True if self.block.p_right_reward > self.block.p_left_reward else False - logger.debug("Delivering autowater: is_auto_response_right = %s" % is_auto_response_right) + is_auto_reward_right = True if self.block.p_right_reward > self.block.p_left_reward else False + logger.debug("Delivering autowater: is_auto_reward_right = %s" % is_auto_reward_right) # determine bias correction. Overrides autowater lickspout_offset_delta = 0 if self.bias_intervention.are_antibias_conditions_met(self.bias): - is_auto_response_right, lickspout_offset_delta = self.bias_intervention.determine_antibias_intervention( + is_auto_reward_right, lickspout_offset_delta = self.bias_intervention.determine_antibias_intervention( self.bias ) logger.debug( - "Performing bias intervention: is_auto_response_right = %s, lickspout_offset_delta = %s." - % (is_auto_response_right, lickspout_offset_delta) + "Performing bias intervention: is_auto_reward_right = %s, lickspout_offset_delta = %s." + % (is_auto_reward_right, lickspout_offset_delta) ) return Trial( - p_reward_left=1 if (self.is_left_baited or is_auto_response_right is False) else self.block.p_left_reward, - p_reward_right=1 if (self.is_right_baited or is_auto_response_right) else self.block.p_right_reward, + p_reward_left=1 if (self.is_left_baited or is_auto_reward_right is False) else self.block.p_left_reward, + p_reward_right=1 if (self.is_right_baited or is_auto_reward_right) else self.block.p_right_reward, reward_consumption_duration=self.spec.reward_consumption_duration, response_deadline_duration=self.spec.response_duration, quiescence_period_duration=quiescent, inter_trial_interval_duration=iti, lickspout_offset_delta=lickspout_offset_delta, - is_auto_response_right=is_auto_response_right, + is_auto_reward_right=is_auto_reward_right, metadata=Metadata( p_reward_left=self.block.p_left_reward, p_reward_right=self.block.p_right_reward, diff --git a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/integration_test_trial_generator.py b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/integration_test_trial_generator.py index 72b53fed..7023604a 100644 --- a/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/integration_test_trial_generator.py +++ b/src/aind_behavior_dynamic_foraging/task_logic/trial_generators/integration_test_trial_generator.py @@ -26,29 +26,29 @@ def __init__(self, spec: IntegrationTestTrialGeneratorSpec) -> None: Trial(p_reward_left=1.0, p_reward_right=1.0), # 4: both reward # auto response right Trial( - p_reward_left=1.0, p_reward_right=0.0, is_auto_response_right=True + p_reward_left=1.0, p_reward_right=0.0, is_auto_reward_right=True ), # 5: left reward, auto response right Trial( - p_reward_left=0.0, p_reward_right=1.0, is_auto_response_right=True + p_reward_left=0.0, p_reward_right=1.0, is_auto_reward_right=True ), # 6: right reward, auto response right Trial( - p_reward_left=1.0, p_reward_right=1.0, is_auto_response_right=True + p_reward_left=1.0, p_reward_right=1.0, is_auto_reward_right=True ), # 7: both reward, auto response right, Trial( - p_reward_left=0.0, p_reward_right=0.0, is_auto_response_right=True + p_reward_left=0.0, p_reward_right=0.0, is_auto_reward_right=True ), # 8: no reward, auto response right, # auto response left Trial( - p_reward_left=1.0, p_reward_right=0.0, is_auto_response_right=False + p_reward_left=1.0, p_reward_right=0.0, is_auto_reward_right=False ), # 9: left reward, auto response left, Trial( - p_reward_left=0.0, p_reward_right=1.0, is_auto_response_right=False + p_reward_left=0.0, p_reward_right=1.0, is_auto_reward_right=False ), # 10: right reward, auto response left, Trial( - p_reward_left=0.0, p_reward_right=1.0, is_auto_response_right=False + p_reward_left=0.0, p_reward_right=1.0, is_auto_reward_right=False ), # 11: both reward, auto response left, Trial( - p_reward_left=0.0, p_reward_right=0.0, is_auto_response_right=False + p_reward_left=0.0, p_reward_right=0.0, is_auto_reward_right=False ), # 12: no reward, auto response left, # fast retract Trial( diff --git a/src/aind_behavior_dynamic_foraging/task_logic/trial_models.py b/src/aind_behavior_dynamic_foraging/task_logic/trial_models.py index e4390d43..f1b93237 100644 --- a/src/aind_behavior_dynamic_foraging/task_logic/trial_models.py +++ b/src/aind_behavior_dynamic_foraging/task_logic/trial_models.py @@ -97,9 +97,9 @@ class Trial(BaseModel): inter_trial_interval_duration: float = Field( default=5.0, ge=0.5, description="Duration of the inter-trial interval (in seconds)." ) - is_auto_response_right: Optional[bool] = Field( + is_auto_reward_right: Optional[bool] = Field( default=None, - description="If set, the trial will automatically (and immediately) register a response to the right (True) or left (False).", + description="If set, the trial will automatically (and immediately) trigger a reward to the right (True) or left (False). The trial will continue normally after delivery.", ) lickspout_offset_delta: float = Field( default=0.0, diff --git a/src/test_visualizer.bonsai b/src/test_visualizer.bonsai index 886110b4..252f14a5 100644 --- a/src/test_visualizer.bonsai +++ b/src/test_visualizer.bonsai @@ -222,7 +222,7 @@ - + @@ -235,7 +235,7 @@ false 0.5 5 - true + true 0 diff --git a/tests/trial_generators/test_block_based_trial_generator.py b/tests/trial_generators/test_block_based_trial_generator.py index 77b2db61..be62ba10 100644 --- a/tests/trial_generators/test_block_based_trial_generator.py +++ b/tests/trial_generators/test_block_based_trial_generator.py @@ -111,20 +111,20 @@ def test_next_gives_right_autowater_on_left_bias(self): gen = self._make_generator(bias=-0.9) trial = gen.next() assert trial is not None - self.assertTrue(trial.is_auto_response_right) + self.assertTrue(trial.is_auto_reward_right) def test_next_gives_left_autowater_on_right_bias(self): gen = self._make_generator(bias=0.9) trial = gen.next() assert trial is not None - self.assertFalse(trial.is_auto_response_right) + self.assertFalse(trial.is_auto_reward_right) def test_next_no_antibias_when_below_interval(self): """No antibias effect when trials_in_bias_intervention has not exceeded interval.""" gen = self._make_generator(bias=-0.9, trials_in_bias_intervention=5) trial = gen.next() assert trial is not None - self.assertIsNone(trial.is_auto_response_right) + self.assertIsNone(trial.is_auto_reward_right) def test_next_antibias_overrides_autowater(self): """When both autowater and antibias conditions are met, antibias takes precedence.""" @@ -146,7 +146,7 @@ def test_next_antibias_overrides_autowater(self): # Antibias (left bias → give right water) should win assert trial is not None - self.assertTrue(trial.is_auto_response_right) + self.assertTrue(trial.is_auto_reward_right) def test_next_lickspout_delta_nonzero_after_corrections_exhausted(self): """After max water corrections, next() should produce a nonzero lickspout delta.""" diff --git a/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py index cdab6c50..b58af42c 100644 --- a/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py +++ b/workspace/aind_behavior_dynamic_foraging_curricula/src/aind_behavior_dynamic_foraging_curricula/metrics.py @@ -68,7 +68,7 @@ def metrics_from_dataset( trial_outcomes = software_events["TrialOutcome"].data["data"].iloc # exclude auto response and ignored trials filtered = [ - t for t in trial_outcomes if t["is_right_choice"] is not None and t["trial"]["is_auto_response_right"] is None + t for t in trial_outcomes if t["is_right_choice"] is not None and t["trial"]["is_auto_reward_right"] is None ] is_right_choice = [to["is_right_choice"] for to in filtered] is_rewarded = [to["is_rewarded"] for to in filtered] diff --git a/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py b/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py index 71f62784..4d157d57 100644 --- a/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py +++ b/workspace/aind_behavior_dynamic_foraging_curricula/tests/test_metrics.py @@ -15,7 +15,7 @@ def _make_trial( is_rewarded: bool, p_reward_right: float, p_reward_left: float, - is_auto_response_right: Optional[bool] = False, + is_auto_reward_right: Optional[bool] = False, ) -> dict: return { "is_right_choice": is_right_choice, @@ -23,7 +23,7 @@ def _make_trial( "trial": { "p_reward_right": p_reward_right, "p_reward_left": p_reward_left, - "is_auto_response_right": is_auto_response_right, + "is_auto_reward_right": is_auto_reward_right, }, } From 4b44ef582d9bfa05609f00d658ea2668d9045fcf Mon Sep 17 00:00:00 2001 From: Micah Woodard Date: Thu, 2 Jul 2026 14:50:35 -0700 Subject: [PATCH 2/4] witholds reward on auto reward trial --- src/Extensions/TaskEngine.bonsai | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/Extensions/TaskEngine.bonsai b/src/Extensions/TaskEngine.bonsai index ddd4736a..21d04991 100644 --- a/src/Extensions/TaskEngine.bonsai +++ b/src/Extensions/TaskEngine.bonsai @@ -986,11 +986,35 @@ IsRewarded + + ThisTrial + + + IsAutoRewardRight + + + HasValue + + + + + + + + false + + - + + + + + + + From 56692da05e9c62c9a631219e1ce80b0f8842a7eb Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Thu, 2 Jul 2026 17:37:35 -0700 Subject: [PATCH 3/4] Short-circuit reward consumption logic if auto-water is enabled --- src/Extensions/TaskEngine.bonsai | 132 ++++++++++++++----------------- 1 file changed, 61 insertions(+), 71 deletions(-) diff --git a/src/Extensions/TaskEngine.bonsai b/src/Extensions/TaskEngine.bonsai index 21d04991..0841eafe 100644 --- a/src/Extensions/TaskEngine.bonsai +++ b/src/Extensions/TaskEngine.bonsai @@ -937,6 +937,37 @@ ResponseOutcome + + ThisTrial + + + IsAutoRewardRight + + + HasValue + + + + + + Source1 + + + + + + + + + + AutoWater + + + + + false + + ResponseOutcome @@ -959,64 +990,18 @@ PT0S - - IsRewarded + + Not IsRewarded + !IsRewarded - - - - - - Source1 - - - - - - - + + + false + - - IsRewarded - - - - Source1 - - - IsRewarded - - - ThisTrial - - - IsAutoRewardRight - - - HasValue - - - - - - - - false - - - - - - - - - - - - - - - + + IsRewarded + it.IsRewarded Response.Value.Value @@ -1074,31 +1059,36 @@ - + - + - - - + + - + - - + + + - + - - - - + + + + - - + + + + + + + From 72b1c3bfe0e6115bf1d062ab4e72e5b5625693b3 Mon Sep 17 00:00:00 2001 From: bruno-f-cruz <7049351+bruno-f-cruz@users.noreply.github.com> Date: Mon, 6 Jul 2026 07:50:53 -0700 Subject: [PATCH 4/4] Nest AutoWater inside reward delay logic --- src/Extensions/TaskEngine.bonsai | 114 ++++++++++++++++++------------- 1 file changed, 66 insertions(+), 48 deletions(-) diff --git a/src/Extensions/TaskEngine.bonsai b/src/Extensions/TaskEngine.bonsai index 0841eafe..7cb5dde1 100644 --- a/src/Extensions/TaskEngine.bonsai +++ b/src/Extensions/TaskEngine.bonsai @@ -937,14 +937,53 @@ ResponseOutcome + + ResponseOutcome + ThisTrial - IsAutoRewardRight + RewardDelayDuration - - HasValue + + + + + + + + + + + PT0S + + + + AutoWater short + + + + CheckAutoWater + + + + ThisTrial + + + IsAutoRewardRight + + + HasValue + + + + + + + + + @@ -959,37 +998,11 @@ - - AutoWater - - false - - ResponseOutcome - - - ThisTrial - - - RewardDelayDuration - - - - - - - - - - - - PT0S - - Not IsRewarded !IsRewarded @@ -1009,6 +1022,11 @@ GiveRewardRight + + + true + + @@ -1059,36 +1077,36 @@ - + - + - - + + + + - - - - + + + - + - - - - + + + + - - - - + + + + - + -