Skip to content

Commit bcb2f96

Browse files
authored
Add "Learning Sets" example (#557)
* Add example for one shot reversal task * Change experiment name * Linting
1 parent 2f73831 commit bcb2f96

1 file changed

Lines changed: 184 additions & 0 deletions

File tree

examples/task_learning_sets.py

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,184 @@
1+
import os
2+
import random
3+
4+
import aind_behavior_services.task.distributions as distributions
5+
import aind_behavior_vr_foraging.task_logic as vr_task_logic
6+
from aind_behavior_curriculum import Stage, TrainerState
7+
from aind_behavior_vr_foraging.task_logic import (
8+
AindVrForagingTaskLogic,
9+
AindVrForagingTaskParameters,
10+
)
11+
from collections import deque
12+
13+
MINIMUM_INTERPATCH_LENGTH = 50
14+
MEAN_INTERPATCH_LENGTH = 120
15+
MAXIMUM_INTERPATCH_LENGTH = 450
16+
INTERSITE_LENGTH = 50
17+
REWARDSITE_LENGTH = 50
18+
REWARD_AMOUNT = 7
19+
VELOCITY_THRESHOLD = 8 # cm/s
20+
21+
ODOR_COUNT = 7
22+
23+
24+
def odor_concentration_from_index(
25+
odor_index: int, concentration: float = 1.0
26+
) -> vr_task_logic.OdorMixture:
27+
"""Helper function to create an odor concentration vector from an index and concentration value."""
28+
arr = [0.0 for x in range(ODOR_COUNT)]
29+
arr[odor_index] = concentration
30+
return arr
31+
32+
33+
def make_patch(
34+
is_rewarded: bool,
35+
odor_index: int,
36+
):
37+
38+
return vr_task_logic.Patch(
39+
label=f"{odor_index}_Rewarded" if is_rewarded else f"{odor_index}_NonRewarded",
40+
state_index=odor_index + ODOR_COUNT * int(is_rewarded),
41+
odor_specification=odor_concentration_from_index(odor_index, 1.0),
42+
patch_terminators=[
43+
vr_task_logic.PatchTerminatorOnRewardSite(
44+
count=vr_task_logic.scalar_value(1)
45+
),
46+
],
47+
reward_specification=vr_task_logic.RewardSpecification(
48+
amount=vr_task_logic.scalar_value(REWARD_AMOUNT),
49+
probability=vr_task_logic.scalar_value(1.0 if is_rewarded else 0.0),
50+
delay=vr_task_logic.scalar_value(0.5),
51+
operant_logic=vr_task_logic.OperantLogic(
52+
is_operant=False,
53+
stop_duration=2.0,
54+
time_to_collect_reward=100000,
55+
grace_distance_threshold=10,
56+
),
57+
),
58+
patch_virtual_sites_generator=vr_task_logic.PatchVirtualSitesGenerator(
59+
inter_patch=vr_task_logic.VirtualSiteGenerator(
60+
render_specification=vr_task_logic.RenderSpecification(contrast=1),
61+
label=vr_task_logic.VirtualSiteLabels.INTERPATCH,
62+
length_distribution=distributions.ExponentialDistribution(
63+
distribution_parameters=distributions.ExponentialDistributionParameters(
64+
rate=1 / MEAN_INTERPATCH_LENGTH
65+
),
66+
scaling_parameters=distributions.ScalingParameters(
67+
offset=MINIMUM_INTERPATCH_LENGTH
68+
),
69+
truncation_parameters=distributions.TruncationParameters(
70+
min=MINIMUM_INTERPATCH_LENGTH,
71+
max=MAXIMUM_INTERPATCH_LENGTH,
72+
),
73+
),
74+
),
75+
inter_site=vr_task_logic.VirtualSiteGenerator(
76+
render_specification=vr_task_logic.RenderSpecification(contrast=0.5),
77+
label=vr_task_logic.VirtualSiteLabels.INTERSITE,
78+
length_distribution=vr_task_logic.scalar_value(INTERSITE_LENGTH),
79+
),
80+
reward_site=vr_task_logic.VirtualSiteGenerator(
81+
render_specification=vr_task_logic.RenderSpecification(contrast=0.5),
82+
label=vr_task_logic.VirtualSiteLabels.REWARDSITE,
83+
length_distribution=vr_task_logic.scalar_value(REWARDSITE_LENGTH),
84+
),
85+
),
86+
)
87+
88+
89+
def get_odor_sequence(total_trials: int, n: int) -> list[tuple[int, int]]:
90+
91+
ODORS = list(range(ODOR_COUNT))
92+
93+
if len(ODORS) < 2 * n + 2:
94+
raise ValueError(
95+
"Not enough odors to satisfy the constraints with the given n."
96+
)
97+
98+
patches = []
99+
history = deque(maxlen=n)
100+
101+
for _ in range(total_trials):
102+
forbidden = set()
103+
for pair in history:
104+
forbidden.update(pair)
105+
106+
available = [o for o in ODORS if o not in forbidden]
107+
108+
pos = random.choice(available)
109+
available.remove(pos)
110+
neg = random.choice(available)
111+
112+
patches.append((neg, pos))
113+
history.append((neg, pos))
114+
115+
return patches
116+
117+
118+
def make_block(
119+
n_sites_each: int = 5,
120+
n_pairs: int = 500,
121+
) -> vr_task_logic.Block:
122+
123+
odor_sequence = get_odor_sequence(total_trials=n_pairs, n=1)
124+
trial_sequence: list[int] = []
125+
for pair in odor_sequence:
126+
this_block = [pair[0], pair[1] + ODOR_COUNT] * n_sites_each
127+
random.shuffle(this_block)
128+
trial_sequence.extend(this_block)
129+
130+
return vr_task_logic.Block(
131+
environment=vr_task_logic.SequenceEnvironment(
132+
patches=[
133+
make_patch(is_rewarded=False, odor_index=i) for i in range(ODOR_COUNT)
134+
]
135+
+ [make_patch(is_rewarded=True, odor_index=i) for i in range(ODOR_COUNT)],
136+
sampling_mode="Ordered",
137+
patch_indices=trial_sequence,
138+
),
139+
end_conditions=[
140+
vr_task_logic.BlockEndConditionPatchCount(
141+
value=vr_task_logic.scalar_value(n_sites_each * 2)
142+
)
143+
],
144+
)
145+
146+
147+
operation_control = vr_task_logic.OperationControl(
148+
position_control=vr_task_logic.PositionControl(
149+
frequency_filter_cutoff=5,
150+
velocity_threshold=VELOCITY_THRESHOLD,
151+
),
152+
)
153+
154+
task_logic = AindVrForagingTaskLogic(
155+
task_parameters=AindVrForagingTaskParameters(
156+
rng_seed=None,
157+
environment=vr_task_logic.BlockStructure(
158+
blocks=[make_block(n_sites_each=5, n_pairs=100)],
159+
),
160+
operation_control=operation_control,
161+
),
162+
stage_name="LearningSets",
163+
)
164+
165+
166+
def main(path_seed: str = "./local/LearningSets_{schema}.json"):
167+
example_task_logic = task_logic
168+
example_trainer_state = TrainerState(
169+
stage=Stage(name="example_stage", task=example_task_logic),
170+
curriculum=None,
171+
is_on_curriculum=False,
172+
)
173+
os.makedirs(os.path.dirname(path_seed), exist_ok=True)
174+
models = [example_task_logic, example_trainer_state]
175+
176+
for model in models:
177+
with open(
178+
path_seed.format(schema=model.__class__.__name__), "w", encoding="utf-8"
179+
) as f:
180+
f.write(model.model_dump_json(indent=2))
181+
182+
183+
if __name__ == "__main__":
184+
main()

0 commit comments

Comments
 (0)