frappy_psi.tnmr: added a module (frappy_psi.tnmr.OTFModule) to interface with the Tecmag NMR (TNMR) program from a frappy server.

This commit is contained in:
2025-06-11 08:50:21 +02:00
parent 2c5d5da773
commit 05324a8966
7 changed files with 1008 additions and 0 deletions

View File

@@ -0,0 +1,268 @@
# -*- coding: utf-8 -*-
"""
TNMR_DG_Extension
______________________
Version: 1.0
Authors: Davis Garrad (Paul Scherrer Institute, CH)
______________________
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.core as fc
import frappy
import win32com
import pythoncom
import numpy as np
import threading
import time
import os
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: a dictionary describing the currently-built sequence
(parameter) value: an array of complexes representing the TNMR data return (technically inherited from Readable)
(parameter) acquisition_time: float (usecs) which describes the length of acquisition
(parameter) ringdown_time: float (usecs) which describes the length of ringdown
(parameter) pre_acquisition_time: float (usecs) which describes the length of time to wait after ringdown finishes (1u is okay)
(parameter) post_acquisition_time: float (ms) which describes the length of time to wait after finishing acquisition
(parameter) acq_phase_cycle: str, the phase cycle to run on acquisition (eg., '0 1 1 2', '0 1 2 3', '1 1 2 2 0 0 3 3 1 2 3 4', ...)
Methods
-------
(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
Inherited Attributes
--------------------
name: from Module
logger: from Module
cfgdict: from Module
src: from Module
status: from Readable
value: from Readable
pollinterval: from Readable
"""
# inherited
value = fc.Parameter('data_return', fc.ArrayOf(fc.FloatRange(), maxlen=4096), default=[])
status = fc.Parameter(datatype=frappy.datatypes.StatusType(fc.Readable, "DISABLED", 'PREPARED', 'BUSY'))
pollinterval = fc.Parameter(default=1)
# basic
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'),
phase_cycle=fc.StringType())))
# 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='')
# 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')
acq_phase_cycle = fc.Parameter('acq_phase_cycle', fc.StringType(), readonly=False, group='sequence_editor', default='')
inited = False
### SETUP
def tnmr(self):
if not(self.inited):
self.ntnmr = te.TNMR()
self.inited = True
return self.ntnmr
def initialReads(self):
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="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")
def compile_and_run(self):
threading.Thread(target=lambda s=self: s.__compile_and_run()).start()
### 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 read_sequence_data(self):
return self.sequence_data
def read_pulse_width(self):
return self.pulse_width
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_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
self.status = ('IDLE', 'ok - uncompiled')
return self.read_acq_phase_cycle()
### PRIVATE (Utility)
def __compile_sequence(self):
if(self.status != ('PREPARED', 'compiled')) and (self.status[0] != 'BUSY'):
self.status = ('BUSY', 'compiling')
# first, create the sequence
seq = seq_gen.get_initial_block()
i = 0
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['phase_cycle'])))
i += 1
seq = seq_gen.combine_blocks(seq, seq_gen.get_final_block(str(self.ringdown_time) + 'u',
str(self.pre_acquisition_time) + 'u',
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()}'
filename = filepath + '/' + filename.replace('.','')
seq_gen.save_sequence(filename, seq)
seq_gen.save_sequence_cfg(filename, seq)
# then, load the thing into TNMR
self.tnmr().load_sequence(filename)
# finally, let ourselves know we're ready
self.status = ('PREPARED', 'compiled')
def __zero_go(self):
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)
self.status = ('PREPARED', 'compiled')
def __compile_and_run(self):
self.__compile_sequence()
self.__zero_go()