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