Fixed hardware issue?

This commit is contained in:
2025-06-24 11:35:32 +02:00
parent 2fce39c381
commit 388748c995
4 changed files with 216 additions and 157 deletions

View File

@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
TNMR_DG_Extension OTFModule
______________________ ______________________
Version: 1.0 Version: 1.0
Authors: Davis Garrad (Paul Scherrer Institute, CH) Authors: Davis Garrad (Paul Scherrer Institute, CH)
______________________ ______________________
frappy-based module for generating and running pulse sequences on TNMR (Tecmag). frappy-based module for generating and running pulse sequences on TNMR (Tecmag). The On-The-Fly module is meant to represent a SECoP node frappy-side, so it really just interacts with the TNMR software.
""" """
#On-the-fly! #On-the-fly!
@@ -24,33 +25,36 @@ import numpy as np
import threading import threading
import time import time
import os import os
import traceback
class ProgrammedSequence(fc.Readable): class ProgrammedSequence(fc.Readable):
"""An NMR device being driven by an instance of TNMR. Requires that an instance of TNMR is opened before creation. """An NMR device being driven by an instance of TNMR. Requires that an instance of TNMR is opened before creation.
Instance Attributes Use
------------------- ---
(parameter) title: a title which will be embedded to the sequence files. Use this for identification. Generating a pulse sequence is as simple as setting the sequence_data parameter to be a list of sequence dictionaries, in the order you want them to be executed. The next step is to set the acquisition parameters (see below), including title, acquisition_time, pre_acquisition_time, post_acquisition_time, ringdown_time, and acq_phase_cycle.
(parameter) sequence_data: an array of structs: keys are { 'pulse_width': (width of pulse in us), 'pulse_height': (amplitude of pulse in a.u.), 'relaxation_time': (relaxation time in us), 'phase_cycle': (a str denoting a phase cycle, e.g., '0 1 2 3') }
(parameter) value: an array of complexes representing the TNMR data return (technically inherited from Readable)
(parameter) acquisition_time: float (usecs) which describes the length of acquisition
(parameter) ringdown_time: float (usecs) which describes the length of ringdown
(parameter) pre_acquisition_time: float (usecs) which describes the length of time to wait after ringdown finishes (1u is okay)
(parameter) post_acquisition_time: float (ms) which describes the length of time to wait after finishing acquisition
(parameter) acq_phase_cycle: str, the phase cycle to run on acquisition (eg., '0 1 1 2', '0 1 2 3', '1 1 2 2 0 0 3 3 1 2 3 4', ...)
Methods Once all the parameters are set, you can call the frappy command and member function compile_and_run() (NOT the private member function __compile_and_run()).
-------
(cmd) add_pulse: adds a pulse to the end of the sequence. Attributes
Arguments: ----------
- pulse_width: pulse width to add to sequence (usec) value: an array of complexes representing the TNMR data return (technically inherited from Readable)
- pulse_height: pulse height to add to sequence (a.u.) sequence_data: an array of structs: keys are { 'pulse_width': (width of pulse in us), 'pulse_height': (amplitude of pulse in a.u.), 'delay_time': (delay time in us), 'phase_cycle': (a str denoting a phase cycle, e.g., '0 1 2 3') }
- relaxation_time: relaxation time to add to sequence (usec)
- phase_cycle: the phase cycle to run for this pulse (eg., '0 1 1 2', '0 1 2 3', '1 1 2 2 0 0 3 3 1 2 3 4', ...) Acquisition Parameters
(cmd) pop_pulse: removes the last pulse from the sequence ----------------------
(cmd) compile_sequence: finishes building the sequence, saves it and its configuration to files (timestamped), and sets the system ready to run title: a title which will be embedded to the sequence files. Use this for identification.
(cmd) run: if compiled, starts data acquisition on TNMR (async) acquisition_time: float (usecs) which describes the length of acquisition
(cmd) compile and run: you can guess ringdown_time: float (usecs) which describes the length of ringdown
pre_acquisition_time: float (usecs) which describes the length of time to wait after ringdown finishes (1u is okay)
post_acquisition_time: float (ms) which describes the length of time to wait after finishing acquisition
acq_phase_cycle: str, the phase cycle to run on acquisition (eg., '0 1 1 2', '0 1 2 3', '1 1 2 2 0 0 3 3 1 2 3 4', ...)
num_scans: int (ct), the number of 1D scans to take per sequence
obs_freq: float (MHz), the NMR frequency
Commands
--------
compile and run: finishes building the sequence, saves it and its configuration to files (timestamped), and starts data acquisition on TNMR (async)
Inherited Attributes Inherited Attributes
-------------------- --------------------
@@ -75,17 +79,11 @@ class ProgrammedSequence(fc.Readable):
title = fc.Parameter('title', fc.StringType(), default='Sequence', readonly=False) title = fc.Parameter('title', fc.StringType(), default='Sequence', readonly=False)
sequence_data = fc.Parameter('sequence_config', fc.ArrayOf(fc.StructOf(pulse_width=fc.FloatRange(unit='u'), sequence_data = fc.Parameter('sequence_config', fc.ArrayOf(fc.StructOf(pulse_width=fc.FloatRange(unit='u'),
pulse_height=fc.FloatRange(), pulse_height=fc.FloatRange(),
relaxation_time=fc.FloatRange(unit='u'), delay_time=fc.FloatRange(unit='u'),
phase_cycle=fc.StringType())), default=[], readonly=False) phase_cycle=fc.StringType())), default=[], readonly=False)
# sequence edit
#pulse_width = fc.Parameter('pulse_width', fc.FloatRange(unit='u'), readonly=False, group='pulse_editor', default=5)
#pulse_height = fc.Parameter('pulse_height', fc.FloatRange(), readonly=False, group='pulse_editor', default=40)
#relaxation_time = fc.Parameter('relaxation_time', fc.FloatRange(unit='u', min=0.1), readonly=False, group='pulse_editor', default=50)
#phase_cycle = fc.Parameter('phase_cycle', fc.StringType(), readonly=False, group='pulse_editor', default='')
# final details # final details
acquisition_time = fc.Parameter('acquisition_time', fc.FloatRange(unit='u'), readonly=False, group='sequence_editor', default=204.8) # this is a limit set by the dwell limit and number of acquisition points (1024, TODO: Make this adjustable) acquisition_time = fc.Parameter('acquisition_time', fc.FloatRange(unit='u'), readonly=False, group='sequence_editor', default=204.8) # this is a limit set by the dwell limit and number of acquisition points
ringdown_time = fc.Parameter('ringdown_time', fc.FloatRange(unit='u'), readonly=False, group='sequence_editor', default=1) ringdown_time = fc.Parameter('ringdown_time', fc.FloatRange(unit='u'), readonly=False, group='sequence_editor', default=1)
pre_acquisition_time = fc.Parameter('pre_acquisition_time', fc.FloatRange(unit='u'), readonly=False, group='sequence_editor', default=1) pre_acquisition_time = fc.Parameter('pre_acquisition_time', fc.FloatRange(unit='u'), readonly=False, group='sequence_editor', default=1)
post_acquisition_time = fc.Parameter('post_acquisition_time', fc.FloatRange(unit='m'), readonly=False, group='sequence_editor', default=500) post_acquisition_time = fc.Parameter('post_acquisition_time', fc.FloatRange(unit='m'), readonly=False, group='sequence_editor', default=500)
@@ -93,10 +91,17 @@ class ProgrammedSequence(fc.Readable):
num_scans = fc.Parameter('num_scans', fc.IntRange(), readonly=False, group='sequence_editor', default=16) num_scans = fc.Parameter('num_scans', fc.IntRange(), readonly=False, group='sequence_editor', default=16)
obs_freq = fc.Parameter('obs_freq', fc.FloatRange(unit='MHz'), readonly=False, group='sequence_editor', default=213.16) obs_freq = fc.Parameter('obs_freq', fc.FloatRange(unit='MHz'), readonly=False, group='sequence_editor', default=213.16)
compiled_parameters = {} # so that we can store the values of parameters only when compiling, effectively giving us an instance of each parameter loaded into TNMR, as well as "targets" (those above)
inited = False inited = False
### SETUP ### SETUP
def tnmr(self): def tnmr(self):
'''Creates a new instance or retrieves a previously-made instance of the TNMR API wrapper.
Returns
-------
an instance of the TNMR API wrapper.
'''
if not(self.inited): if not(self.inited):
try: try:
self.ntnmr = te.TNMR() self.ntnmr = te.TNMR()
@@ -110,33 +115,14 @@ class ProgrammedSequence(fc.Readable):
def initialReads(self): def initialReads(self):
pass pass
### COMMANDS
#@fc.Command(description="Add Pulse", group='pulse_editor')
#def add_pulse(self):
# if(self.status == ('PREPARED', 'compiled')):
# self.status = ('IDLE', 'ok - uncompiled')
# data = list(self.sequence_data) # should be a tuple when it comes out of ArrayOf __call__, so make it mutable
# data += [ { 'pulse_width': self.pulse_width, 'pulse_height': self.pulse_height, 'relaxation_time': self.relaxation_time, 'phase_cycle': self.phase_cycle } ]
# self.sequence_data = data
#@fc.Command(description="Pop Pulse", group='pulse_editor')
#def pop_pulse(self):
# if(self.status == ('PREPARED', 'compiled')):
# self.status = ('IDLE', 'ok - uncompiled')
# data = list(self.sequence_data) # should be a tuple when it comes out of ArrayOf __call__, so make it mutable
# data = data[:-1] # chop off the tail
# self.sequence_data = data
#@fc.Command(description="Compile", group='sequence_editor')
#def compile_sequence(self):
# threading.Thread(target=lambda s=self: s.__compile_sequence()).start()
#@fc.Command(description="Run")
#def run(self):
# threading.Thread(target=lambda s=self: s.__zero_go()).start()
@fc.Command(description="Compile & Run", argument={'type': 'bool'}) @fc.Command(description="Compile & Run", argument={'type': 'bool'})
def compile_and_run(self, thread=True): def compile_and_run(self, thread=True):
'''Compiles and runs the currently loaded sequence (in sequence_data), populating this instance's value member with the results.
Parameters
----------
thread: bool, determines if a new thread is created and detached for this process. (default true)
'''
if(thread): if(thread):
threading.Thread(target=lambda s=self: s.__compile_and_run()).start() threading.Thread(target=lambda s=self: s.__compile_and_run()).start()
else: else:
@@ -144,6 +130,10 @@ class ProgrammedSequence(fc.Readable):
@fc.Command(description="Kill") @fc.Command(description="Kill")
def kill(self): def kill(self):
'''Aborts the current scan, if one is running. Else, does nothing'''
self.stop()
def stop(self):
try: try:
self.tnmr().get_instance().Abort self.tnmr().get_instance().Abort
self.status = ('IDLE', 'ok - killed') self.status = ('IDLE', 'ok - killed')
@@ -157,22 +147,6 @@ class ProgrammedSequence(fc.Readable):
self.status = ('IDLE', 'ok - uncompiled') self.status = ('IDLE', 'ok - uncompiled')
return self.read_title() return self.read_title()
#def write_pulse_width(self, t):
# self.pulse_width = t
# return self.read_pulse_width()
#def write_pulse_height(self, t):
# self.pulse_height = t
# return self.read_pulse_height()
#def write_relaxation_time(self, t):
# self.relaxation_time = t
# return self.read_relaxation_time()
#def write_phase_cycle(self, t):
# self.phase_cycle = t
# return self.read_phase_cycle()
def write_acquisition_time(self, t): def write_acquisition_time(self, t):
self.acquisition_time = t self.acquisition_time = t
self.status = ('IDLE', 'ok - uncompiled') self.status = ('IDLE', 'ok - uncompiled')
@@ -218,6 +192,16 @@ class ProgrammedSequence(fc.Readable):
### PRIVATE (Utility) ### PRIVATE (Utility)
def __compile_sequence(self): def __compile_sequence(self):
'''Compiles the sequence loaded in sequence_data.
This involves:
1. creating the sequence table via seq_gen.get_single_pulse_block calls;
2. combining them all;
3. saving this sequence where TNMR can see it, and in a format it can read;
4. taking a copy of all the acquisition parameters (so that if they are changed mid-acquisition, no incorrect information is written to files)
5. telling TNMR to read it (i.e., tnmr().load_sequence()), reloading the dashboard and parameters in the process
5. giving TNMR the correct parameters to populate the new dashboard with
'''
if(self.status[0] != 'BUSY'): if(self.status[0] != 'BUSY'):
self.status = ('BUSY', 'compiling') self.status = ('BUSY', 'compiling')
# first, create the sequence # first, create the sequence
@@ -226,7 +210,7 @@ class ProgrammedSequence(fc.Readable):
for s in self.sequence_data: for s in self.sequence_data:
seq = seq_gen.combine_blocks(seq, seq_gen.get_single_pulse_block(f'pulse_{i}', str(s['pulse_width']) + 'u', seq = seq_gen.combine_blocks(seq, seq_gen.get_single_pulse_block(f'pulse_{i}', str(s['pulse_width']) + 'u',
str(s['pulse_height']), str(s['pulse_height']),
str(s['relaxation_time']) + 'u', str(s['delay_time']) + 'u',
str(s['phase_cycle']))) str(s['phase_cycle'])))
i += 1 i += 1
seq = seq_gen.combine_blocks(seq, seq_gen.get_final_block(str(self.ringdown_time) + 'u', seq = seq_gen.combine_blocks(seq, seq_gen.get_final_block(str(self.ringdown_time) + 'u',
@@ -234,7 +218,7 @@ class ProgrammedSequence(fc.Readable):
str(self.acquisition_time) + 'u', str(self.acquisition_time) + 'u',
str(self.post_acquisition_time) + 'm', str(self.post_acquisition_time) + 'm',
str(self.acq_phase_cycle))) str(self.acq_phase_cycle)))
# then, save the thing # then, save the thing
filepath = os.getcwd() filepath = os.getcwd()
filename = self.title + f'_{time.time()}' filename = self.title + f'_{time.time()}'
@@ -246,30 +230,49 @@ class ProgrammedSequence(fc.Readable):
'Scans 1D': self.read_num_scans(), 'Scans 1D': self.read_num_scans(),
} }
self.compiled_parameters['ringdown_time'] = self.ringdown_time
self.compiled_parameters['pre_acquisition_time'] = self.pre_acquisition_time
self.compiled_parameters['acquisition_time'] = self.acquisition_time
self.compiled_parameters['post_acquisition_time'] = self.post_acquisition_time
self.compiled_parameters['acq_phase_cycle'] = self.acq_phase_cycle
self.compiled_parameters['num_scans'] = self.read_num_scans()
self.compiled_parameters['obs_freq'] = self.read_obs_freq()
# then, load the thing into TNMR # then, load the thing into TNMR
self.tnmr().load_sequence(filename) self.tnmr().load_sequence(filename)
time.sleep(1.0) # hardware module issue???
# load some parameters back to TNMR # load some parameters back to TNMR
for key, val in dashboard_params.items(): for key, val in dashboard_params.items():
self.tnmr().set_nmrparameter(key, val) self.tnmr().set_nmrparameter(key, val)
time.sleep(0.5)
# finally, let ourselves know we're ready # finally, let ourselves know we're ready
self.status = ('PREPARED', 'compiled') self.status = ('PREPARED', 'compiled')
else:
traceback.print_exc()
def __zero_go(self): def __zero_go(self):
'''Tells TNMR to acquire data. Only call after __compile_sequence().'''
if(self.status[0] != 'BUSY'): if(self.status[0] != 'BUSY'):
self.status = ('BUSY', 'acquiring') self.status = ('BUSY', 'acquiring')
self.tnmr().ZeroGo(lock=True, interval=0.5) self.tnmr().ZeroGo(lock=True, interval=0.5)
newvals = {} newvals = {}
newvals['reals'] = self.tnmr().get_data()[0] newvals['reals'] = self.tnmr().get_data()[0]
newvals['imags'] = self.tnmr().get_data()[1] newvals['imags'] = self.tnmr().get_data()[1]
newvals['t'] = self.tnmr().get_data_times() newvals['t'] = [ self.compiled_parameters['acquisition_time'] * i/self.compiled_parameters['num_scans'] for i in range(0, self.compiled_parameters['num_scans']) ]
self.value = newvals self.value = newvals
self.status = ('PREPARED', 'compiled') self.status = ('PREPARED', 'compiled')
def __compile_and_run(self, thread=True, recurse=True): def __compile_and_run(self, thread=True):
self.tnmr().reset_NTNMR_instance() '''Compiles and runs the currently-loaded sequence
Parameters
----------
thread: bool, determines if this should open a child thread and detach the process
'''
self.__compile_sequence() self.__compile_sequence()
time.sleep(0.5)
self.__zero_go() self.__zero_go()

