Skip to content

Commit abcc78a

Browse files
committed
- Removed code for saving .txt file format data. These changes also cause the log output to resemble the format of the tsv files rather than the .txt files.
- The values of all variables are now automatically saved to the data file at the start and end of each run, with an entry in the 'name' column of the tsv file indicating that these are the 'run_start' and 'run_end' values.
1 parent a483ce7 commit abcc78a

File tree

7 files changed

+64
-118
lines changed

7 files changed

+64
-118
lines changed

com/data_logger.py

Lines changed: 54 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import os
2-
import json
32
import numpy as np
43
from datetime import datetime
54
from shutil import copyfile
@@ -19,47 +18,34 @@ def set_state_machine(self, sm_info):
1918
self.sm_info = sm_info
2019
self.ID2name_fw = self.sm_info['ID2name'] # Dict mapping framework IDs to names.
2120

22-
def open_data_file(self, data_dir, experiment_name, setup_ID, subject_ID,
23-
file_type, datetime_now=None):
21+
def open_data_file(self, data_dir, experiment_name, setup_ID, subject_ID, datetime_now=None):
2422
'''Open file tsv/txt file for event data and write header information.
2523
If state machine uses analog inputs instantiate analog data writers.'''
2624
self.data_dir = data_dir
2725
self.experiment_name = experiment_name
2826
self.subject_ID = subject_ID
2927
self.setup_ID = setup_ID
30-
self.file_type = file_type
3128
if datetime_now is None: datetime_now = datetime.now()
32-
self.end_time = -1
33-
file_name = self.subject_ID + datetime_now.strftime('-%Y-%m-%d-%H%M%S') + '.' + self.file_type
29+
self.end_timestamp = -1
30+
file_name = self.subject_ID + datetime_now.strftime('-%Y-%m-%d-%H%M%S') + '.tsv'
3431
self.file_path = os.path.join(self.data_dir, file_name)
3532
self.data_file = open(self.file_path, 'w', newline = '\n')
36-
if self.file_type == 'tsv': # Write header.
37-
self.data_file.write(self.tsv_row_str(
38-
rtype='type', time='time', name='name', value='value'))
39-
self.write_info_line('Experiment name', self.experiment_name)
40-
self.write_info_line('Task name', self.sm_info['name'])
41-
self.write_info_line('Task file hash', self.sm_info['task_hash'])
42-
self.write_info_line('Setup ID', self.setup_ID)
43-
self.write_info_line('Framework version', self.sm_info['framework_version'])
44-
self.write_info_line('Micropython version', self.sm_info['micropython_version'])
45-
self.write_info_line('Subject ID', self.subject_ID)
46-
if self.file_type == 'txt':
47-
self.write_info_line('Start date', datetime_now.strftime('%Y/%m/%d %H:%M:%S'))
48-
self.data_file.write('\n')
49-
self.data_file.write('S {}\n\n'.format(json.dumps(self.sm_info['states'])))
50-
self.data_file.write('E {}\n\n'.format(json.dumps(self.sm_info['events'])))
51-
else:
52-
self.write_info_line('start_time', datetime.utcnow().isoformat(timespec='milliseconds'))
33+
self.data_file.write(self.tsv_row_str( # Write header with row names.
34+
rtype='type', time='time', name='name', value='value'))
35+
self.write_info_line('experiment_name', self.experiment_name)
36+
self.write_info_line('task_name', self.sm_info['name'])
37+
self.write_info_line('task_file_hash', self.sm_info['task_hash'])
38+
self.write_info_line('setup_ID', self.setup_ID)
39+
self.write_info_line('framework_version', self.sm_info['framework_version'])
40+
self.write_info_line('micropython_version', self.sm_info['micropython_version'])
41+
self.write_info_line('subject_ID', self.subject_ID)
42+
self.write_info_line('start_time', datetime.utcnow().isoformat(timespec='milliseconds'))
5343
self.analog_writers = {ID:
5444
Analog_writer(ai['name'], ai['fs'], ai['dtype'], self.file_path)
5545
for ID, ai in self.sm_info['analog_inputs'].items()}
5646

