diff --git a/frappy_psi/tnmr/OTFModule.py b/frappy_psi/tnmr/OTFModule.py index 08b5a4aa..7b3ac687 100644 --- a/frappy_psi/tnmr/OTFModule.py +++ b/frappy_psi/tnmr/OTFModule.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- """ -TNMR_DG_Extension +OTFModule ______________________ Version: 1.0 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! @@ -24,33 +25,36 @@ import numpy as np import threading import time import os +import traceback class ProgrammedSequence(fc.Readable): """An NMR device being driven by an instance of TNMR. Requires that an instance of TNMR is opened before creation. - - Instance Attributes - ------------------- - (parameter) title: a title which will be embedded to the sequence files. Use this for identification. - (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', ...) + + Use + --- + 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. - Methods - ------- - (cmd) add_pulse: adds a pulse to the end of the sequence. - Arguments: - - pulse_width: pulse width to add to sequence (usec) - - pulse_height: pulse height to add to sequence (a.u.) - - 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', ...) - (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 - (cmd) run: if compiled, starts data acquisition on TNMR (async) - (cmd) compile and run: you can guess + 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()). + + Attributes + ---------- + value: an array of complexes representing the TNMR data return (technically inherited from Readable) + 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') } + + Acquisition Parameters + ---------------------- + title: a title which will be embedded to the sequence files. Use this for identification. + acquisition_time: float (usecs) which describes the length of acquisition + 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 -------------------- @@ -75,17 +79,11 @@ class ProgrammedSequence(fc.Readable): 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'), pulse_height=fc.FloatRange(), - relaxation_time=fc.FloatRange(unit='u'), + delay_time=fc.FloatRange(unit='u'), 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 - 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) 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) @@ -93,10 +91,17 @@ class ProgrammedSequence(fc.Readable): 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) + 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 ### SETUP 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): try: self.ntnmr = te.TNMR() @@ -110,33 +115,14 @@ class ProgrammedSequence(fc.Readable): def initialReads(self): 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'}) 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): threading.Thread(target=lambda s=self: s.__compile_and_run()).start() else: @@ -144,6 +130,10 @@ class ProgrammedSequence(fc.Readable): @fc.Command(description="Kill") def kill(self): + '''Aborts the current scan, if one is running. Else, does nothing''' + self.stop() + + def stop(self): try: self.tnmr().get_instance().Abort self.status = ('IDLE', 'ok - killed') @@ -157,22 +147,6 @@ class ProgrammedSequence(fc.Readable): self.status = ('IDLE', 'ok - uncompiled') 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): self.acquisition_time = t self.status = ('IDLE', 'ok - uncompiled') @@ -218,6 +192,16 @@ class ProgrammedSequence(fc.Readable): ### PRIVATE (Utility) 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'): self.status = ('BUSY', 'compiling') # first, create the sequence @@ -226,7 +210,7 @@ class ProgrammedSequence(fc.Readable): 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', str(s['pulse_height']), - str(s['relaxation_time']) + 'u', + str(s['delay_time']) + 'u', str(s['phase_cycle']))) i += 1 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.post_acquisition_time) + 'm', str(self.acq_phase_cycle))) - + # then, save the thing filepath = os.getcwd() filename = self.title + f'_{time.time()}' @@ -246,30 +230,49 @@ class ProgrammedSequence(fc.Readable): '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 self.tnmr().load_sequence(filename) + time.sleep(1.0) # hardware module issue??? # load some parameters back to TNMR for key, val in dashboard_params.items(): self.tnmr().set_nmrparameter(key, val) + time.sleep(0.5) # finally, let ourselves know we're ready self.status = ('PREPARED', 'compiled') + else: + traceback.print_exc() def __zero_go(self): + '''Tells TNMR to acquire data. Only call after __compile_sequence().''' if(self.status[0] != 'BUSY'): self.status = ('BUSY', 'acquiring') self.tnmr().ZeroGo(lock=True, interval=0.5) newvals = {} newvals['reals'] = self.tnmr().get_data()[0] 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.status = ('PREPARED', 'compiled') - def __compile_and_run(self, thread=True, recurse=True): - self.tnmr().reset_NTNMR_instance() + def __compile_and_run(self, thread=True): + '''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() + time.sleep(0.5) self.__zero_go() diff --git a/frappy_psi/tnmr/sequence_fileformat.py b/frappy_psi/tnmr/sequence_fileformat.py index 5ae14db6..faf91569 100644 --- a/frappy_psi/tnmr/sequence_fileformat.py +++ b/frappy_psi/tnmr/sequence_fileformat.py @@ -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 # TODO: See if I can't get my name into this... # PSEQ1.001.18 BIN @@ -87,7 +99,7 @@ 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_Ph': ('\x05', 'M', 'E', 'H', 'P', '\x02'), 'F1_PhMod': ('\x17', 'P', 'E', '3', 'P', '\x08'), @@ -107,7 +119,8 @@ event_codes = { } 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_Ph': -1, 'F1_UnBlank': 0, @@ -132,6 +145,18 @@ def fm(s, spacing=3): 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) @@ -150,6 +175,13 @@ def get_info_header(filename, author, col_names, tuning_number, binary_name='PSE 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 @@ -163,15 +195,15 @@ def get_delay_header(col_delays, tuning_number): return headerstr 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 ------ - 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. 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: 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 ''' 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' filtr = f'{freq}Hz' - #sweep = '2500000.0Hz' - #filtr = '2500000.0Hz' - #dwell = '400.0n' # hard limit apparently... headerstr += Z*52 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(dwell)) 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: if not(tables[i] in list(table_reg.keys())): headerstr += Z*56 @@ -229,11 +258,11 @@ def get_event_header(event_type, vals, tables, table_reg, tuning_number, col_del return headerstr 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 ---------- - 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 += Z*56 @@ -257,8 +286,8 @@ def generate_default_sequence(col_names, col_delays): Parameters ---------- - col_names: an iterable of each of the column titles - col_delays: an iterable of each of the column delay values. + 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 ------- @@ -274,12 +303,12 @@ def generate_default_sequence(col_names, col_delays): return full_dict 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 ---------- - filename: str - 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. + 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. This is best generated using generate_default_sequence and then modifying the given sequence. author [optional]: str to describe the file creator. ''' content = '' diff --git a/frappy_psi/tnmr/sequence_generation.py b/frappy_psi/tnmr/sequence_generation.py index 0c540af8..928eb673 100644 --- a/frappy_psi/tnmr/sequence_generation.py +++ b/frappy_psi/tnmr/sequence_generation.py @@ -1,13 +1,13 @@ # -*- coding: utf-8 -*- """ -SequenceGeneration +sequence_generation ______________________ Version: 1.0 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 @@ -15,7 +15,7 @@ from pydantic.utils import deep_update 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. 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! 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...) - relaxation_time: str, in the format '10u' - phase_cycle: a given phase cycle (steps of 4, 0-3 inc.) (eg., '0 0 1 1 2 3 0 1', etc.) + 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.) (default '0') 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'): - relax_time = float(relaxation_time.strip()[:-1]) - elif(relaxation_time.strip()[-1] == 'n'): - relax_time = float(relaxation_time.strip()[:-1]) * 1000 - if(relaxation_time.strip()[-1] == 'm'): - relax_time = float(relaxation_time.strip()[:-1]) / 1e3 - if(relaxation_time.strip()[-1] == 's'): - relax_time = float(relaxation_time.strip()[:-1]) / 1e6 - + if(delay_time.strip()[-1] == 'u'): + delay_time = float(delay_time.strip()[:-1]) + elif(delay_time.strip()[-1] == 'n'): + delay_time = float(delay_time.strip()[:-1]) * 1000 + elif(delay_time.strip()[-1] == 'm'): + delay_time = float(delay_time.strip()[:-1]) / 1e3 + elif(delay_time.strip()[-1] == 's'): + delay_time = float(delay_time.strip()[:-1]) / 1e6 + else: + delay_time = float(delay_time.strip()) # assume in us + ph = name + '_phase' - rl = name + '_relaxation' - block = se.generate_default_sequence([ ph, rl ] if relax_time > 0 else [ ph ], [ pulse_width, relaxation_time ] if relax_time > 0 else [pulse_width]) + rl = name + '_delay' + 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 # 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]['Rx_Blank']['value'] = '1' - if(relax_time > 0): - # relaxation column + if(delay_time > 0): + # delay column block['columns'][rl]['F1_UnBlank']['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 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']) # Phase reset 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 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 + Returns ------- 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 def combine_blocks(l, r): + '''Combines two dictionaries (just deep copies them, as they each have recursive dictionaries''' return deep_update(l, r) def save_sequence(filename, sequence): + '''Saves the given sequence to a file''' se.create_sequence_file(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: json.dump(sequence, file, indent=4) file.close() diff --git a/frappy_psi/tnmr/tnmr_interface.py b/frappy_psi/tnmr/tnmr_interface.py index 35ac600a..1d8baeb9 100644 --- a/frappy_psi/tnmr/tnmr_interface.py +++ b/frappy_psi/tnmr/tnmr_interface.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -TNMR_DG_Extension +tnmr_interface ______________________ Version: 1.0 @@ -30,12 +30,17 @@ class TNMR: Instance Attributes ------------------- - NMTNR: win32com object ACTIVEFILE: str path to active TNMR file + ACTIVEPATH: str + path to active TNMR file's parent directory. 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): opens the tnt file specified by filepath if active is true, the newly opened file will be set to ACTIVEFILE @@ -47,8 +52,28 @@ class TNMR: the acqusition endswith acquisition_running(): 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, 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 """ #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') - self.reset_NTNMR_instance() - else: - self.NTNMR = NTNMR_inst + print('Opening new TNMR connection') + ntnmr = self.get_instance() # next we open a specified file. If none is specified, then we use the active file if filepath != "": print(f'Loading file {filepath}') - self.NTNMR.OpenFile(filepath) - self.ACTIVEFILE = self.NTNMR.GetActiveDocPath + ntnmr.OpenFile(filepath) + self.ACTIVEFILE = ntnmr.GetActiveDocPath self.ACTIVEPATH = os.path.dirname(self.ACTIVEFILE) 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): + '''Tries to open up a Windows COM connection to the TNMR program, and returns an instance if able''' try: pythoncom.CoInitialize() return win32com.client.GetActiveObject("NTNMR.Application") @@ -85,9 +101,10 @@ class TNMR: raise TNMRNotRunnningError 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 - 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 def openfile(self, filepath, active = True): @@ -102,15 +119,16 @@ class TNMR: active: bool """ print(f'Opening file {filepath}') - self.NTNMR.OpenFile(filepath) + ntnmr = self.get_instance() + ntnmr.OpenFile(filepath) if active: - self.ACTIVEFILE = self.NTNMR.GetActiveDocPath + self.ACTIVEFILE = ntnmr.GetActiveDocPath print(f'Active file: {self.ACTIVEFILE} in path {self.ACTIVEPATH}') def set_activefile(self): """ 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) print(f'Active file: {self.ACTIVEFILE} in path {self.ACTIVEPATH}') @@ -129,6 +147,7 @@ class TNMR: print('Zero-going...') ntnmr = self.get_instance() if not(self.acquisition_running()): + ntnmr.Reset # to avoid hardware issues? ntnmr.ZG else: print('An Acquisition is already running') @@ -137,6 +156,7 @@ class TNMR: print("Application locked during acquisition\n...waiting...") while self.acquisition_running(): 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") def acquisition_running(self): @@ -147,34 +167,22 @@ class TNMR: True: if running False: if not running """ - #try: ntnmr = self.get_instance() res = not(ntnmr.CheckAcquisition) - #except AttributeError as e: - # if(e - # res = False return res def get_data(self): + '''Pulls data from TNMR + + Returns + ------- + a tuple of ([real_array], [imaginary_array]) + ''' raw_data = self.get_instance().GetData reals = raw_data[::2] imags = raw_data[1::2] 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=''): """ Save file to filepath. if no filepath specified, save current active file @@ -244,9 +252,6 @@ class TNMR: try: self.get_instance().GetNMRParameter(param_name) return True - except AttributeError: - self.reset_NTNMR_instance() - return self.is_nmrparameter(param_name) except: return False @@ -341,6 +346,16 @@ class TNMR: return True 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}') success = self.get_instance().LoadParameterSetupFromFile(dashboard_fn)