Skip to content

Commit 0753925

Browse files
Blink task as simple task example ;)
restructure example dir into folders, add to docs.
1 parent a6b262f commit 0753925

File tree

7 files changed

+229
-0
lines changed

7 files changed

+229
-0
lines changed

docs/examples.rst

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
Examples
2+
=========
3+
4+
We're working on writing more examples! Please let us know in the discussion board what you'd like to see :)
5+
6+
Also see the ``examples`` folder in the repository for jupyter notebooks we haven't set up Sphinx rendering for yet ;)
7+
8+
.. toctree::
9+
maxdepth: 1
10+
:caption: Tasks
11+
12+
examples.tasks.blink

docs/examples.tasks.blink.rst

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
Blink
2+
======================
3+
4+
.. automodule:: examples.tasks.blink
5+
:members:
6+
:undoc-members:
7+
:show-inheritance:

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ This documentation is very young and is very much a work in progress! Please `su
4646
Training a Subject <guide.training>
4747
Writing a Task <guide.task>
4848
Writing a Hardware Class <guide.hardware>
49+
Examples <examples>
4950

5051

5152
.. toctree::

examples/__init__.py

Whitespace-only changes.

examples/tasks/__init__.py

Whitespace-only changes.

examples/tasks/blink.py

Lines changed: 209 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,209 @@
1+
"""
2+
A very simple task: Blink an LED
3+
4+
Written by @mikewehr in the ``mike`` branch: https://github.com/wehr-lab/autopilot/blob/mike/autopilot/tasks/blink.py
5+
6+
Demonstrates the basic structure of a task with one stage,
7+
described in the comments throughout the task.
8+
9+
See the main tutorial for more detail: https://docs.auto-pi-lot.com/en/latest/guide.task.html#
10+
11+
This page is rendered in the docs here in order to provide links to the mentioned objects/classes/etc., but
12+
this example was intended to be read as source code, as some comments will only be visible there.
13+
14+
.. note::
15+
Currently, after completion, the task needs to be explicitly imported and added to :data:`.tasks.TASK_LIST` ,
16+
this will be remedied shortly (see the ``registries`` branch).
17+
"""
18+
import itertools
19+
import tables
20+
import time
21+
from datetime import datetime
22+
23+
from autopilot.hardware import gpio
24+
from autopilot.tasks import Task
25+
from collections import OrderedDict as odict
26+
27+
class Blink(Task):
28+
"""
29+
Blink an LED.
30+
31+
Note that we subclass the :class:`.tasks.Task` class (``Blink(Task)``) to provide us with some methods
32+
useful for all Tasks.
33+
34+
Args:
35+
pulse_duration (int, float): Duration the LED should be on, in ms
36+
pulse_interval (int, float): Duration the LED should be off, in ms
37+
38+
"""
39+
# Tasks need to have a few class attributes defined to be integrated into the rest of the system
40+
# See here for more about class vs. instance attributes https://www.toptal.com/python/python-class-attributes-an-overly-thorough-guide
41+
42+
STAGE_NAMES = ["pulse"] # type: list
43+
"""
44+
An (optional) list or tuple of names of methods that will be used as stages for the task.
45+
46+
See :attr:`.Blink.stages` for more information
47+
"""
48+
49+
PARAMS = odict()
50+
"""
51+
A dictionary that specifies the parameters that control the operation of the task -- each task presumably has some
52+
range of options that allow slight variations (eg. different stimuli, etc.) on a shared task structure. This
53+
dictionary specifies each ``PARAM`` as a human-readable ``tag`` and a ``type`` that is used by the gui to
54+
create an appropriate input object. For example::
55+
56+
PARAMS['pulse_duration'] = {'tag': 'LED Pulse Duration (ms)', 'type': 'int'}
57+
58+
When instantiated, these params are passed to the ``__init__`` method.
59+
60+
A :class:`collections.OrderedDict` is used so that parameters can be presented in a predictable way to users.
61+
"""
62+
PARAMS['pulse_duration'] = {'tag': 'LED Pulse Duration (ms)', 'type': 'int'}
63+
PARAMS['pulse_interval'] = {'tag': 'LED Pulse Interval (ms)', 'type': 'int'}
64+
65+
class TrialData(tables.IsDescription):
66+
"""
67+
This class declares the data that will be returned for each "trial" -- or complete set of executed task
68+
stages. It is used by the :class:`.subject.Subject` object to make a data table with the
69+
correct data types. Declare each piece of data using a pytables Column descriptor
70+
(see https://www.pytables.org/usersguide/libref/declarative_classes.html#col-sub-classes for available
71+
data types, and the pytables guide: https://www.pytables.org/usersguide/tutorials.html for more information)
72+
73+
For each trial, we'll return two timestamps, the time we turned the LED on, the time we turned it off,
74+
and the trial number. Note that we use a 26-character :class:`tables.StringCol` for the timestamps,
75+
which are given as an isoformatted string like ``'2021-02-16T18:11:35.752110'``
76+
"""
77+
trial_num = tables.Int32Col()
78+
timestamp_on = tables.StringCol(26)
79+
timestamp_off = tables.StringCol(26)
80+
81+
82+
HARDWARE = {
83+
'LEDS': {
84+
'dLED': gpio.Digital_Out
85+
}
86+
}
87+
"""
88+
Declare the hardware that will be used in the task. Each hardware object is specified with a ``group`` and
89+
an ``id`` as nested dictionaries. These descriptions require a set of hardware parameters in the autopilot
90+
``prefs.json`` (typically generated by :mod:`autopilot.setup.setup_autopilot` ) with a matching ``group`` and
91+
``id`` structure. For example, an LED declared like this in the :attr:`.Blink.HARDWARE` attribute::
92+
93+
HARDWARE = {'LEDS': {'dLED': gpio.Digital_Out}}
94+
95+
requires an entry in ``prefs.json`` like this::
96+
97+
"HARDWARE": {"LEDS": {"dLED": {
98+
"pin": 1,
99+
"polarity": 1
100+
}}}
101+
102+
that will be used to instantiate the :class:`.hardware.gpio.Digital_Out` object, which is then available for use
103+
in the task like::
104+
105+
self.hardware['LEDS']['dLED'].set(1)
106+
"""
107+
108+
def __init__(self, stage_block=None, pulse_duration=100, pulse_interval=500, *args, **kwargs):
109+
# first we call the superclass ('Task')'s initialization method. All tasks should accept ``*args``
110+
# and ``**kwargs`` to pass parameters not explicitly specified by subclass up to the superclass.
111+
super(Blink, self).__init__(*args, **kwargs)
112+
113+
# store parameters given on instantiation as instance attributes
114+
self.pulse_duration = int(pulse_duration)
115+
self.pulse_interval = int(pulse_interval)
116+
self.stage_block = stage_block # type: "threading.Event"
117+
118+
# This allows us to cycle through the task by just repeatedly calling self.stages.next()
119+
self.stages = itertools.cycle([self.pulse])
120+
"""
121+
Some generator that returns the stage methods that define the operation of the task.
122+
123+
To run a task, the :class:`.pilot.Pilot` object will call each stage function, which can return some dictionary
124+
of data (see :meth:`.Blink.pulse` ) and wait until some flag (:attr:`.Blink.stage_block` ) is set to compute the
125+
next stage. Since in this case we want to call the same method (:meth:`.Blink.pulse` ) over and over again,
126+
we use an :class:`itertools.cycle` object (if we have more than one stage to call in a cycle, we could provide
127+
them like ``itertools.cycle([self.stage_method_1, self.stage_method_2])`` . More complex tasks can define a custom
128+
generator for finer control over stage progression.
129+
"""
130+
131+
self.trial_counter = itertools.count()
132+
"""
133+
Some counter to keep track of the trial number
134+
"""
135+
136+
137+
self.init_hardware()
138+
"""
139+
Hardware is initialized by the superclass's :meth:`.Task.init_hardware` method, which creates all the
140+
hardware objects defined in :attr:`.Blink.HARDWARE` according to their parameterization in
141+
``prefs.json`` , and makes them available in the :attr:`.Blink.hardware` dictionary.
142+
"""
143+
self.logger.debug('Hardware initialized')
144+
"""
145+
All task subclass objects have an :attr:`.Task.logger` -- a :class:`logging.Logger` that allows
146+
users to easily debug their tasks and see feedback about their operation. To prevent stdout from
147+
getting clogged, logging messages are printed and stored according to the ``LOGLEVEL`` pref -- so this
148+
message would only appear if ``LOGLEVEL == "DEBUG"``
149+
"""
150+
151+
self.stage_block.set()
152+
"""
153+
We set the stage block and never clear it so that the :class:`.Pilot` doesn't wait for a trigger
154+
to call the next stage -- it just does it as soon as the previous one completes.
155+
156+
See :meth:`.Pilot.run_task` for more detail on this loop.
157+
"""
158+
159+
160+
##################################################################################
161+
# Stage Functions
162+
##################################################################################
163+
def pulse(self, *args, **kwargs):
164+
"""
165+
Turn an LED on and off according to :attr:`.Blink.pulse_duration` and :attr:`.Blink.pulse_interval`
166+
167+
Returns:
168+
dict: A dictionary containing the trial number and two timestamps.
169+
"""
170+
# -------------
171+
# turn light on
172+
173+
# use :meth:`.hardware.gpio.Digital_Out.set` method to turn the LED on
174+
self.hardware['LEDS']['dLED'].set(1)
175+
# store the timestamp
176+
timestamp_on = datetime.now().isoformat()
177+
# log status as a debug message
178+
self.logger.debug('light on')
179+
# sleep for the pulse_duration
180+
time.sleep(self.pulse_duration / 1000)
181+
182+
# ------------
183+
# turn light off, same as turning it on.
184+
185+
self.hardware['LEDS']['dLED'].set(0)
186+
timestamp_off = datetime.now().isoformat()
187+
self.logger.debug('light off')
188+
time.sleep(self.pulse_interval / 1000)
189+
190+
# count and store the number of the current trial
191+
self.current_trial = next(self.trial_counter)
192+
193+
194+
data = {
195+
'trial_num': self.current_trial,
196+
'timestamp_on': timestamp_on,
197+
'timestamp_off': timestamp_off
198+
}
199+
"""
200+
Create the data dictionary to be returned from the stage. Note that each of the keys in the dictionary
201+
must correspond to the names of the columns declared in the :attr:`.Blink.TrialData` descriptor.
202+
203+
At the conclusion of running the task, we will be able to access the data from the run with
204+
:meth:`.Subject.get_trial_data`, which will be a :class:`pandas.DataFrame` with a row for each trial, and
205+
a column for each of the fields here.
206+
"""
207+
208+
# return the data dictionary from the stage method and yr done :)
209+
return data

0 commit comments

Comments
 (0)