5747
def write_info_line(self, name, value, time=0):
58-
if self.file_type == 'tsv':
59-
name = name.lower().replace(' ', '_')
60-
self.data_file.write(self.tsv_row_str('info', time=time, name=name, value=value))
61-
elif self.file_type == 'txt':
62-
self.data_file.write(f'I {name} : {value}\n')
48+
self.data_file.write(self.tsv_row_str('info', time=time, name=name, value=value))
6349

6450
def tsv_row_str(self, rtype, time='', name='', value=''):
6551
time_str = f'{time/1000:.3f}' if type(time) == int else time
@@ -78,6 +64,7 @@ def copy_task_file(self, data_dir, tasks_dir, dir_name='task_files'):
7864

7965
def close_files(self):
8066
if self.data_file:
67+
self.write_info_line('end_time', self.end_datetime.isoformat(timespec='milliseconds'), self.end_timestamp)
8168
self.data_file.close()
8269
self.data_file = None
8370
self.file_path = None
@@ -91,7 +78,7 @@ def process_data(self, new_data):
9178
if self.data_file:
9279
self.write_to_file(new_data)
9380
if self.print_func:
94-
self.print_func(self.data_to_string(new_data, verbose=True), end='')
81+
self.print_func(self.data_to_string(new_data).replace('\t\t', '\t'), end='')
9582
if self.data_consumers:
9683
for data_consumer in self.data_consumers:
9784
data_consumer.process_data(new_data)
@@ -105,49 +92,27 @@ def write_to_file(self, new_data):
10592
if nd.type == 'A':
10693
self.analog_writers[nd.ID].save_analog_chunk(timestamp=nd.time, data_array=nd.data)
10794

108-
def data_to_string(self, new_data, verbose=False):
95+
def data_to_string(self, new_data):
10996
'''Convert list of data tuples into a string. If verbose=True state and event names are used,
11097
if verbose=False state and event IDs are used.'''
11198
data_string = ''
11299
for nd in new_data:
113-
if verbose or self.file_type == 'txt':
114-
if nd.type == 'D': # State entry or event.
115-
if verbose: # Print state or event name.
116-
data_string += f'D {nd.time} {self.ID2name_fw[nd.ID]}\n'
117-
else: # Print state or event ID.
118-
data_string += f'D {nd.time} {nd.ID}\n'
119-
elif nd.type == 'P': # User print output.
120-
data_string += f'P {nd.time} {nd.data}\n'
121-
elif nd.type == 'V': # Variables
122-
if nd.ID == 'print':
123-
data_string += f'P {nd.time} {nd.data}\n'
124-
elif nd.ID in ('set','get'):
125-
for v_name, v_value in json.loads(nd.data).items():
126-
data_string += f'V {nd.time} {v_name} {v_value}\n'
127-
elif nd.type == '!': # Warning
128-
data_string += f'! {nd.data}\n'
129-
elif nd.type == '!!': # Crash traceback.
130-
error_string = nd.data
131-
if not verbose: # In data files multi-line tracebacks have ! prepended to all lines aid parsing data file.
132-
error_string = '! ' + error_string.replace('\n', '\n! ')
133-
data_string += '\n' + error_string + '\n'
134-
elif self.file_type == 'tsv':
135-
if nd.type == 'D': # State entry or event.
136-
if nd.ID in self.sm_info['states'].values():
137-
data_string += self.tsv_row_str('state', time=nd.time, name=self.ID2name_fw[nd.ID])
138-
else:
139-
data_string += self.tsv_row_str('event', time=nd.time, name=self.ID2name_fw[nd.ID])
140-
elif nd.type == 'P': # User print output.
141-
data_string += self.tsv_row_str('print', time=nd.time, value=nd.data)
142-
elif nd.type == 'V': # Variable.
143-
data_string += self.tsv_row_str('variable', time=nd.time, name=nd.ID, value=nd.data)
144-
elif nd.type == '!': # Warning
145-
data_string += self.tsv_row_str('warning', value=nd.data)
146-
elif nd.type == '!!': # Error
147-
data_string += self.tsv_row_str('error', value=nd.data.replace('\n','|').replace('\r','|'))
148-
elif nd.type == 'S': # Framework stop.
149-
self.write_info_line('end_time', datetime.utcnow().isoformat(timespec='milliseconds'), time=nd.time)
150-
self.end_time = nd.time # Used by run_experiment_tab for printing summary variables to file.
100+
if nd.type == 'D': # State entry or event.
101+
if nd.ID in self.sm_info['states'].values():
102+
data_string += self.tsv_row_str('state', time=nd.time, name=self.ID2name_fw[nd.ID])
103+
else:
104+
data_string += self.tsv_row_str('event', time=nd.time, name=self.ID2name_fw[nd.ID])
105+
elif nd.type == 'P': # User print output.
106+
data_string += self.tsv_row_str('print', time=nd.time, value=nd.data)
107+
elif nd.type == 'V': # Variable.
108+
data_string += self.tsv_row_str('variable', time=nd.time, name=nd.ID, value=nd.data)
109+
elif nd.type == '!': # Warning
110+
data_string += self.tsv_row_str('warning', value=nd.data)
111+
elif nd.type == '!!': # Error
112+
data_string += self.tsv_row_str('error', value=nd.data.replace('\n','|').replace('\r','|'))
113+
elif nd.type == 'S': # Framework stop.
114+
self.end_datetime = datetime.utcnow()
115+
self.end_timestamp = nd.time
151116
return data_string
152117

