A few things: 1. Got it working again; 2. Renamed files to make more sense; 3. Replaced template tmp.tnt with an emptied out file that previously took data, now data is collected correctly (bug, I'm not sure where this need comes from but this is, as far as I know, a permanent workaround); 4. Added automatic COM interface restart on errors compiling; 5. Implemented variable acquisition times.
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
import frappy.core as fc
|
||||
import TNMRExt.OTFModule as mod
|
||||
|
||||
Node('example_TNMR.psi.ch', 'The NMR system running the Scout and controlled with TNMR', interface='tcp://5000')
|
||||
|
||||
Mod('tnmr_otf_module', mod.ProgrammedSequence, 'NMR Sequence')
|
||||
Mod('tnmr_otf_module', 'frappy_psi.tnmr.OTFModule.ProgrammedSequence', 'NMR Sequence')
|
||||
@@ -11,8 +11,8 @@ frappy-based module for generating and running pulse sequences on TNMR (Tecmag).
|
||||
"""
|
||||
#On-the-fly!
|
||||
|
||||
import TNMRExt.TNMR_DG_Extension as te
|
||||
import TNMRExt.SequenceGeneration as seq_gen
|
||||
import frappy_psi.tnmr.tnmr_interface as te
|
||||
import frappy_psi.tnmr.sequence_generation as seq_gen
|
||||
|
||||
import frappy.core as fc
|
||||
import frappy
|
||||
@@ -31,7 +31,7 @@ class ProgrammedSequence(fc.Readable):
|
||||
Instance Attributes
|
||||
-------------------
|
||||
(parameter) title: a title which will be embedded to the sequence files. Use this for identification.
|
||||
(parameter) sequence_data: a dictionary describing the currently-built sequence
|
||||
(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
|
||||
@@ -64,7 +64,10 @@ class ProgrammedSequence(fc.Readable):
|
||||
"""
|
||||
|
||||
# inherited
|
||||
value = fc.Parameter('data_return', fc.ArrayOf(fc.FloatRange(), maxlen=4096), default=[])
|
||||
value = fc.Parameter('data_return', fc.StructOf(reals=fc.ArrayOf(fc.FloatRange(), maxlen=4096), # real values
|
||||
imags=fc.ArrayOf(fc.FloatRange(), maxlen=4096), # imag values
|
||||
t =fc.ArrayOf(fc.FloatRange(), maxlen=4096)), # times (starting from zero)
|
||||
default={ 'reals': [], 'imags': [], 't': [] })
|
||||
status = fc.Parameter(datatype=frappy.datatypes.StatusType(fc.Readable, "DISABLED", 'PREPARED', 'BUSY'))
|
||||
pollinterval = fc.Parameter(default=1)
|
||||
|
||||
@@ -73,19 +76,19 @@ class ProgrammedSequence(fc.Readable):
|
||||
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'),
|
||||
phase_cycle=fc.StringType())))
|
||||
phase_cycle=fc.StringType())), default=[], readonly=False)
|
||||
|
||||
# sequence edit
|
||||
pulse_width = fc.Parameter('pulse_width', fc.FloatRange(unit='u'), readonly=False, group='pulse_editor')
|
||||
pulse_height = fc.Parameter('pulse_height', fc.FloatRange(), readonly=False, group='pulse_editor')
|
||||
relaxation_time = fc.Parameter('relaxation_time', fc.FloatRange(unit='u', min=0.1), readonly=False, group='pulse_editor')
|
||||
phase_cycle = fc.Parameter('phase_cycle', fc.StringType(), readonly=False, group='pulse_editor', default='')
|
||||
#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=True, 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)
|
||||
ringdown_time = fc.Parameter('ringdown_time', fc.FloatRange(unit='u'), readonly=False, group='sequence_editor')
|
||||
pre_acquisition_time = fc.Parameter('pre_acquisition_time', fc.FloatRange(unit='u'), readonly=False, group='sequence_editor')
|
||||
post_acquisition_time = fc.Parameter('post_acquisition_time', fc.FloatRange(unit='m'), readonly=False, group='sequence_editor')
|
||||
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)
|
||||
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)
|
||||
acq_phase_cycle = fc.Parameter('acq_phase_cycle', fc.StringType(), readonly=False, group='sequence_editor', default='')
|
||||
|
||||
inited = False
|
||||
@@ -101,111 +104,79 @@ class ProgrammedSequence(fc.Readable):
|
||||
pass
|
||||
|
||||
### COMMANDS
|
||||
@fc.Command(description="Add Pulse", group='pulse_editor', argument={ 'type': 'struct' }, members={ 'a': { 'type': 'string' }})
|
||||
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="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="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="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="Run")
|
||||
#def run(self):
|
||||
# threading.Thread(target=lambda s=self: s.__zero_go()).start()
|
||||
|
||||
@fc.Command(description="Compile & Run")
|
||||
def compile_and_run(self):
|
||||
threading.Thread(target=lambda s=self: s.__compile_and_run()).start()
|
||||
@fc.Command(description="Compile & Run", argument={'type': 'bool'})
|
||||
def compile_and_run(self, thread=True):
|
||||
if(thread):
|
||||
threading.Thread(target=lambda s=self: s.__compile_and_run()).start()
|
||||
else:
|
||||
self.__compile_and_run()
|
||||
|
||||
### READ/WRITE
|
||||
def read_value(self):
|
||||
return self.value # TODO: this is only reals
|
||||
|
||||
def read_title(self):
|
||||
return self.title
|
||||
|
||||
def write_title(self, t):
|
||||
self.title = t
|
||||
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 read_sequence_data(self):
|
||||
return self.sequence_data
|
||||
#def write_pulse_height(self, t):
|
||||
# self.pulse_height = t
|
||||
# return self.read_pulse_height()
|
||||
|
||||
def read_pulse_width(self):
|
||||
return self.pulse_width
|
||||
#def write_relaxation_time(self, t):
|
||||
# self.relaxation_time = t
|
||||
# return self.read_relaxation_time()
|
||||
|
||||
def write_pulse_width(self, t):
|
||||
self.pulse_width = t
|
||||
return self.read_pulse_width()
|
||||
|
||||
def read_pulse_height(self):
|
||||
return self.pulse_height
|
||||
|
||||
def write_pulse_height(self, t):
|
||||
self.pulse_height = t
|
||||
return self.read_pulse_height()
|
||||
|
||||
def read_relaxation_time(self):
|
||||
return self.relaxation_time
|
||||
|
||||
def write_relaxation_time(self, t):
|
||||
self.relaxation_time = t
|
||||
return self.read_relaxation_time()
|
||||
|
||||
def read_phase_cycle(self):
|
||||
return self.phase_cycle
|
||||
|
||||
def write_phase_cycle(self, t):
|
||||
self.phase_cycle = t
|
||||
return self.read_phase_cycle()
|
||||
|
||||
def read_acquisition_time(self):
|
||||
return self.acquisition_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')
|
||||
return self.read_acquisition_time()
|
||||
|
||||
def read_ringdown_time(self):
|
||||
return self.ringdown_time
|
||||
|
||||
def write_ringdown_time(self, t):
|
||||
self.ringdown_time = t
|
||||
self.status = ('IDLE', 'ok - uncompiled')
|
||||
return self.read_ringdown_time()
|
||||
|
||||
def read_pre_acquisition_time(self):
|
||||
return self.pre_acquisition_time
|
||||
|
||||
def write_pre_acquisition_time(self, t):
|
||||
self.pre_acquisition_time = t
|
||||
self.status = ('IDLE', 'ok - uncompiled')
|
||||
return self.read_pre_acquisition_time()
|
||||
|
||||
def read_post_acquisition_time(self):
|
||||
return self.post_acquisition_time
|
||||
|
||||
def write_post_acquisition_time(self, t):
|
||||
self.post_acquisition_time = t
|
||||
self.status = ('IDLE', 'ok - uncompiled')
|
||||
return self.read_post_acquisition_time()
|
||||
|
||||
def read_acq_phase_cycle(self):
|
||||
return self.acq_phase_cycle
|
||||
|
||||
def write_acq_phase_cycle(self, t):
|
||||
self.acq_phase_cycle = t
|
||||
@@ -214,7 +185,7 @@ class ProgrammedSequence(fc.Readable):
|
||||
|
||||
### PRIVATE (Utility)
|
||||
def __compile_sequence(self):
|
||||
if(self.status != ('PREPARED', 'compiled')) and (self.status[0] != 'BUSY'):
|
||||
if(self.status[0] != 'BUSY'):
|
||||
self.status = ('BUSY', 'compiling')
|
||||
# first, create the sequence
|
||||
seq = seq_gen.get_initial_block()
|
||||
@@ -248,13 +219,30 @@ class ProgrammedSequence(fc.Readable):
|
||||
if(self.status[0] != 'BUSY'):
|
||||
self.status = ('BUSY', 'acquiring')
|
||||
self.tnmr().ZeroGo(lock=True, interval=0.5)
|
||||
self.value = self.tnmr().get_data()[0] # TODO: this is only reals...
|
||||
print(self.value)
|
||||
newvals = {}
|
||||
newvals['reals'] = self.tnmr().get_data()[0]
|
||||
newvals['imags'] = self.tnmr().get_data()[1]
|
||||
newvals['t'] = self.tnmr().get_data_times()
|
||||
self.value = newvals
|
||||
self.status = ('PREPARED', 'compiled')
|
||||
|
||||
def __compile_and_run(self):
|
||||
self.__compile_sequence()
|
||||
self.__zero_go()
|
||||
def __compile_and_run(self, thread=True, recurse=True):
|
||||
pythoncom.CoInitialize()
|
||||
try:
|
||||
self.__compile_sequence()
|
||||
self.__zero_go()
|
||||
except AttributeError as e:
|
||||
print(f'Attribute error on compile and run.{" Resetting the COM interface and retrying..." if recurse else " Resetting did not fix this problem!"}')
|
||||
self.status = ('IDLE', 'ok - uncompiled')
|
||||
self.tnmr().reset_NTNMR_instance()
|
||||
self.__compile_and_run(thread, recurse=False)
|
||||
except Exception as e:
|
||||
print('Failed to compile and run!')
|
||||
print(str(e))
|
||||
print(repr(e))
|
||||
self.status = ('IDLE', 'ok - uncompiled')
|
||||
if(thread):
|
||||
pythoncom.CoUninitialize()
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -170,6 +170,9 @@ def get_event_header(event_type, vals, tables, table_reg, tuning_number, col_del
|
||||
event_type: str describing the event
|
||||
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
|
||||
col_delays: the array of column delays
|
||||
'''
|
||||
codes = event_codes[event_type]
|
||||
headerstr = ''
|
||||
@@ -191,19 +194,24 @@ def get_event_header(event_type, vals, tables, table_reg, tuning_number, col_del
|
||||
headerstr += fm(str(vals[i]))
|
||||
if(event_type == 'Acq' and str(vals[i]) == '1'):
|
||||
acq_points = 1024
|
||||
sweep = '2500000.0Hz'
|
||||
filtr = '2500000.0Hz'
|
||||
dwell = '200.0n' # hard limit apparently...
|
||||
#acq_length = col_delays[i]
|
||||
#if('u' in acq_length):
|
||||
# acq_length = float(acq_length.strip()[:-1])
|
||||
#elif('m' in acq_length):
|
||||
# acq_length = 1000*float(acq_length.strip()[:-1])
|
||||
#elif('n' in acq_length):
|
||||
# acq_length = 0.001*float(acq_length.strip()[:-1])
|
||||
#dwell = acq_length / acq_points
|
||||
#dwell = str(dwell) + 'u'
|
||||
acq_time = col_delays[i]
|
||||
if('u' in acq_time):
|
||||
acq_time = float(acq_time.strip()[:-1])
|
||||
elif('m' in acq_time):
|
||||
acq_time = 1000*float(acq_time.strip()[:-1])
|
||||
elif('n' in acq_time):
|
||||
acq_time = 0.001*float(acq_time.strip()[:-1])
|
||||
|
||||
dwell_us = acq_time / acq_points
|
||||
dwell = f'{dwell_us*1000}n'
|
||||
freq = 1/(dwell_us/1e6) / 2 # put it in Hz. TODO: Figure out why the factor of 2 is necessary...
|
||||
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))
|
||||
headerstr += fm(str(sweep))
|
||||
@@ -257,7 +265,6 @@ def generate_default_sequence(col_names, col_delays):
|
||||
a data dictionary in the form required by create_sequence_file.
|
||||
'''
|
||||
full_dict = { 'columns': {}, 'tables': {} }
|
||||
print(full_dict)
|
||||
for c, delay in zip(col_names, col_delays):
|
||||
sub_dict = {}
|
||||
for e in event_types:
|
||||
@@ -9,7 +9,7 @@ ______________________
|
||||
|
||||
Wrapper for the API I wrote to generate pulse sequences programmatically in TNMR (Tecmag).
|
||||
"""
|
||||
import TNMRSeq.sequence_generator as se
|
||||
import frappy_psi.tnmr.sequence_fileformat as se
|
||||
|
||||
from pydantic.utils import deep_update
|
||||
|
||||
BIN
frappy_psi/tnmr/templates/tmp.tnt
Normal file
BIN
frappy_psi/tnmr/templates/tmp.tnt
Normal file
Binary file not shown.
Binary file not shown.
@@ -11,7 +11,7 @@ Wrapper for communication with TecMag TNMR software, specifically in the context
|
||||
"""
|
||||
import os
|
||||
|
||||
TEMPLATE_FILE_PATH = os.path.dirname(os.path.realpath(__file__)) + '/templates/' # TODO: Make some sort of installer/initialiser that sets this all up...
|
||||
TEMPLATE_FILE_PATH = os.path.dirname(os.path.realpath(__file__)) + '/templates/' # TODO: make prettier
|
||||
|
||||
import win32com.client
|
||||
import pythoncom
|
||||
@@ -58,14 +58,10 @@ class TNMR:
|
||||
"""
|
||||
#first we check if an instance of TNMR is running an get it or create it
|
||||
print('Opening TNMR connection')
|
||||
try:
|
||||
if(NTNMR_inst is None):
|
||||
self.NTNMR = win32com.client.GetActiveObject("NTNMR.Application")
|
||||
else:
|
||||
self.NTNMR = NTNMR_inst
|
||||
except pythoncom.com_error:
|
||||
raise TNMRNotRunnningError
|
||||
|
||||
if(NTNMR_inst is None):
|
||||
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
|
||||
if filepath != "":
|
||||
print(f'Loading file {filepath}')
|
||||
@@ -74,6 +70,12 @@ class TNMR:
|
||||
self.ACTIVEPATH = os.path.dirname(self.ACTIVEFILE)
|
||||
print(f'Active file: {self.ACTIVEFILE} in path {self.ACTIVEPATH}')
|
||||
|
||||
def reset_NTNMR_instance(self):
|
||||
try:
|
||||
self.NTNMR = win32com.client.GetActiveObject("NTNMR.Application")
|
||||
except pythoncom.com_error:
|
||||
raise TNMRNotRunnningError
|
||||
|
||||
def execute_cmd(self, cmd):
|
||||
print('W: Executing arbitrary command: ' + f'out = self.NTNMR.{cmd}')
|
||||
out = 0
|
||||
@@ -144,6 +146,20 @@ class TNMR:
|
||||
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
|
||||
Reference in New Issue
Block a user