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