153118

@@ -163,46 +128,29 @@ def __init__(self, name, sampling_rate, data_type, session_filepath):
163128
def open_data_files(self, session_filepath):
164129
ses_path_stem, file_ext = os.path.splitext(session_filepath)
165130
self.path_stem = ses_path_stem + f'_{self.name}'
166-
self.file_type = 'npy' if file_ext[-3:] == 'tsv' else 'pca'
167-
if self.file_type == 'pca':
168-
file_path = self.path_stem + '.pca'
169-
self.pca_file = open(file_path, 'wb')
170-
elif self.file_type == 'npy':
171-
self.t_tempfile_path = self.path_stem + '.time.temp'
172-
self.d_tempfile_path = self.path_stem + f'.data.1{self.data_type}.temp'
173-
self.time_tempfile = open(self.t_tempfile_path, 'wb')
174-
self.data_tempfile = open(self.d_tempfile_path, 'wb')
131+
self.t_tempfile_path = self.path_stem + '.time.temp'
132+
self.d_tempfile_path = self.path_stem + f'.data.1{self.data_type}.temp'
133+
self.time_tempfile = open(self.t_tempfile_path, 'wb')
134+
self.data_tempfile = open(self.d_tempfile_path, 'wb')
175135

176136
def close_files(self):
177137
'''Close data files. Convert temp files to numpy.'''
178-
if self.file_type == 'pca':
179-
self.pca_file.close()
180-
elif self.file_type == 'npy':
181-
self.time_tempfile.close()
182-
self.data_tempfile.close()
183-
with open(self.t_tempfile_path, 'rb') as f:
184-
times = np.frombuffer(f.read(), dtype='float64')
185-
np.save(self.path_stem + '.time.npy', times)
186-
with open(self.d_tempfile_path, 'rb') as f:
187-
data = np.frombuffer(f.read(), dtype=self.data_type)
188-
np.save(self.path_stem + '.data.npy', data)
189-
os.remove(self.t_tempfile_path)
190-
os.remove(self.d_tempfile_path)
138+
self.time_tempfile.close()
139+
self.data_tempfile.close()
140+
with open(self.t_tempfile_path, 'rb') as f:
141+
times = np.frombuffer(f.read(), dtype='float64')
142+
np.save(self.path_stem + '.time.npy', times)
143+
with open(self.d_tempfile_path, 'rb') as f:
144+
data = np.frombuffer(f.read(), dtype=self.data_type)
145+
np.save(self.path_stem + '.data.npy', data)
146+
os.remove(self.t_tempfile_path)
147+
os.remove(self.d_tempfile_path)
191148