View File

@@ -1,3 +1,15 @@
# -*- 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 # HEADER SYNTAX
# TODO: See if I can't get my name into this... # TODO: See if I can't get my name into this...
# PSEQ1.001.18 BIN # PSEQ1.001.18 BIN
@@ -87,7 +99,7 @@
Z = '\x00' # for my sanity Z = '\x00' # for my sanity
event_codes = { 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_Ampl': ('\x1f', 'P', 'E', '3', 'R', '\x08'), # F1
'F1_Ph': ('\x05', 'M', 'E', 'H', 'P', '\x02'), 'F1_Ph': ('\x05', 'M', 'E', 'H', 'P', '\x02'),
'F1_PhMod': ('\x17', 'P', 'E', '3', 'P', '\x08'), 'F1_PhMod': ('\x17', 'P', 'E', '3', 'P', '\x08'),
@@ -107,7 +119,8 @@ event_codes = {
} }
event_types = list(event_codes.keys()) event_types = list(event_codes.keys())
event_defaults = { 'F1_Ampl': 0, # F1 event_defaults = { # default values of each of the events, if nothing else is set.
'F1_Ampl': 0, # F1
'F1_PhMod': -1, 'F1_PhMod': -1,
'F1_Ph': -1, 'F1_Ph': -1,
'F1_UnBlank': 0, 'F1_UnBlank': 0,
@@ -132,6 +145,18 @@ def fm(s, spacing=3):
return a return a
def get_info_header(filename, author, col_names, tuning_number, binary_name='PSEQ1.001.18 BIN'): 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 = ''
headerstr += 'PSEQ1.001.18 BIN' headerstr += 'PSEQ1.001.18 BIN'
headerstr += fm(filename) headerstr += fm(filename)
@@ -150,6 +175,13 @@ def get_info_header(filename, author, col_names, tuning_number, binary_name='PSE
return headerstr return headerstr
def get_delay_header(col_delays, tuning_number): 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 = ''
headerstr += f'{tuning_number}{Z*3}\x01{Z*7}\x01{Z*3}\x01' headerstr += f'{tuning_number}{Z*3}\x01{Z*7}\x01{Z*3}\x01'
headerstr += Z*11 headerstr += Z*11
@@ -163,15 +195,15 @@ def get_delay_header(col_delays, tuning_number):
return headerstr return headerstr
def get_event_header(event_type, vals, tables, table_reg, tuning_number, col_delays): def get_event_header(event_type, vals, tables, table_reg, tuning_number, col_delays):
'''Generates the file information for the events section. '''Generates the file information for the events section. This should come after the delay header (see get_delay_header())
Params Params
------ ------
event_type: str describing the event event_type: str describing the event (an element of event_types)
vals: an array of strs to be written. 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) ... ] 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 table_reg: a dictionary of the table registry
tuning_number: the number of columns, minus 1 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 col_delays: the array of column delays
''' '''
codes = event_codes[event_type] codes = event_codes[event_type]
@@ -208,9 +240,6 @@ def get_event_header(event_type, vals, tables, table_reg, tuning_number, col_del
sweep = f'{freq}Hz' sweep = f'{freq}Hz'
filtr = f'{freq}Hz' filtr = f'{freq}Hz'
#sweep = '2500000.0Hz'
#filtr = '2500000.0Hz'
#dwell = '400.0n' # hard limit apparently...
headerstr += Z*52 headerstr += Z*52
headerstr += f'\x01{Z*3}' + fm(str(acq_points)) headerstr += f'\x01{Z*3}' + fm(str(acq_points))
@@ -218,7 +247,7 @@ def get_event_header(event_type, vals, tables, table_reg, tuning_number, col_del
headerstr += fm(str(filtr)) headerstr += fm(str(filtr))
headerstr += fm(str(dwell)) headerstr += fm(str(dwell))
headerstr += fm(str(col_delays[i])) headerstr += fm(str(col_delays[i]))
headerstr += f'\x00{Z*5}' # I believe this is to link to dashboard... (set to zero or else it will just go to default) headerstr += f'\x00{Z*5}' # This is to link to dashboard... (set to zero or else it will just go to dashboard default)
else: else:
if not(tables[i] in list(table_reg.keys())): if not(tables[i] in list(table_reg.keys())):
headerstr += Z*56 headerstr += Z*56
@@ -229,11 +258,11 @@ def get_event_header(event_type, vals, tables, table_reg, tuning_number, col_del
return headerstr return headerstr
def get_table_spec(tables): def get_table_spec(tables):
'''Generates the file information for a set of tables. '''Generates the file information for a set of tables. This should go after the event section (see get_event_header())
Parameters Parameters
---------- ----------
tables: a dictionary of form { [table_name]: { 'values': '1 2 3', 'typestr': 'HP', 'start': 1 } } 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 = ''
specstr += Z*56 specstr += Z*56
@@ -257,8 +286,8 @@ def generate_default_sequence(col_names, col_delays):
Parameters Parameters
---------- ----------
col_names: an iterable of each of the column titles col_names: an iterable of each of the column titles.
col_delays: an iterable of each of the column delay values. col_delays: an iterable of each of the column delay values. Should match the ordering of col_names
Returns Returns
------- -------
@@ -274,12 +303,12 @@ def generate_default_sequence(col_names, col_delays):
return full_dict return full_dict
def create_sequence_file(filename, data, author='NA'): def create_sequence_file(filename, data, author='NA'):
'''Generates a Tecmag sequence file for use in Tecmag NMR (TNMR). '''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 Parameters
---------- ----------
filename: str 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': {...}, ... } }. 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. data: a dictionary in the form { 'columns': { [column_name_0]: { 'F1_Ampl': [value], ..., 'Rx_Blank': [value], 'Delay': [value] }, ... }, 'tables': { 'table_1': {...}, ... } }. 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. author [optional]: str to describe the file creator.
''' '''
content = '' content = ''

View File

@@ -1,13 +1,13 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
SequenceGeneration sequence_generation
______________________ ______________________
Version: 1.0 Version: 1.0
Authors: Davis Garrad (Paul Scherrer Institute, CH) Authors: Davis Garrad (Paul Scherrer Institute, CH)
______________________ ______________________
Wrapper for the API I wrote to generate pulse sequences programmatically in TNMR (Tecmag). Wrapper for the API I wrote to generate pulse sequences programmatically in TNMR (Tecmag), with a little more polish than the original driver code.
""" """
import frappy_psi.tnmr.sequence_fileformat as se import frappy_psi.tnmr.sequence_fileformat as se
@@ -15,7 +15,7 @@ from pydantic.utils import deep_update
import json import json
def get_single_pulse_block(name, pulse_width, pulse_height, relaxation_time, phase_cycle='0'): def get_single_pulse_block(name, pulse_width, pulse_height, delay_time, phase_cycle='0'):
'''Generates a single block of data to create a sequence with. '''Generates a single block of data to create a sequence with.
Parameters Parameters
@@ -23,26 +23,28 @@ def get_single_pulse_block(name, pulse_width, pulse_height, relaxation_time, pha
name: str, just the prefix for the column names. Ensure this is unique over the whole sequence! name: str, just the prefix for the column names. Ensure this is unique over the whole sequence!
pulse_width: str, in the format '10u' (10 microseconds) pulse_width: str, in the format '10u' (10 microseconds)
pulse_height: str, in the format '40' (I'm not honestly sure what units this is in...) pulse_height: str, in the format '40' (I'm not honestly sure what units this is in...)
relaxation_time: str, in the format '10u' delay_time: str, in the format '10u'. Minimum value of '0.1u'. If '0', then only the pulse is added (not the delay afterwards). If no unit is given, assumes microseconds.
phase_cycle: a given phase cycle (steps of 4, 0-3 inc.) (eg., '0 0 1 1 2 3 0 1', etc.) phase_cycle: a given phase cycle (steps of 4, 0-3 inc.) (eg., '0 0 1 1 2 3 0 1', etc.) (default '0')
Returns Returns
------- -------
a dictionary which can be updated with others to generate a larger, more complex sequence. a dictionary which can be updated with others (with combine_blocks) to generate a larger, more complex sequence.
''' '''
if(relaxation_time.strip()[-1] == 'u'): if(delay_time.strip()[-1] == 'u'):
relax_time = float(relaxation_time.strip()[:-1]) delay_time = float(delay_time.strip()[:-1])
elif(relaxation_time.strip()[-1] == 'n'): elif(delay_time.strip()[-1] == 'n'):
relax_time = float(relaxation_time.strip()[:-1]) * 1000 delay_time = float(delay_time.strip()[:-1]) * 1000
if(relaxation_time.strip()[-1] == 'm'): elif(delay_time.strip()[-1] == 'm'):
relax_time = float(relaxation_time.strip()[:-1]) / 1e3 delay_time = float(delay_time.strip()[:-1]) / 1e3
if(relaxation_time.strip()[-1] == 's'): elif(delay_time.strip()[-1] == 's'):
relax_time = float(relaxation_time.strip()[:-1]) / 1e6 delay_time = float(delay_time.strip()[:-1]) / 1e6
else:
delay_time = float(delay_time.strip()) # assume in us
ph = name + '_phase' ph = name + '_phase'
rl = name + '_relaxation' rl = name + '_delay'
block = se.generate_default_sequence([ ph, rl ] if relax_time > 0 else [ ph ], [ pulse_width, relaxation_time ] if relax_time > 0 else [pulse_width]) block = se.generate_default_sequence([ ph, rl ] if delay_time > 0 else [ ph ], [ pulse_width, str(delay_time) + 'u' ] if delay_time > 0 else [pulse_width])
# COLUMNNS # COLUMNNS
# PH column # PH column
@@ -51,8 +53,8 @@ def get_single_pulse_block(name, pulse_width, pulse_height, relaxation_time, pha
block['columns'][ph]['F1_UnBlank']['value'] = '1' block['columns'][ph]['F1_UnBlank']['value'] = '1'
block['columns'][ph]['Rx_Blank']['value'] = '1' block['columns'][ph]['Rx_Blank']['value'] = '1'
if(relax_time > 0): if(delay_time > 0):
# relaxation column # delay column
block['columns'][rl]['F1_UnBlank']['value'] = '1' block['columns'][rl]['F1_UnBlank']['value'] = '1'
block['columns'][rl]['Rx_Blank']['value'] = '1' block['columns'][rl]['Rx_Blank']['value'] = '1'
@@ -64,6 +66,12 @@ def get_single_pulse_block(name, pulse_width, pulse_height, relaxation_time, pha
return block return block
def get_initial_block(): def get_initial_block():
'''Generates the mandatory initial block, which consists of phase reset and unblanking
Returns
-------
a dictionary which can be updated with others (with combine_blocks) to generate a larger, more complex sequence.
'''
block = se.generate_default_sequence(['Phase reset', 'Unblank'], ['1u', '10u']) block = se.generate_default_sequence(['Phase reset', 'Unblank'], ['1u', '10u'])
# Phase reset # Phase reset
block['columns']['Phase reset']['F1_PhRst']['value'] = '1' block['columns']['Phase reset']['F1_PhRst']['value'] = '1'
@@ -83,6 +91,7 @@ def get_final_block(ringdown_time, preacquire_time, acquire_time, cooldown_time,
acquire_time: str, in the format '10u', how long to acquire data for acquire_time: str, in the format '10u', how long to acquire data for
cooldown_time: str, in the format '40u', how long to wait after acquisition. cooldown_time: str, in the format '40u', how long to wait after acquisition.
acq_phase_cycle: str, the phase cycle that the acquisition should follow acq_phase_cycle: str, the phase cycle that the acquisition should follow
Returns Returns
------- -------
a dictionary which can be updated with others to generate a larger, more complex sequence. a dictionary which can be updated with others to generate a larger, more complex sequence.
@@ -104,12 +113,15 @@ def get_final_block(ringdown_time, preacquire_time, acquire_time, cooldown_time,
return block return block
def combine_blocks(l, r): def combine_blocks(l, r):
'''Combines two dictionaries (just deep copies them, as they each have recursive dictionaries'''
return deep_update(l, r) return deep_update(l, r)
def save_sequence(filename, sequence): def save_sequence(filename, sequence):
'''Saves the given sequence to a file'''
se.create_sequence_file(filename, sequence) se.create_sequence_file(filename, sequence)
def save_sequence_cfg(filename, sequence): def save_sequence_cfg(filename, sequence):
'''Saves the sequence to a file in JSON form to be read easier. Appends the extension ".cfg" '''
with open(filename + '.cfg', 'w') as file: with open(filename + '.cfg', 'w') as file:
json.dump(sequence, file, indent=4) json.dump(sequence, file, indent=4)
file.close() file.close()

View File

@@ -1,6 +1,6 @@
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
""" """
TNMR_DG_Extension tnmr_interface
______________________ ______________________
Version: 1.0 Version: 1.0
@@ -30,12 +30,17 @@ class TNMR:
Instance Attributes Instance Attributes
------------------- -------------------
NMTNR: win32com object
ACTIVEFILE: str ACTIVEFILE: str
path to active TNMR file path to active TNMR file
ACTIVEPATH: str
path to active TNMR file's parent directory.
Methods Methods
------- -------
get_instance():
Gets an instance of the NTNMR "API". Best to use this rather than keep an object, as threads can mess with the usefulness of an object.
execute_cmd(cmd):
Sends an arbitrary command to the TNMR software. Best for debugging.
openfile(filepath: str, active: bool): openfile(filepath: str, active: bool):
opens the tnt file specified by filepath opens the tnt file specified by filepath
if active is true, the newly opened file will be set to ACTIVEFILE if active is true, the newly opened file will be set to ACTIVEFILE
@@ -47,8 +52,28 @@ class TNMR:
the acqusition endswith the acqusition endswith
acquisition_running(): acquisition_running():
returns the acquisition status of TNMR returns the acquisition status of TNMR
get_data():
Pulls the currently loaded data from TNMR and returns it.
save_file(filepath=''):
Saves the experiment to a file
set_nmrparameter(param_name: str, value: str):
If it exists, sets an NMR parameter
get_nmrparameter(param_name: str):
If it exists, returns the value of an NMR parameter
is_nmrparameter(param_name: str):
Checks if an NMR parameter exists.
get_all_nmrparameters():
Returns all possible NMR parameters in the dashboard.
get_page_parameters(page):
Returns all possible NMR parameters on a page of the dashboard.
load_sequence(filename):
WARNING: POSSIBLY DESTRUCTIVE. ENSURE DATA IS SAVED BEFORE CALLING.
Loads a sequence (and reloads the template dashboard) into the TNMR software.
load_dashboard(dashboard_fn):
Loads a dashboard (resetting parameters) into TNMR.
""" """
def __init__(self, filepath = "", NTNMR_inst=None): def __init__(self, filepath = ""):
""" Creates an instance of the NTNMR class, which is used to communicate with TNMR, """ Creates an instance of the NTNMR class, which is used to communicate with TNMR,
Tecmags control software. Basically a wrapper for TNMRs api Tecmags control software. Basically a wrapper for TNMRs api
@@ -57,27 +82,18 @@ class TNMR:
filepath: specifies a path to the file tnt you want to use filepath: specifies a path to the file tnt you want to use
""" """
#first we check if an instance of TNMR is running an get it or create it #first we check if an instance of TNMR is running an get it or create it
if(NTNMR_inst is None): print('Opening new TNMR connection')
print('Opening new TNMR connection') ntnmr = self.get_instance()
self.reset_NTNMR_instance()
else:
self.NTNMR = NTNMR_inst
# next we open a specified file. If none is specified, then we use the active file # next we open a specified file. If none is specified, then we use the active file
if filepath != "": if filepath != "":
print(f'Loading file {filepath}') print(f'Loading file {filepath}')
self.NTNMR.OpenFile(filepath) ntnmr.OpenFile(filepath)
self.ACTIVEFILE = self.NTNMR.GetActiveDocPath self.ACTIVEFILE = ntnmr.GetActiveDocPath
self.ACTIVEPATH = os.path.dirname(self.ACTIVEFILE) self.ACTIVEPATH = os.path.dirname(self.ACTIVEFILE)
print(f'Active file: {self.ACTIVEFILE} in path {self.ACTIVEPATH}') print(f'Active file: {self.ACTIVEFILE} in path {self.ACTIVEPATH}')
def reset_NTNMR_instance(self):
try:
pythoncom.CoInitialize()
self.NTNMR = win32com.client.GetActiveObject("NTNMR.Application")
except pythoncom.com_error:
raise TNMRNotRunnningError
def get_instance(self): def get_instance(self):
'''Tries to open up a Windows COM connection to the TNMR program, and returns an instance if able'''
try: try:
pythoncom.CoInitialize() pythoncom.CoInitialize()
return win32com.client.GetActiveObject("NTNMR.Application") return win32com.client.GetActiveObject("NTNMR.Application")
@@ -85,9 +101,10 @@ class TNMR:
raise TNMRNotRunnningError raise TNMRNotRunnningError
def execute_cmd(self, cmd): def execute_cmd(self, cmd):
print('W: Executing arbitrary command: ' + f'out = self.NTNMR.{cmd}') '''Sends an arbitrary command through to TNMR'''
print('W: Executing arbitrary command: ' + f'out = self.get_instance().{cmd}')
out = 0 out = 0
exec(f'out = self.NTNMR.{cmd}\nprint("W: OUTPUT: " + str(out))') exec(f'out = self.get_instance().{cmd}\nprint("W: OUTPUT: " + str(out))')
return out return out
def openfile(self, filepath, active = True): def openfile(self, filepath, active = True):
@@ -102,15 +119,16 @@ class TNMR:
active: bool active: bool
""" """
print(f'Opening file {filepath}') print(f'Opening file {filepath}')
self.NTNMR.OpenFile(filepath) ntnmr = self.get_instance()
ntnmr.OpenFile(filepath)
if active: if active:
self.ACTIVEFILE = self.NTNMR.GetActiveDocPath self.ACTIVEFILE = ntnmr.GetActiveDocPath
print(f'Active file: {self.ACTIVEFILE} in path {self.ACTIVEPATH}') print(f'Active file: {self.ACTIVEFILE} in path {self.ACTIVEPATH}')
def set_activefile(self): def set_activefile(self):
""" Sets TNMR active doc path to ACTIVEFILE """ Sets TNMR active doc path to ACTIVEFILE
""" """
self.ACTIVEFILE = self.NTNMR.GetActiveDocPath self.ACTIVEFILE = self.get_instance().GetActiveDocPath
self.ACTIVEPATH = os.path.dirname(self.ACTIVEFILE) self.ACTIVEPATH = os.path.dirname(self.ACTIVEFILE)
print(f'Active file: {self.ACTIVEFILE} in path {self.ACTIVEPATH}') print(f'Active file: {self.ACTIVEFILE} in path {self.ACTIVEPATH}')
@@ -129,6 +147,7 @@ class TNMR:
print('Zero-going...') print('Zero-going...')
ntnmr = self.get_instance() ntnmr = self.get_instance()
if not(self.acquisition_running()): if not(self.acquisition_running()):
ntnmr.Reset # to avoid hardware issues?
ntnmr.ZG ntnmr.ZG
else: else:
print('An Acquisition is already running') print('An Acquisition is already running')
@@ -137,6 +156,7 @@ class TNMR:
print("Application locked during acquisition\n...waiting...") print("Application locked during acquisition\n...waiting...")
while self.acquisition_running(): while self.acquisition_running():
time.sleep(interval) time.sleep(interval)
# TODO: https://stackoverflow.com/questions/27586411/how-do-i-close-window-with-handle-using-win32gui-in-python to close any tecmag dialogues that show up. Need to determine proper search string, so next time it pops up, run some tests.
print("Acquisition done") print("Acquisition done")
def acquisition_running(self): def acquisition_running(self):
@@ -147,34 +167,22 @@ class TNMR:
True: if running True: if running
False: if not running False: if not running
""" """
#try:
ntnmr = self.get_instance() ntnmr = self.get_instance()
res = not(ntnmr.CheckAcquisition) res = not(ntnmr.CheckAcquisition)
#except AttributeError as e:
# if(e
# res = False
return res return res
def get_data(self): def get_data(self):
'''Pulls data from TNMR
Returns
-------
a tuple of ([real_array], [imaginary_array])
'''
raw_data = self.get_instance().GetData raw_data = self.get_instance().GetData
reals = raw_data[::2] reals = raw_data[::2]
imags = raw_data[1::2] imags = raw_data[1::2]
return (reals, imags) return (reals, imags)
def get_data_times(self):
#acq_n = int(self.NTNMR.GetNMRParameter('Acq. Points')) # TODO: These do NOT return the actual used values!
#acq_t = self.NTNMR.GetNMRParameter('Acq. Time')
#acq_t = acq_t.strip()
#if(acq_t[-1] == 'm'):
# acq_t = float(acq_t[:-1]) * 1000
#elif(acq_t[-1] == 'u'):
# acq_t = float(acq_t[:-1])
#elif(acq_t[-1] == 'n'):
# acq_t = float(acq_t[:-1]) / 1000
acq_t = 204.8 # us
acq_n = 1024
return [ i * (acq_t / (acq_n - 1)) for i in range(0, acq_n+1) ]
def save_file(self, filepath=''): def save_file(self, filepath=''):
""" Save file to filepath. if no filepath specified, save current active file """ Save file to filepath. if no filepath specified, save current active file
@@ -244,9 +252,6 @@ class TNMR:
try: try:
self.get_instance().GetNMRParameter(param_name) self.get_instance().GetNMRParameter(param_name)
return True return True
except AttributeError:
self.reset_NTNMR_instance()
return self.is_nmrparameter(param_name)
except: except:
return False return False
@@ -341,6 +346,16 @@ class TNMR:
return True return True
def load_dashboard(self, dashboard_fn): def load_dashboard(self, dashboard_fn):
'''Loads a dashboard into TNMR. Resets the parameters to those in the dashboard file, despite what the TNMR documentation says.
Parameters
----------
filename: str, designates the dashboard to load
Returns
-------
A success flag - beware; sometimes, this can fail and still provide a false positive. False negatives, as far as I'm aware, never happen, though, so feel free to rely on that.
'''
print(f'I: Loading dashboard setup from {dashboard_fn}') print(f'I: Loading dashboard setup from {dashboard_fn}')
success = self.get_instance().LoadParameterSetupFromFile(dashboard_fn) success = self.get_instance().LoadParameterSetupFromFile(dashboard_fn)