359 lines
14 KiB
Python
359 lines
14 KiB
Python
# -*- coding: utf-8 -*-
|
|
"""
|
|
sequence_fileformat
|
|
______________________
|
|
|
|
Version: 1.0
|
|
Authors: Davis Garrad (Paul Scherrer Institute, CH)
|
|
______________________
|
|
|
|
A basic system to generate Tecmag Sequence (.tps) files for reading by TNMR and sequence generation.
|
|
"""
|
|
|
|
# HEADER SYNTAX
|
|
# TODO: See if I can't get my name into this...
|
|
# PSEQ1.001.18 BIN
|
|
# # 0*3 [filename]
|
|
# 0x4 0*3 0*4
|
|
# # 0*3 [user]
|
|
# 0x8 0*3 (2 numbers)Ah0000 (timestamp) (4 bytes, in LSB first - little endian, then 4 zeros)
|
|
# 0x12 0*3 (device control 2)
|
|
# (small number, 0x2, 0x4) 0*3 (same as previous) 0*15 0x1
|
|
# 0*11
|
|
# 0x1 0*3 [space]
|
|
# # 0*3 Name:
|
|
# # 0*3 [name]
|
|
# 0*56
|
|
# # 0*3 [col 1 name]
|
|
# 0*56
|
|
# # 0*3 [col 2 name]
|
|
# ...
|
|
# 0*56
|
|
#
|
|
|
|
# DELAY SYNTAX
|
|
# (same number as above) 0*3 0x1 0*7 0x1 0*3 0x1 (don't ask)
|
|
# 0*11
|
|
# # 0*3 [default]
|
|
# # * 0*3 Delay
|
|
# # * 0*3 Delay
|
|
# 0*56
|
|
# # 0*3 [val 1]
|
|
# 0*56
|
|
# # 0*3 [val 2]
|
|
# ...
|
|
# 0*56
|
|
|
|
# EVENT SYNTAX
|
|
#
|
|
# (this part specifies the section)
|
|
# 0x2 0*3 [some number] 1 [X] [Y]
|
|
# 0x1 0*3 [Z] [W] 0*2 0x1
|
|
#
|
|
# (this part specifies the subsection)
|
|
# 0x11
|
|
# # 0*3 [default value]
|
|
# # 0*3 [event type (submenu)]
|
|
# # 0*3 [event type (submenu)]
|
|
# 0*8
|
|
# [ (# 0*3 [table 1D name] 0x1) if table exists, else (nothing) ]
|
|
# 0*8
|
|
# [ (# 0*3 [table 2D name] 0x1) if table exists, else (nothing) ]
|
|
# 0*8
|
|
# ...
|
|
# 0*8
|
|
# 0*11
|
|
# # 0*3 [value]
|
|
# 0*56
|
|
# 0*3 [value 2]
|
|
# 0*56
|
|
# ...
|
|
#
|
|
# (this part specifies another subsection) (only for CTRL section)
|
|
# 0x2
|
|
# 0*3 0xd (seemingly random identifiers)
|
|
# 0*3 0x18
|
|
# 0*3 0xd
|
|
# 0*3 0x1
|
|
# 0x11
|
|
# # 0*3 [value]
|
|
# # 0*3 [event type (submenu)]
|
|
# # 0*3 [event type (submenu)]
|
|
# 0*56
|
|
# # 0*3 [value]
|
|
# 0*56
|
|
|
|
# TABLE SPEC SYNTAX
|
|
# 0*56
|
|
# 0*56
|
|
# 0*11
|
|
# 0*8
|
|
# [num_tables]
|
|
# 0*3
|
|
# # 0*3 [table_1_name]
|
|
# # 0*3 [table values] 0*16 [type] (normally just HP)
|
|
# 0*2 [starting offset (in hex) - 1 is default] 0*3 0x4 8)7 0x1 0*31 0x5
|
|
# 0*4
|
|
# ...
|
|
# 0*10 0x0
|
|
|
|
Z = '\x00' # for my sanity
|
|
|
|
event_codes = { # These hold codes that TNMR uses to identify different events. I'm not sure as to what the black magic is governing them, but I took them from files TNMR generated.
|
|
'F1_Ampl': ('\x1f', 'P', 'E', '3', 'R', '\x08'), # F1
|
|
'F1_Ph': ('\x05', 'M', 'E', 'H', 'P', '\x02'),
|
|
'F1_PhMod': ('\x17', 'P', 'E', '3', 'P', '\x08'),
|
|
'F1_HOP': ('\x0d', 'M', 'E', 'X', 'T', '\x01'),
|
|
'F1_UnBlank': ('\x17', 'M', 'E', 'X', 'T', '\x01'),
|
|
'F1_PhRst': ('\x00', 'M', 'E', 'X', 'T', '\x01'), # RST
|
|
'F1_ExtTrig': ('\x09', 'M', 'E', 'X', 'T', '\x01'),
|
|
'Scope_Trig': ('\x14', 'M', 'E', 'X', 'T', '\x01'), # CTRL (first)
|
|
'Loop': ('\x0d', '\x18', '\x0d', '\x01'),
|
|
'CBranch': ('\x0e', '\x00', '\x0e', '\x01'),
|
|
'CTest': ('\x0f', '\x00', '\x0f', '\x01'),
|
|
'Ext_Trig': ('\x10', '\x00', '\x10', '\x01'),
|
|
'RT_Update': ('\x11', '\x00', '\x11', '\x01'),
|
|
'Acq': ('\x17', 'Q', 'A', 'C', 'A', '\x18'), # ACQ
|
|
'Acq_phase': ('\x03', 'Q', 'A', 'H', 'P', '\x02'),
|
|
'Rx_Blank': ('\x0f', 'M', 'E', 'X', 'T', '\x01'), # RX
|
|
}
|
|
|
|
event_types = list(event_codes.keys())
|
|
event_defaults = { # default values of each of the events, if nothing else is set.
|
|
'F1_Ampl': 0, # F1
|
|
'F1_PhMod': -1,
|
|
'F1_Ph': -1,
|
|
'F1_UnBlank': 0,
|
|
'F1_HOP': 0,
|
|
'F1_PhRst': 0, # RST
|
|
'F1_ExtTrig': 0,
|
|
'Ext_Trig': 0, # CTRL
|
|
'Loop': 0,
|
|
'Scope_Trig': 0, # special CTRL (first)
|
|
'CBranch': 0,
|
|
'CTest': 0,
|
|
'RT_Update': 0,
|
|
'Acq': 0, # ACQ
|
|
'Acq_phase': -1,
|
|
'Rx_Blank': 0, # RX
|
|
}
|
|
|
|
|
|
def fm(s, spacing=3):
|
|
'''formats a string with its length, then 3 zeroes, then the str'''
|
|
a = f'{chr(len(s))}{Z*spacing}{s}'
|
|
return a
|
|
|
|
def get_info_header(filename, author, col_names, tuning_number, binary_name='PSEQ1.001.18 BIN'):
|
|
'''Creates a string which should be written (in binary format) to the top of a TNMR sequence file (.tps)
|
|
|
|
Parameters
|
|
----------
|
|
filename: str, the filename of the sequence file which will be written to (doesn't need to exist, just for TNMR to read)
|
|
author: str, the author of the sequence
|
|
col_names: list(str), a list of all the column names that will be in use in the sequence
|
|
tuning_number: str, the number of columns+1, in UTF-8 (i.e., it should be a single character. For 2 columns, tuning_number = '\x02')
|
|
binary_name: str, a code specifying which version of TNMR Sequence Editor generated this sequence. Best kept its default, PSEQ1.001.18 BIN
|
|
'''
|
|
|
|
|
|
headerstr = ''
|
|
headerstr += 'PSEQ1.001.18 BIN'
|
|
headerstr += fm(filename)
|
|
headerstr += f'\x04{Z*3}{Z*4}'
|
|
headerstr += fm(author)
|
|
headerstr += f'\x08{Z*3}\xda\xf1\x50\x00{Z*4}' # TODO: Timestamp, but right now it doesn't really matter.
|
|
headerstr += f'\x12{Z*3}'
|
|
headerstr += f'{tuning_number}{Z*3}{tuning_number}{Z*15}\x01{Z*11}'
|
|
headerstr += fm(' ')
|
|
headerstr += fm('Name:')
|
|
headerstr += fm('Name:')
|
|
headerstr += Z*56
|
|
for i in col_names:
|
|
headerstr += fm(i)
|
|
headerstr += Z*56
|
|
return headerstr
|
|
|
|
def get_delay_header(col_delays, tuning_number):
|
|
'''Creates a string which should be written (in binary format) after the info header (see get_info_header()) of a TNMR sequence file (.tps). Specifies the delays of each column.
|
|
|
|
Parameters
|
|
----------
|
|
col_delays: list(float), a list of all the column delays that will be in use in the sequence. Ordered. In microseconds.
|
|
tuning_number: str, the number of columns+1, in UTF-8 (i.e., it should be a single character. For 2 columns, tuning_number = '\x02')
|
|
'''
|
|
headerstr = ''
|
|
headerstr += f'{tuning_number}{Z*3}\x01{Z*7}\x01{Z*3}\x01'
|
|
headerstr += Z*11
|
|
headerstr += fm('1u')
|
|
headerstr += fm('Delay')
|
|
headerstr += fm('Delay')
|
|
headerstr += Z*56
|
|
for i in col_delays:
|
|
headerstr += fm(str(i))
|
|
headerstr += Z*56
|
|
return headerstr
|
|
|
|
def get_event_header(event_type, vals, tables, table_reg, tuning_number, col_delays, num_acq_points):
|
|
'''Generates the file information for the events section. This should come after the delay header (see get_delay_header())
|
|
|
|
Params
|
|
------
|
|
event_type: str describing the event (an element of event_types)
|
|
vals: an array of strs to be written.
|
|
tables: an array of dictionaries of table names to be written in format [ { '1D': None/str } (for col0), { '1D': None/str } (for col1) ... ]
|
|
table_reg: a dictionary of the table registry
|
|
tuning_number: str, the number of columns+1, in UTF-8 (i.e., it should be a single character. For 2 columns, tuning_number = '\x02')
|
|
col_delays: the array of column delays
|
|
'''
|
|
codes = event_codes[event_type]
|
|
headerstr = ''
|
|
if(len(codes) == 6): # regular header
|
|
headerstr += f'{tuning_number}{Z*3}{codes[0]}1{codes[1]}{codes[2]}'
|
|
headerstr += f'{codes[5]}{Z*3}{codes[3]}{codes[4]}{Z*2}\x01'
|
|
elif(len(codes) == 4): # extension
|
|
headerstr += f'{tuning_number}{Z*3}{codes[0]}{Z*3}{codes[1]}{Z*3}{codes[2]}{Z*3}{codes[3]}'
|
|
else:
|
|
print('PANIC')
|
|
raise Exception
|
|
|
|
headerstr += Z*11
|
|
headerstr += fm(str(event_defaults[event_type]))
|
|
headerstr += fm(event_type)
|
|
headerstr += fm(event_type)
|
|
headerstr += Z*56
|
|
for i in range(len(vals)):
|
|
headerstr += fm(str(vals[i]))
|
|
if(event_type == 'Acq' and str(vals[i]) == '1'):
|
|
acq_points = num_acq_points
|
|
acq_time = col_delays[i]
|
|
if('u' in acq_time):
|
|
acq_time = float(acq_time.strip()[:-1])
|
|
elif('m' in acq_time):
|
|
acq_time = 1000*float(acq_time.strip()[:-1])
|
|
elif('n' in acq_time):
|
|
acq_time = 0.001*float(acq_time.strip()[:-1])
|
|
|
|
dwell_us = acq_time / acq_points
|
|
dwell = f'{dwell_us*1000}n'
|
|
|
|
# spectral width (here "sweep") and dwell time are linked by the relation
|
|
# DT = (2*SW)^(-1)
|
|
# Therefore, SW = 1/(DT*2)
|
|
|
|
freq = 1/(dwell_us/1e6) / 2 # put it in Hz.
|
|
sweep = f'{freq}Hz'
|
|
filtr = f'{freq}Hz'
|
|
|
|
|
|
headerstr += Z*52
|
|
headerstr += f'\x01{Z*3}' + fm(str(acq_points))
|
|
headerstr += fm(str(sweep))
|
|
headerstr += fm(str(filtr))
|
|
headerstr += fm(str(dwell))
|
|
headerstr += fm(str(col_delays[i]))
|
|
headerstr += f'\x00{Z*5}' # This is to link to dashboard... (set to zero or else it will just go to dashboard default)
|
|
else:
|
|
if not(tables[i] in list(table_reg.keys())):
|
|
headerstr += Z*56
|
|
else:
|
|
headerstr += Z*8 + fm(tables[i]) + '\x01' # 1D
|
|
headerstr += Z*43
|
|
|
|
return headerstr
|
|
|
|
def get_table_spec(tables):
|
|
'''Generates the file information for a set of tables. This should go after the event section (see get_event_header())
|
|
|
|
Parameters
|
|
----------
|
|
tables: a dictionary of form { [table_name]: { 'values': '1 2 3', 'typestr': 'HP', 'start': 1 } }. typestr should be HP for phase, and you'll have to figure out what the other codes are. Start should almost always be 1.
|
|
'''
|
|
specstr = ''
|
|
specstr += Z*56
|
|
specstr += Z*56
|
|
specstr += Z*11
|
|
specstr += Z*5
|
|
|
|
if(len(list(tables.keys())) > 0):
|
|
specstr += Z*3
|
|
specstr += chr(len(list(tables.keys())))
|
|
for t in list(tables.keys()):
|
|
specstr += Z*3
|
|
specstr += fm(t)
|
|
specstr += fm(tables[t]['values']) + Z*16 + tables[t]['typestr']
|
|
specstr += Z*2 + chr(tables[t]['start']) + Z*3 + chr(0x04) + Z*7 + chr(0x01) + Z*31 + chr(0x05) + Z*4
|
|
specstr += Z*10+Z
|
|
return specstr
|
|
|
|
def generate_default_sequence(col_names, col_delays):
|
|
'''Generates a dictionary for use in create_sequence_file, and populates it with all the default values as specified by event_defaults and delays.
|
|
|
|
Parameters
|
|
----------
|
|
col_names: an iterable of each of the column titles.
|
|
col_delays: an iterable of each of the column delay values. Should match the ordering of col_names
|
|
|
|
Returns
|
|
-------
|
|
a data dictionary in the form required by create_sequence_file.
|
|
'''
|
|
full_dict = { 'columns': {}, 'tables': {} }
|
|
for c, delay in zip(col_names, col_delays):
|
|
sub_dict = {}
|
|
for e in event_types:
|
|
sub_dict[e] = { 'value': str(event_defaults[e]), 'table': '' }
|
|
sub_dict['Delay'] = delay
|
|
full_dict['columns'][c] = sub_dict.copy()
|
|
full_dict['num_acq_points'] = 1024
|
|
return full_dict
|
|
|
|
def create_sequence_file(filename, data, author='NA'):
|
|
'''Generates a Tecmag sequence file (.tps) for use in Tecmag NMR (TNMR). Combines header, delay, event, and table information to create a fully-readable file for TNMR.
|
|
|
|
Parameters
|
|
----------
|
|
filename: str, where to write this.
|
|
data: a dictionary in the form { 'columns': {
|
|
[column_name_0]: {
|
|
'F1_Ampl': [value], ..., 'Rx_Blank': [value], 'Delay': [value] }
|
|
, ... },
|
|
'tables': {
|
|
'table_1': {...}, ... } ,
|
|
'num_acq_points': [integer]
|
|
}. Any column with Acq enabled should also have a field "num_acq_points". If any sub-entries are empty, they will be given default values (requires that all event_types are present). See event_types and event_defaults. This is best generated using generate_default_sequence and then modifying the given sequence.
|
|
author [optional]: str to describe the file creator.
|
|
'''
|
|
content = ''
|
|
|
|
column_names = list(data['columns'].keys())
|
|
tuning_number = (len(column_names)+1).to_bytes().decode('utf-8')
|
|
column_delays = []
|
|
for i in column_names:
|
|
column_delays += [ str(data['columns'][i]['Delay']) ]
|
|
|
|
content += get_info_header(filename, author, column_names, tuning_number)
|
|
content += get_delay_header(column_delays, tuning_number)
|
|
|
|
for evnt in event_types:
|
|
evnt_data_values = []
|
|
evnt_data_tables = []
|
|
for i in column_names:
|
|
evnt_data_values += [ str(data['columns'][i][evnt]['value']) ]
|
|
evnt_data_tables += [ data['columns'][i][evnt]['table'] ]
|
|
content += get_event_header(evnt, evnt_data_values, evnt_data_tables, data['tables'], tuning_number, column_delays, int(data['num_acq_points']))
|
|
|
|
content += ' '
|
|
content += get_table_spec(data['tables'])
|
|
|
|
with open(f'{filename}' if '.tps' in filename[-4:] else f'{filename}.tps', 'bw') as file:
|
|
bs = []
|
|
for char in content:
|
|
bs += [ord(char)]
|
|
file.write(bytes(bs))
|
|
file.close()
|
|
|
|
|
|
|
|
|