192149
def save_analog_chunk(self, timestamp, data_array):
193-
'''Save a chunk of analog data to .pca data file. File is created if not
194-
already open for that analog input.'''
195-
if self.file_type == 'pca':
196-
ms_per_sample = 1000 / self.sampling_rate
197-
for i, x in enumerate(data_array):
198-
t = int(timestamp + i*ms_per_sample)
199-
self.pca_file.write(t.to_bytes(4,'little', signed=True))
200-
self.pca_file.write(x.to_bytes(4,'little', signed=True))
201-
self.pca_file.flush()
202-
elif self.file_type == 'npy':
203-
times = (np.arange(len(data_array), dtype='float64')
204-
/ self.sampling_rate) + timestamp/1000 # Seconds
205-
self.time_tempfile.write(times.tobytes())
206-
self.data_tempfile.write(data_array.tobytes())
207-
self.time_tempfile.flush()
208-
self.data_tempfile.flush()
150+
'''Save a chunk of analog data to .pca data file.'''
151+
times = (np.arange(len(data_array), dtype='float64')
152+
/ self.sampling_rate) + timestamp/1000 # Seconds
153+
self.time_tempfile.write(times.tobytes())
154+
self.data_tempfile.write(data_array.tobytes())
155+
self.time_tempfile.flush()
156+
self.data_tempfile.flush()

com/pycboard.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -474,7 +474,7 @@ def process_data(self):
474474
elif data_type == 'P': # User print.
475475
new_data.append(Datatuple(type=data_type,time=timestamp, data=data_str))
476476
elif data_type == 'V': # Store new variable value in sm_info
477-
op_ID = {'g':'get', 's':'set', 'p':'print'}[data_str[0]]
477+
op_ID = {'g':'get', 's':'set', 'p':'print', 't':'run_start', 'e':'run_end'}[data_str[0]]
478478
var_json = data_str[1:]
479479
var_dict = json.loads(var_json)
480480
self.sm_info['variables'].update(var_dict)

gui/run_experiment_tab.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -440,10 +440,7 @@ def start_task(self):
440440
self.start_time = datetime.now()
441441
ex = self.run_exp_tab.experiment
442442
self.print_to_log('\nStarting experiment.\n')
443-
self.data_logger.open_data_file(ex['data_dir'], ex['name'], self.setup_name, self.subject, "tsv", datetime.now())
444-
if self.subject_variables: # Write variables set pre run to data file.
445-
var_dict = {v_name: eval(v_value) for v_name, v_value, pv in self.variables_set_pre_run}
446-
self.data_logger.write_to_file([Datatuple(type='V',time=0, ID='set', data=json.dumps(var_dict))])
443+
self.data_logger.open_data_file(ex['data_dir'], ex['name'], self.setup_name, self.subject, datetime.now())
447444
self.board.start_framework()
448445
self.start_stop_button.setText('Stop')
449446
self.start_stop_button.setIcon(QtGui.QIcon("gui/icons/stop.svg"))
@@ -478,7 +475,6 @@ def stop_task(self):
478475
if summary_variables:
479476
self.subject_sumr_vars = {v['name']:
480477
self.board.get_variable(v['name']) for v in summary_variables}
481-
self.data_logger.write_to_file([Datatuple(type='V',time=self.data_logger.end_time, ID='get', data=json.dumps(self.subject_sumr_vars))])
482478
# Close data files and disconnect from board.
483479
self.data_logger.close_files()
484480
self.board.close()

