forked from jamieboyd/AutoHeadFix
-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathAHF_DataLogger_text.py
277 lines (248 loc) · 12.5 KB
/
AHF_DataLogger_text.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
#! /usr/bin/python -*-coding: utf-8 -*-
from os import path, makedirs, chown, listdir
from pwd import getpwnam
from grp import getgrnam
from time import time, localtime,timezone, sleep
from datetime import datetime
import AHF_ClassAndDictUtils as CAD
from AHF_DataLogger import AHF_DataLogger
class AHF_DataLogger_text(AHF_DataLogger):
"""
Simple text-based data logger modified from the original Auto Head Fix code
makes a new text logfile for each day, saved in default data path.
Mouse data is stored in a specified folder, also as text files, one text file per mouse
containing JSON formatted configuration and performance data. These files will opened and
updated after each exit from the experimental tube, in case the program needs to be restarted
The file name for each mouse contains RFID tag 0-padded to 13 spaces: AHF_mouse_1234567890123.jsn
"""
PSEUDO_MUTEX =0
"""
The class field PSEUDO_MUTEX helps prevent print statements from different places in the code(main vs
callbacks) from executing at the same time, which leads to garbled output. Unlike a real mutex, execution
of the thread is not halted while waiting on the PSEUDO_MUTEX, hence the loops with calls to sleep to
allow the other threads of execution to continue while waiting for the mutex to be free. Also, read/write
to the PSEUDO_MUTEX is not atomic; one thread may read PSEUDO_MUTEX as 0, and set it to 1, but in the
interval between reading and writing to PSEUDO_MUTEX,another thread may have read PSEUDO_MUTEX as 0 and
both threads think they have the mutex
"""
defaultCage = 'cage1'
defaultDataPath='/home/pi/Documents/'
defaultConfigPath = '/home/pi/Documents/MiceConfig/'
@staticmethod
def about():
return 'Simple text-based data logger that prints mouse id, time, event type, and event dictionary to a text file, and to the shell.'
@staticmethod
def config_user_get(starterDict = {}):
# cage ID
cageID = starterDict.get('cageID', AHF_DataLogger_text.defaultCage)
response = input('Enter a name for the cage ID(currently %s): ' % cageID)
if response != '':
cageID = response
#old style
oldVersion = starterDict.get("oldVersion", False)
response = input("Do you want to save the text files in the old style (no event dict)? (y/n) Currently %s" % oldVersion)
if response != '':
if response[0].lower() == 'y':
oldVersion = True
else:
oldVersion = False
# data path
dataPath = starterDict.get('dataPath', AHF_DataLogger_text.defaultDataPath)
response = input('Enter the path to the directory where the data will be saved(currently %s): ' % dataPath)
if response != '':
dataPath = response
if not dataPath.endswith('/'):
dataPath += '/'
# config path
configPath = starterDict.get('mouseConfigPath', AHF_DataLogger_text.defaultConfigPath)
response = input('Enter the path to the directory from which to load and store mouse configuration(currently %s): ' % configPath)
if response != '':
configPath = response
# update and return dict
starterDict.update({'cageID' : cageID, 'dataPath' : dataPath, 'mouseConfigPath' : configPath, 'oldVersion': oldVersion})
return starterDict
def setup(self):
"""
copies settings, creates folders for data and stats, creates initial log file and writes start session
dataPath path to data folder
_|_
/ \
logPath statsPath subfolders within data folder
| |
logFilePath statsFilePath paths to individual files within corresponding subfolders
"""
self.cageID = self.settingsDict.get('cageID')
self.oldVersion = self.settingsDict.get('oldVersion')
self.lastStim = None
self.dataPath = self.settingsDict.get('dataPath')
self.configPath = self.settingsDict.get('mouseConfigPath')
self.logFP = None # reference to log file that will be created
self.dataPath = self.settingsDict.get('dataPath')
self.configPath = self.settingsDict.get('mouseConfigPath')
self.logFP = None # reference to log file that will be created
self.logPath = self.dataPath + 'LogFiles/' # path to the folder containing log files
self.logFilePath = '' # path to the log file
self.statsPath = self.dataPath + 'QuickStats/'
self.uid = getpwnam('pi').pw_uid
self.gid = getgrnam('pi').gr_gid
# data path
if not path.exists(self.dataPath):
makedirs(self.dataPath, mode=0o777, exist_ok=True)
chown(self.dataPath, self.uid, self.gid)
# logPath, folder in data path
if not path.exists(self.logPath):
makedirs(self.logPath, mode=0o777, exist_ok=True)
chown(self.logPath, self.uid, self.gid)
# stats path, a different folder in data path
if not path.exists(self.statsPath):
makedirs(self.statsPath, mode=0o777, exist_ok=True)
chown(self.statsPath, self.uid, self.gid)
# mouseConFigPath can be anywhere, not obliged to be in data path
if not path.exists(self.configPath):
makedirs(self.configPath, mode=0o777, exist_ok=True)
chown(self.configPath, self.uid, self.gid)
self.setDateStr() # makes a string for today's date, used in file names
self.makeLogFile() # makes and opens a file for today's data inside logFilePath folder, sets logFP
self.writeToLogFile(0, 'SeshStart', None, time())
def hardwareTest(self):
"""
Tests functionality, gives user a chance to change settings
"""
pass
def setdown(self):
"""
Writes session end and closes log file
"""
if getattr(self, 'logFP', None) is not None:
self.writeToLogFile(0, 'SeshEnd', None, time())
self.logFP.close()
self.logFP = None
mice = self.task.Subjects.get_all()
for tag, mouse in mice.items():
self.storeConfig(tag, mouse)
def configGenerator(self):
"""
Each configuration file has config data for a single subject. This function loads data
from all of them in turn, and returning each as a a tuple of(tagID, dictionary)
"""
for fname in listdir(self.configPath):
if fname.startswith('AHF_mouse_') and fname.endswith('.jsn'):
tagStr = fname[10:len(fname)-4]
yield(int(tagStr), CAD.File_to_dict('mouse', tagStr, '.jsn', dir = self.configPath))
def getConfigData(self, tag):
"""
returns saved dictionary for given tag
"""
return CAD.File_to_dict('mouse', '{:013}'.format(int(tag)), '.jsn', dir = self.configPath)
def storeConfig(self, tag, configDict, source = ""):
"""
saves data to corresponding json text file, overwriting old file
"""
if tag is int:
CAD.Dict_to_file(configDict, 'mouse', '{:013}'.format(int(tag)), '.jsn', dir = self.configPath)
def saveNewMouse(self, tag, note, dictionary = {}):
self.storeConfig(tag, dictionary)
self.writeToLogFile(tag, "NewMouse", dictionary, time())
def getMice(self):
mice = []
for config in self.configGenerator():
mice.append(config[0])
return mice
def retireMouse(self, tag, reason):
CAD.Remove_file('mouse', '{:013}'.format(int(tag)), '.jsn', dir = self.configPath)
self.writeToLogFile(tag, "Retirement", {'reason': reason}, time())
def newDay(self):
self.writeToLogFile(0, 'SeshEnd', None, time())
if self.logFP is not None:
self.logFP.close()
self.setDateStr()
self.makeLogFile()
def makeLogFile(self):
"""
open a new text log file for today, or open an exisiting text file with 'a' for append
"""
try:
self.logFilePath = self.logPath + 'headFix_' + self.cageID + '_' + self.dateStr + '.txt'
self.logFP = open(self.logFilePath, 'a')
chown(self.logFilePath, self.uid, self.gid)
except Exception as e:
print("Error maing log file\n", str(e))
def readFromLogFile(self, tag, index):
pass
def writeToLogFile(self, tag, eventKind, eventDict, timeStamp, toShellOrFile=3):
"""
Writes the time and type of each event to a text log file, and also to the shell
Format of the output string: tag time_epoch or datetime event
The computer-parsable time_epoch is printed to the log file and user-friendly datetime is printed to the shell
:param tag: the tag of mouse, usually from RFIDTagreader.globalTag
:param eventKind: the type of event to be printed, entry, exit, reward, etc.
:param eventDict: a dictionary containing data about the event(may be None if no associated data)
returns: nothing
"""
if eventKind == 'SeshStart' or eventKind == 'SeshEnd':
tag = 0
eventDict = None
LogOutputStr = '{:013}\t{:s}\t{:s}\t{:s}\n'.format(int(tag), eventKind, str(eventDict), datetime.fromtimestamp(int(timeStamp)).isoformat(' '))
while AHF_DataLogger_text.PSEUDO_MUTEX ==1:
sleep(0.01)
AHF_DataLogger_text.PSEUDO_MUTEX = 1
print(LogOutputStr)
AHF_DataLogger_text.PSEUDO_MUTEX = 0
if getattr(self, 'logFP', None) is not None and self.task.logToFile: # logMouse is set to False for test mice, or unknown mice
if self.oldVersion:
if eventKind == 'Reward':
if eventDict['kind'] == 'entry':
eventKind = 'entryReward'
else:
eventKind = 'reward'
elif eventKind == 'Fix':
eventKind = eventDict["result"]
elif eventKind == 'lick':
eventKind += ":" + str(eventDict['chan'])
elif eventKind == 'VideoStart':
eventKind = "video:" + eventDict["name"]
elif eventKind == 'Stimulus':
self.lastStim = timeStamp
self.stimKind = None
if eventDict is not None:
self.stimKind = eventDict['trial']
return
elif eventKind == 'Outcome':
N = 1
if self.stimKind == "NO-GO":
N = 2
eventKind = 'lickWitholdTime={:.2f},Buzz:N={:.2f},length={:2f},period={:.2f},{:s}={:s}'.format(eventDict["withholdTime"], N, self.task.Stimulus.length(), self.task.Stimulus.period(), self.stimKind, str(eventDict['code']))
timeStamp = self.lastStim
FileOutputStr = '{:013}\t{:s}\t{:s}\t{:s}\t{:s}\n'.format(int(tag), str(timeStamp), eventKind, datetime.fromtimestamp(int(timeStamp)).isoformat (' '), str(eventDict))
else:
FileOutputStr = '{:013}\t{:s}\t{:s}\t{:s}\t{:s}\n'.format(int(tag), str(timeStamp), eventKind, str(eventDict), datetime.fromtimestamp(int(timeStamp)).isoformat (' '))
self.logFP.write(FileOutputStr)
self.logFP.flush()
def setDateStr(self):
"""
Sets the string corresponding to todays date that is used when making files
"""
dateTimeStruct = localtime()
self.dateStr='{:04}{:02}{:02}'.format(dateTimeStruct.tm_year, dateTimeStruct.tm_mon, dateTimeStruct.tm_mday)
def makeQuickStatsFile(self, mice):
"""
makes a quickStats file for today's results.
QuickStats file contains daily totals of rewards and headFixes for each mouse
"""
statsFilePath = self.statsPath + 'quickStats_' + self.cageID + '_' + self.dateStr + '.txt'
statsFP = open(statsFilePath, 'w')
statsFP.write('Mouse_ID\tentries\tent_rew\thfixes\thf_rew')
if mice is not None:
keyList = self.task.Stimulator.MousePrecis(mice)
mice.addMiceFromFile(self.statsFP)
mice.show()
else:
self.statsFP = open(self.statsFilePath, 'w')
self.statsFP.write('Mouse_ID\tentries\tent_rew\thfixes\thf_rew\n')
self.statsFP.close()
self.statsFP = open(self.statsFilePath, 'r+')
uid = getpwnam('pi').pw_uid
gid = getgrnam('pi').gr_gid
chown(self.statsFilePath, uid, gid)
def __del__(self):
self.setdown()