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:
2025-06-12 11:04:57 +02:00
parent 05324a8966
commit 365f0a2374
8 changed files with 125 additions and 115 deletions

View File

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

View File

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

View File

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

View File

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

Binary file not shown.

Binary file not shown.

View File

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