gui/run_task_tab.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,7 @@ def start_task(self):
365365
return
366366
subject_ID = self.subject_text.text()
367367
setup_ID = self.board_select.currentText()
368-
self.data_logger.open_data_file(self.data_dir, "run_task", setup_ID, subject_ID, "tsv")
368+
self.data_logger.open_data_file(self.data_dir, "run_task", setup_ID, subject_ID)
369369
self.data_logger.copy_task_file(self.data_dir, self.GUI_main.task_directory, "run_task-task_files")
370370
self.fresh_task = False
371371
self.running = True

pyControl/framework.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from . import timer
44
from . import state_machine as sm
55
from . import hardware as hw
6+
from . import utility as ut
67

78
VERSION = '1.8'
89

@@ -16,7 +17,7 @@ class pyControlError(BaseException): # Exception for pyControl errors.
1617
timer_typ = const(3) # User timer : (time, timer_typ, event_ID)
1718
print_typ = const(4) # User print : (time, print_typ, print_string)
1819
hardw_typ = const(5) # Harware callback : (time, hardw_typ, hardware_ID)
19-
varbl_typ = const(6) # Variable change : (time, varbl_typ, set/get/print byte, json_str)
20+
varbl_typ = const(6) # Variable change : (time, varbl_typ, [s]et/[g]et/[p]rint/s[t]art/[e]nd byte, json_str)
2021
warng_typ = const(7) # Warning : (time, warng_typ, print_string)
2122
stopf_typ = const(8) # Stop framework : (time, stopf_type)
2223

@@ -126,6 +127,7 @@ def run():
126127
data_output_queue.reset()
127128
if not hw.initialised: hw.initialise()
128129
current_time = 0
130+
ut.print_variables(when='t')
129131
hw.run_start()
130132
start_time = pyb.millis()
131133
clock.init(freq=1000)
@@ -168,6 +170,7 @@ def run():
168170
elif data_output_queue.available:
169171
output_data(data_output_queue.get())
170172
# Post run
173+
ut.print_variables(when='e')
171174
data_output_queue.put((current_time, stopf_typ))
172175
usb_serial.setinterrupt(3) # Enable 'ctrl+c' on serial raising KeyboardInterrupt.
173176
clock.deinit()

pyControl/hardware.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from . import timer
44
from . import framework as fw
55
from . import state_machine as sm
6-
from .utility import randint, warning, print_variables
6+
from .utility import randint, warning
77

88
# Ring buffer -----------------------------------------------------------------
99

@@ -69,7 +69,6 @@ def run_start():
6969
# Called at start of each framework run.
7070
interrupt_queue.reset()
7171
stream_data_queue.reset()
72-
print_variables()
7372
for IO_object in IO_dict.values():
7473
IO_object._run_start()
7574

pyControl/utility.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,15 @@ def print(print_string):
5454
if fw.data_output:
5555
fw.data_output_queue.put((fw.current_time, fw.print_typ, str(print_string)))
5656

57-
def print_variables(variables='all'):
57+
def print_variables(variables='all', when='p'):
5858
# Print specified variables to data log as a json string.
5959
if variables=='all':
6060
var_dict = {k:var for k,var in v.__dict__.items() if not hasattr(var, '__init__')}
6161
elif sys.implementation.version >= (1,12):
6262
var_dict = OrderedDict([(k, getattr(v,k)) for k in variables if not hasattr(getattr(v,k), '__init__')])
6363
else: # old versions of ujson don't support OrdereDict.
6464
var_dict = {k:getattr(v,k) for k in variables if not hasattr(getattr(v,k), '__init__')}
65-
fw.data_output_queue.put((fw.current_time, fw.varbl_typ, 'p', ujson.dumps(var_dict)))
65+
fw.data_output_queue.put((fw.current_time, fw.varbl_typ, when, ujson.dumps(var_dict)))
6666

6767
def warning(message):
6868
# Print a warning message to the log.

0 commit comments

Comments
 (0)