Files
frappy/frappy_psi/tnmr/sequence_fileformat.py

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()