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

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
# 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 = ''

View File

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

View File

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