From d5d9d70713e775807df50984932caa5f708471a1 Mon Sep 17 00:00:00 2001 From: Last Davis Vern Date: Thu, 24 Jul 2025 11:24:56 +0200 Subject: [PATCH 1/3] Fixed weird bug where the length of pulse sequences could never increase. Now, they have a hard limit of 100 pulses. Not a big deal. --- frappy_psi/tnmr/OTFModule.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/frappy_psi/tnmr/OTFModule.py b/frappy_psi/tnmr/OTFModule.py index 057a4202..643534d4 100644 --- a/frappy_psi/tnmr/OTFModule.py +++ b/frappy_psi/tnmr/OTFModule.py @@ -81,10 +81,11 @@ class ProgrammedSequence(fc.Readable): comments = fc.Parameter('comments', fc.StringType(), default='', readonly=False) nucleus = fc.Parameter('nucleus', fc.StringType(), default='', readonly=False) + sequence_length = fc.Parameter('sequence_length', fc.IntRange(), default=0, readonly=True) sequence_data = fc.Parameter('sequence_config', fc.ArrayOf(fc.StructOf(pulse_width=fc.FloatRange(unit='usecs'), pulse_height=fc.FloatRange(unit='%'), delay_time=fc.FloatRange(unit='usecs'), - phase_cycle=fc.StringType())), default=[], readonly=False) + phase_cycle=fc.StringType()), minlen=0), default=[{'pulse_width':0,'pulse_height':0,'delay_time':0,'phase_cycle':''}]*100, readonly=False) # final details acquisition_time = fc.Parameter('acquisition_time', fc.FloatRange(unit='usecs'), readonly=False, group='sequence_editor', default=204.8) # this is a limit set by the dwell limit and number of acquisition points @@ -210,6 +211,16 @@ class ProgrammedSequence(fc.Readable): self.tnmr().set_nmrparameter('Observe Freq.', t) self.status = ('IDLE', 'ok - uncompiled') return self.read_obs_freq() + + def write_sequence_data(self, t): + self.sequence_length = len(t) + seq = [] + seq += t + print(seq) + seq += [{'pulse_width':0,'pulse_height':0,'delay_time':0,'phase_cycle':''}] * (100-self.sequence_length) # because nicos will only send the smallest size it has ever sent... + self.sequence_data = seq + + return self.read_sequence_data() ### PRIVATE (Utility) def __compile_sequence(self): @@ -229,7 +240,8 @@ class ProgrammedSequence(fc.Readable): seq = seq_gen.get_initial_block() i = 0 self.approx_sequence_length = 0 - for s in self.sequence_data: + for si in range(self.sequence_length): + s = self.sequence_data[si] 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['delay_time']) + 'u', @@ -252,7 +264,6 @@ class ProgrammedSequence(fc.Readable): filename = filepath + '/sequences/' + filename.replace('.','') seq_gen.save_sequence(filename, seq) seq_gen.save_sequence_cfg(filename, seq) - print(filename) dashboard_params = { 'Observe Freq.': self.read_obs_freq(), 'Scans 1D': self.read_num_scans(), From e77c48ace0a445b6a69a5f2913325866b935011c Mon Sep 17 00:00:00 2001 From: Last Davis Vern Date: Tue, 29 Jul 2025 14:31:24 +0200 Subject: [PATCH 2/3] Added functionality for the TNMR module to write partial scans - useful for long experiments with many acquisitions, which might need to be terminated early. Also good for impatient people. Added functionality to the ZVL Network Analyser module to allow for use of inbuilt data correction (calibration). --- frappy_psi/network_analysers/ZVL/ZVLDriver.py | 43 +++++++++++----- frappy_psi/network_analysers/ZVL/test.py | 50 ++++++++++++++++++ frappy_psi/tnmr/OTFModule.py | 51 ++++++++++++++----- frappy_psi/tnmr/tnmr_interface.py | 4 ++ 4 files changed, 122 insertions(+), 26 deletions(-) create mode 100644 frappy_psi/network_analysers/ZVL/test.py diff --git a/frappy_psi/network_analysers/ZVL/ZVLDriver.py b/frappy_psi/network_analysers/ZVL/ZVLDriver.py index 8290ec99..580eaf20 100644 --- a/frappy_psi/network_analysers/ZVL/ZVLDriver.py +++ b/frappy_psi/network_analysers/ZVL/ZVLDriver.py @@ -30,32 +30,50 @@ class ZVLNetAnalyzer(): self.base_data = np.array([]) self.base_data = self.get_data()[1] + self.min_freq = 0 + self.max_freq = 0 + def reset(self): - #self.instrument.write('*RST') - #self.instrument.write('SYST:PRES') # reloads current setup. - self.instrument.write('*CLS') + self.instrument.write('*RST') self.instrument.write('INST:NSEL 2') self.instrument.write('DISPlay:WINDow1:STATe ON') self.instrument.write(":CALC:PAR:MEAS 'Trc1', 'S11'") self.instrument.write('CALC:FORM MLOG') self.instrument.write('INIT:CONT OFF') self.instrument.write("SYST:USER:DISP:TITL 'Frappy connection'") - #self.instrument.write('INIT:SCOP OFF') - #self.instrument.write('DISPlay:WINDow2:STATe ON') + self.instrument.write('*ESE') + + self.min_freq, self.max_freq = self.get_freq_range() # default is largest. - def load_calibration(self, f): - self.instrument.write(f":MMEMORY:STORE:CORR 1, 'OSM1 {f}'") # put calibration in pool - self.instrument.write(f":MMEMORY:LOAD:CORR 1, 'OSM1 {f}'") # load from pool + def reload_calibration(self): + self.reset() + #self.instrument.write("DISP:MENU:KEY:SEL 'Correction Off'") + #self.instrument.write("DISP:MENU:KEY:EXEC 'Correction Off'") + self.instrument.write("DISP:MENU:KEY:EXEC 'Recall Last Cal Set'") + time.sleep(1) + self.min_freq, self.max_freq = self.get_freq_range() # default is largest. + + def get_freq_range(self): + start = float(self.instrument.ask('SENS1:FREQ:STAR?')) + stop = float(self.instrument.ask('SENS1:FREQ:STOP?')) + + return start, stop def set_freq_range(self, start, stop): '''In Hz''' - self.instrument.write(f'SENS1:FREQ:STAR {start}') - self.instrument.write(f'SENS1:FREQ:STOP {stop}') + if(start >= self.min_freq) and (stop <= self.max_freq): + + self.instrument.write(f'SENS1:FREQ:STAR {start}') + self.instrument.write(f'SENS1:FREQ:STOP {stop}') + self.start_freq = start + self.stop_freq = stop def set_freq_span(self, center, span): '''In Hz''' - self.instrument.write(f'SENS1:FREQ:CENT {center}') - self.instrument.write(f'SENS1:FREQ:SPAN {span}') + start = center - span/2 + stop = center + span/2 + + self.set_freq_range(start, stop) def set_averaging_passes(self,avgs): ''' @@ -101,6 +119,7 @@ class ZVLNetAnalyzer(): assert(averaging_passes<=999) assert(units in ['dB', 'unitless']) + self.instrument.write('INIT:CONT OFF') self.instrument.write(f'SWE:POIN {N}') self.instrument.write(f'SWE:COUN {averaging_passes}') diff --git a/frappy_psi/network_analysers/ZVL/test.py b/frappy_psi/network_analysers/ZVL/test.py new file mode 100644 index 00000000..922affb5 --- /dev/null +++ b/frappy_psi/network_analysers/ZVL/test.py @@ -0,0 +1,50 @@ +from ZVLDriver import * +import matplotlib.pyplot as plt + +# example code. profiles the per-point delay for reading data and +ip = '129.129.156.201' +ip = '169.254.83.53' +import matplotlib.pyplot as plt +print('start') +z = ZVLNetAnalyzer() +z.reset() + + +#mm, mmi, fr, frq = z.find_peak(50_000_000, 350_000_000, 20_000_000) +#plt.plot(frq, fr) +#plt.axvline(frq[mmi]) +#plt.axhline(mm) +#plt.show() + +#z.reset() +##z.set_freq_range(1_000_000, 2_000_000.5) +##z.set_freq_span(1_000_000, 10_000) +z.set_freq_range(25_750_000, 75_250_000) +plt.scatter(*(z.get_data())) + +z.reload_calibration() +#z.set_freq_span(220_000_000, 50_000_000) +plt.scatter(*(z.get_data())) +plt.show() + +#Ns = np.linspace(3, 1000, 100).astype(int) +#ts = [] +#for N in Ns: +# st = time.time() +# freqs, data = z.get_data(N) +# et = time.time() +# dt = (et-st) +# print(f'got data, {dt/N} ({dt})') +# ts += [dt] +# +#plt.scatter(Ns, ts) +#plt.show() +#plt.scatter(Ns, np.array(ts)/np.array(Ns)) +#plt.show() + +#input() +#plt.plot(*z.get_data(averaging_passes=1), alpha=0.3) +#plt.plot(*z.get_data(averaging_passes=64), alpha=0.3) +#plt.show() + +input() \ No newline at end of file diff --git a/frappy_psi/tnmr/OTFModule.py b/frappy_psi/tnmr/OTFModule.py index 643534d4..c05f8662 100644 --- a/frappy_psi/tnmr/OTFModule.py +++ b/frappy_psi/tnmr/OTFModule.py @@ -49,7 +49,7 @@ class ProgrammedSequence(fc.Readable): 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 + num_acqs: int (ct), the number of 1D scans to take per sequence obs_freq: float (MHz), the NMR frequency Commands @@ -86,6 +86,7 @@ class ProgrammedSequence(fc.Readable): pulse_height=fc.FloatRange(unit='%'), delay_time=fc.FloatRange(unit='usecs'), phase_cycle=fc.StringType()), minlen=0), default=[{'pulse_width':0,'pulse_height':0,'delay_time':0,'phase_cycle':''}]*100, readonly=False) + num_acqs_actual = fc.Parameter('num_acqs', fc.IntRange(), readonly=True, default=0) # final details acquisition_time = fc.Parameter('acquisition_time', fc.FloatRange(unit='usecs'), readonly=False, group='sequence_editor', default=204.8) # this is a limit set by the dwell limit and number of acquisition points @@ -93,7 +94,7 @@ class ProgrammedSequence(fc.Readable): pre_acquisition_time = fc.Parameter('pre_acquisition_time', fc.FloatRange(unit='usecs'), readonly=False, group='sequence_editor', default=1) post_acquisition_time = fc.Parameter('post_acquisition_time', fc.FloatRange(unit='msecs'), readonly=False, group='sequence_editor', default=500) acq_phase_cycle = fc.Parameter('acq_phase_cycle', fc.StringType(), readonly=False, group='sequence_editor', default='') - num_scans = fc.Parameter('num_scans', fc.IntRange(), readonly=False, group='sequence_editor', default=16) + num_acqs = fc.Parameter('num_acqs', 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) @@ -194,14 +195,14 @@ class ProgrammedSequence(fc.Readable): self.status = ('IDLE', 'ok - uncompiled') return self.read_acq_phase_cycle() - def read_num_scans(self): + def read_num_acqs(self): return self.tnmr().get_nmrparameter('Scans 1D') - def write_num_scans(self, t): + def write_num_acqs(self, t): if(self.status[0] != 'BUSY'): self.tnmr().set_nmrparameter('Scans 1D', t) self.status = ('IDLE', 'ok - uncompiled') - return self.read_num_scans() + return self.read_num_acqs() def read_obs_freq(self): return self.tnmr().get_nmrparameter('Observe Freq.') @@ -221,6 +222,34 @@ class ProgrammedSequence(fc.Readable): self.sequence_data = seq return self.read_sequence_data() + + def read_status(self): + if(self.tnmr().acquisition_running()): + self.status = ('BUSY', 'acquiring') + elif(self.status[1] == 'acquiring'): + # we've just finished acquiring, in frappy's perspective + self.status = ('PREPARED', 'compiled') + return self.status + + def read_value(self): + newvals = {} + try: + d = self.tnmr().get_data() + newvals['reals'] = d[0] + newvals['imags'] = d[1] + newvals['t'] = [ self.compiled_parameters['acquisition_time'] * i/len(d[0]) for i in range(0, len(d[0])) ] + except: + newvals['reals'] = [] + newvals['imags'] = [] + newvals['t'] = [] + return newvals + + def read_num_acqs_actual(self): + try: + n = self.tnmr().get_nmrparameter('Actual Scans 1D') + return n + except: + return 0 ### PRIVATE (Utility) def __compile_sequence(self): @@ -266,7 +295,7 @@ class ProgrammedSequence(fc.Readable): seq_gen.save_sequence_cfg(filename, seq) dashboard_params = { 'Observe Freq.': self.read_obs_freq(), - 'Scans 1D': self.read_num_scans(), + 'Scans 1D': self.read_num_acqs(), } self.compiled_parameters['ringdown_time'] = self.ringdown_time @@ -274,7 +303,7 @@ class ProgrammedSequence(fc.Readable): 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['num_acqs'] = self.read_num_acqs() self.compiled_parameters['obs_freq'] = self.read_obs_freq() self.compiled_parameters['title'] = self.read_title() self.compiled_parameters['comments'] = self.read_comments() @@ -303,13 +332,7 @@ class ProgrammedSequence(fc.Readable): '''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, check_time=max(self.approx_sequence_length*5, 5)) - newvals = {} - newvals['reals'] = self.tnmr().get_data()[0] - newvals['imags'] = self.tnmr().get_data()[1] - newvals['t'] = [ self.compiled_parameters['acquisition_time'] * i/1024 for i in range(0, 1024) ] - self.value = newvals - self.status = ('PREPARED', 'compiled') + self.tnmr().ZeroGo(lock=False, check_time=max(self.approx_sequence_length*5, 5)) def __compile_and_run(self, thread=True): '''Compiles and runs the currently-loaded sequence diff --git a/frappy_psi/tnmr/tnmr_interface.py b/frappy_psi/tnmr/tnmr_interface.py index a688a3a2..19ecebbc 100644 --- a/frappy_psi/tnmr/tnmr_interface.py +++ b/frappy_psi/tnmr/tnmr_interface.py @@ -411,6 +411,10 @@ class TNMR: else: print('W: Filenames do not match for sequence!') return False + + d = self.get_data() + ntnmr.ZeroFill(len(d[0])) # to clear everything out. + return True def load_dashboard(self, dashboard_fn): From 7bfc6b3cb866712b140a3b72de781258ebe4d1ed Mon Sep 17 00:00:00 2001 From: Last Davis Vern Date: Wed, 30 Jul 2025 13:56:25 +0200 Subject: [PATCH 3/3] Live data acquisition over SECoP --- frappy_psi/tnmr/OTFModule.py | 61 ++++++++++++++++---------- frappy_psi/tnmr/sequence_generation.py | 3 ++ frappy_psi/tnmr/tnmr_interface.py | 11 +++-- 3 files changed, 48 insertions(+), 27 deletions(-) diff --git a/frappy_psi/tnmr/OTFModule.py b/frappy_psi/tnmr/OTFModule.py index c05f8662..3212af69 100644 --- a/frappy_psi/tnmr/OTFModule.py +++ b/frappy_psi/tnmr/OTFModule.py @@ -27,7 +27,7 @@ import time import os import traceback -class ProgrammedSequence(fc.Readable): +class ProgrammedSequence(fc.Drivable): # Drivable only for kill() funcitonality """An NMR device being driven by an instance of TNMR. Requires that an instance of TNMR is opened before creation. Use @@ -72,7 +72,12 @@ class ProgrammedSequence(fc.Readable): 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'), default=('IDLE', 'ok - uncompiled')) + target = fc.Parameter('dummy', 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': [] }, readonly=True, visibility='w--') + + status = fc.Parameter(datatype=frappy.datatypes.StatusType(fc.Drivable, "DISABLED", 'PREPARED', 'BUSY'), default=('IDLE', 'ok - uncompiled')) pollinterval = fc.Parameter(default=1) # basic @@ -99,6 +104,7 @@ class ProgrammedSequence(fc.Readable): 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 + starting = False approx_sequence_length = 0 ### SETUP @@ -150,6 +156,20 @@ class ProgrammedSequence(fc.Readable): ### READ/WRITE + def read_status(self): + if not(self.inited): + self.status = ('ERROR', 'TNMR disconnected!') + else: + if(self.starting): + self.status = ('BUSY', 'starting') + else: + if(self.tnmr().acquisition_running()): + self.status = ('BUSY', 'acquiring') + elif(self.status[1] == 'acquiring'): + # we've just finished acquiring, in frappy's perspective + self.status = ('PREPARED', 'compiled') + return self.status + def write_title(self, t): self.title = t self.status = ('IDLE', 'ok - uncompiled') @@ -196,7 +216,7 @@ class ProgrammedSequence(fc.Readable): return self.read_acq_phase_cycle() def read_num_acqs(self): - return self.tnmr().get_nmrparameter('Scans 1D') + return int(self.tnmr().get_nmrparameter('Scans 1D')) def write_num_acqs(self, t): if(self.status[0] != 'BUSY'): @@ -223,31 +243,23 @@ class ProgrammedSequence(fc.Readable): return self.read_sequence_data() - def read_status(self): - if(self.tnmr().acquisition_running()): - self.status = ('BUSY', 'acquiring') - elif(self.status[1] == 'acquiring'): - # we've just finished acquiring, in frappy's perspective - self.status = ('PREPARED', 'compiled') - return self.status - def read_value(self): newvals = {} - try: - d = self.tnmr().get_data() - newvals['reals'] = d[0] - newvals['imags'] = d[1] - newvals['t'] = [ self.compiled_parameters['acquisition_time'] * i/len(d[0]) for i in range(0, len(d[0])) ] - except: - newvals['reals'] = [] - newvals['imags'] = [] - newvals['t'] = [] + #try: + d = self.tnmr().get_data() + newvals['reals'] = d[0] + newvals['imags'] = d[1] + newvals['t'] = [ self.compiled_parameters['acquisition_time'] * i/len(d[0]) for i in range(0, len(d[0])) ] + #except: + # newvals['reals'] = [] + # newvals['imags'] = [] + # newvals['t'] = [] return newvals def read_num_acqs_actual(self): try: n = self.tnmr().get_nmrparameter('Actual Scans 1D') - return n + return int(n) except: return 0 @@ -330,9 +342,8 @@ class ProgrammedSequence(fc.Readable): 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=False, check_time=max(self.approx_sequence_length*5, 5)) + if(self.status[0] != 'BUSY' or self.starting): + self.tnmr().ZeroGo(lock=False, check_time=max(int(self.approx_sequence_length*1.5), 5)) def __compile_and_run(self, thread=True): '''Compiles and runs the currently-loaded sequence @@ -341,9 +352,11 @@ class ProgrammedSequence(fc.Readable): ---------- thread: bool, determines if this should open a child thread and detach the process ''' + self.starting = True self.__compile_sequence() time.sleep(1.0) self.__zero_go() + self.starting = False diff --git a/frappy_psi/tnmr/sequence_generation.py b/frappy_psi/tnmr/sequence_generation.py index 928eb673..b0b366b3 100644 --- a/frappy_psi/tnmr/sequence_generation.py +++ b/frappy_psi/tnmr/sequence_generation.py @@ -41,6 +41,9 @@ def get_single_pulse_block(name, pulse_width, pulse_height, delay_time, phase_cy delay_time = float(delay_time.strip()[:-1]) / 1e6 else: delay_time = float(delay_time.strip()) # assume in us + + delay_time_rounded = int(delay_time*10) / 10 # nearest 10ns + delay_time = delay_time_rounded ph = name + '_phase' rl = name + '_delay' diff --git a/frappy_psi/tnmr/tnmr_interface.py b/frappy_psi/tnmr/tnmr_interface.py index 19ecebbc..551847c4 100644 --- a/frappy_psi/tnmr/tnmr_interface.py +++ b/frappy_psi/tnmr/tnmr_interface.py @@ -155,8 +155,8 @@ class TNMR: print('Zero-going...') ntnmr = self.get_instance() if not(self.acquisition_running()): - print('Reset') - ntnmr.Reset() # to avoid hardware issues? EDIT: Doesn't seem to do much... + #print('Reset') + #ntnmr.Reset() # to avoid hardware issues? EDIT: Doesn't seem to do much... if(CHECK_MODE == 'data'): print('Artificially setting the zeroth point to NULL for error detection.') ntnmr.SetDataPoint(1, [0,0]) @@ -258,7 +258,7 @@ class TNMR: """ print('I: Saving') if filepath == '': - self.get_instance().Save + self.get_instance().Save() else: self.get_instance().SaveAs(filepath) print(f'I: Saved to file {filepath}') @@ -378,6 +378,11 @@ class TNMR: """ ntnmr = self.get_instance() + + if (self.acquisition_running()): + ntnmr.Abort() + print('W: Aborting currently running acquisition!') + print(f'Loading sequence at {filename}') ntnmr.CloseActiveFile() success = ntnmr.OpenFile(TEMPLATE_FILE_PATH + 'tmp.tnt')