diff --git a/bin/peus-plot b/bin/peus-plot new file mode 100644 index 0000000..76181b2 --- /dev/null +++ b/bin/peus-plot @@ -0,0 +1,46 @@ +#!/usr/bin/env python3 +import sys +from frappy.client.interactive import Client +from frappy_psi.iqplot import Plot +import numpy as np +import matplotlib.pyplot as plt + +if len(sys.argv) < 2: + print('Usage: python peusplot.py ') + + +def get_modules(name): + return list(filter(None, (globals().get(name % i) for i in range(10)))) + + +secnode = Client('localhost:5000') +time_size = {'time', 'size'} +int_mods = [u] + get_modules('roi%d') +t_rois = get_modules('roi%d') +i_rois = get_modules('roi%di') +q_rois = get_modules('roi%dq') + +if len(sys.argv) > 1: + maxy = float(sys.argv[1]) +else: + maxy = 0.02 + + +iqplot = Plot(maxy) + +for i in range(99): + pass + +try: + while True: + curves = np.array(u.get_curves()) + iqplot.plot(curves, + rois=[(r.time, r.time + r.size) for r in int_mods], + average=([r.time for r in t_rois], + [r.value for r in i_rois], + [r.value for r in q_rois])) + plt.pause(0.5) + if iqplot.fig is None: # graph window closed + break +except KeyboardInterrupt: + iqplot.close() diff --git a/bin/us-plot b/bin/us-plot new file mode 100644 index 0000000..7421c59 --- /dev/null +++ b/bin/us-plot @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +import sys +from frappy.client.interactive import Client +import numpy as np +import matplotlib.pyplot as plt + +Client('pc13252:5000') + + +def plot(array, ax, style, xs): + xaxis = np.arange(len(array)) * xs + return ax.plot(xaxis, array, style)[0] + + +def update(array, line, xs): + xaxis = np.arange(len(array)) * xs + line.set_data(np.array([xaxis, array])) + +def on_close(event): + sys.exit(0) + +# inp_signal.read() +# out_signal.read() + +if len(sys.argv) < 2: + print(""" + Usage: python3 sig.py [ []] + + end: end of window [ns] + start: start of window [n2], default: 0 + npoints: number fo points (default 1000) + """) +else: + start = 0 + end = float(sys.argv[1]) + npoints = 1000 + if len(sys.argv) > 2: + start = float(sys.argv[2]) + if len(sys.argv) > 3: + npoints = float(sys.argv[3]) + + fig, ax = plt.subplots(figsize=(15,3)) + fig.canvas.mpl_connect('close_event', on_close) + try: + get_signal = iq.get_signal + print('plotting RUS signal') + except NameError: + get_signal = u.get_signal + print('plotting PE signal') + + xs, signal = get_signal(start, end, npoints) + + lines = [plot(s, ax, '-', xs) for s in signal] + + while True: + plt.pause(0.5) + plt.draw() + xs, signal = get_signal(start, end, npoints) + for line, sig in zip(lines, signal): + update(sig, line, xs) diff --git a/cfg/PEUS_cfg.py b/cfg/PEUS_cfg.py new file mode 100644 index 0000000..77cd34d --- /dev/null +++ b/cfg/PEUS_cfg.py @@ -0,0 +1,87 @@ +Node('pulse_echo.cfg', + 'ultrasound, pulse_echo configuration', + interface='5000', +) + +Mod('u', + 'frappy_psi.ultrasound.PulseEcho', + 'ultrasound acquisition loop', + freq='f', + # pollinterval=0.1, + time=900.0, + size=5000.0, + nr=500, + sr=32768, + bw=1e7, +) + +Mod('fio', + 'frappy_psi.ultrasound.FreqStringIO', '', + uri='serial:///dev/ttyS1?baudrate=57600', +) + +Mod('f', + 'frappy_psi.ultrasound.Frequency', + 'writable for frequency', + output='R', # L for LF (bnc), R for RF (type N) + io='fio', + amp=0.5, # VPP +) + +Mod('fdif', + 'frappy_psi.ultrasound.FrequencyDif', + 'writable for frequency minus base frequency', + freq='f', + base=41490200.0, +) + +# Mod('curves', +# 'frappy_psi.ultrasound.Curves', +# 't, I, Q and pulse arrays for plot', +# ) + +def roi(name, time, size, components='iqpa', enable=True, control=False, freq=None, **kwds): + description = 'I/Q of region {name}' + if freq: + kwds.update(cls='frappy_psi.ultrasound.ControlRoi', + description=f'{description} as control loop', + freq=freq, **kwds) + else: + kwds.update(cls='frappy_psi.ultrasound.Roi', + description=description, **kwds) + kwds.update({c: name + c for c in components}) + Mod(name, + main='u', + time=time, + size=size, + enable=enable, + **kwds, + ) + for c in components: + Mod(name + c, + 'frappy.modules.Readable', + f'{name}{c} component', + ) + +# control loop +roi('roi0', 2450, 300, freq='f', maxstep=100000, minstep=4000) +# other rois +roi('roi1', 5950, 300) +roi('roi2', 9475, 300) +roi('roi3', 12900, 300) +#roi('roi4', 400, 30, False) +#roi('roi5', 400, 30, False) +#roi('roi6', 400, 30, False) +#roi('roi7', 400, 30, False) +#roi('roi8', 400, 30, False) +#roi('roi9', 400, 30, False) + +Mod('delay', + 'frappy_psi.dg645.Delay', + 'delay line with 2 channels', + uri='serial:///dev/ttyS2', + on1=1e-09, + on2=1e-09, + off1=4e-07, + off2=6e-07, +) diff --git a/cfg/RUS_cfg.py b/cfg/RUS_cfg.py new file mode 100644 index 0000000..db506b1 --- /dev/null +++ b/cfg/RUS_cfg.py @@ -0,0 +1,39 @@ +Node(equipment_id = 'r_ultrasound.psi.ch', + description = 'resonant ultra sound setup', + interface = 'tcp://5000', +) + +Mod('iq', + cls = 'frappy_psi.ultrasound.RUS', + description = 'ultrasound iq mesurement', + imod = 'i', + qmod = 'q', + freq='f', + input_range=10, # VPP + input_delay = 0, + periods = 163, + ) + +Mod('freqio', + 'frappy_psi.ultrasound.FreqStringIO', + ' ', + uri = 'serial:///dev/ttyS1?baudrate=57600', +) + +Mod('f', + cls = 'frappy_psi.ultrasound.Frequency', + description = 'ultrasound frequency', + io='freqio', + output='L', # L for LF (bnc), R for RF (type N) + target=10000, +) + +Mod('i', + cls='frappy.modules.Readable', + description='I component', +) + +Mod('q', + cls='frappy.modules.Readable', + description='Q component', +) diff --git a/frappy_psi/adq_mr.py b/frappy_psi/adq_mr.py index 803c42a..90d45f2 100644 --- a/frappy_psi/adq_mr.py +++ b/frappy_psi/adq_mr.py @@ -50,6 +50,27 @@ GHz = 1e9 RMS_TO_VPP = 2 * np.sqrt(2) +class Timer: + def __init__(self): + self.data = [(time.time(), 'start')] + + def __call__(self, text=''): + now = time.time() + prev = self.data[-1][0] + self.data.append((now, text)) + return now - prev + + def summary(self): + return ' '.join(f'{txt} {tim:.3f}' for tim, txt in self.data[1:]) + + def show(self): + first = prev = self.data[0][0] + print('---', first) + for tim, txt in self.data[1:]: + print(f'{(tim - first) * 1000:9.3f} {(tim - prev) * 1000:9.3f} ms {txt}') + prev = tim + + class Adq: sample_rate = 2 * GHz max_number_of_channels = 2 @@ -161,7 +182,6 @@ class Adq: ADQAPI.ADQ_DisarmTrigger(self.adq_cu, self.adq_num) ADQAPI.ADQ_ArmTrigger(self.adq_cu, self.adq_num) self.data = data - self.starttime = time.time() def get_data(self): """get new data if available""" @@ -173,12 +193,14 @@ class Adq: if ADQAPI.ADQ_GetAcquiredAll(self.adq_cu, self.adq_num): ready = True + data.timer('ready') else: if self.trigger == SW_TRIG: ADQAPI.ADQ_SWTrig(self.adq_cu, self.adq_num) if not ready: self.busy = True return None + self.data = None t = time.time() # Get data from ADQ if not ADQAPI.ADQ_GetData( @@ -187,7 +209,6 @@ class Adq: 0, self.number_of_records, ADQ_CHANNELS_MASK, 0, self.samples_per_record, ADQ_TRANSFER_MODE_NORMAL): raise RuntimeError('no success from ADQ_GetDATA') - self.data = None data.retrieve(self) return data @@ -197,12 +218,20 @@ class PEdata: self.sample_rate = adq.sample_rate self.samp_freq = self.sample_rate / GHz self.number_of_records = adq.number_of_records + self.timer = Timer() + + def retrieve(self, adq): data = [] + rawsignal = [] for ch in range(2): onedim = np.frombuffer(adq.target_buffers[ch].contents, dtype=np.int16) - data.append(onedim.reshape(adq.number_of_records, adq.samples_per_record) / float(2**14)) # 14 bits ADC + rawsignal.append(onedim[:adq.samples_per_record]) + # convert 16 bit int to a value in the range -1 .. 1 + data.append(onedim.reshape(adq.number_of_records, adq.samples_per_record) / float(2 ** 15)) # Now this is an array with all records, but the time is artificial self.data = data + self.rawsignal = rawsignal + self.timer('retrieved') def sinW(self, sig, freq, ti, tf): # sig: signal array @@ -232,6 +261,7 @@ class PEdata: wave1 = sigout * (a * np.cos(2*np.pi*freq*t) + b * np.sin(2*np.pi*freq*t)) wave2 = sigout * (a * np.sin(2*np.pi*freq*t) - b * np.cos(2*np.pi*freq*t)) return wave1, wave2 + def averageiq(self, data, freq, ti, tf): """Average over records""" iorq = np.array([self.mix(data[0][i], data[1][i], freq, ti, tf) for i in range(self.number_of_records)]) @@ -254,24 +284,32 @@ class PEdata: def gates_and_curves(self, freq, pulse, roi, bw_cutoff): """return iq values of rois and prepare plottable curves for iq""" - self.ndecimate = int(round(self.sample_rate / freq)) - # times = [] - # times.append(('aviq', time.time())) + self.timer('gates') + try: + self.ndecimate = int(round(self.sample_rate / freq)) + except TypeError as e: + raise TypeError(f'{self.sample_rate}/{freq} {e}') iq = self.averageiq(self.data, freq / GHz, *pulse) - # times.append(('filtro', time.time())) + self.timer('aviq') iqf = self.filtro(iq, bw_cutoff) - m = len(iqf[0]) // self.ndecimate + self.timer('filtro') + m = max(1, len(iqf[0]) // self.ndecimate) ll = m * self.ndecimate iqf = [iqfx[0:ll] for iqfx in iqf] - # times.append(('iqdec', time.time())) + self.timer('iqf') iqd = np.average(np.resize(iqf, (2, m, self.ndecimate)), axis=2) + self.timer('avg') t_axis = np.arange(m) * self.ndecimate / self.samp_freq pulsig = np.abs(self.data[0][0]) - # times.append(('pulsig', time.time())) + self.timer('pulsig') pulsig = np.average(np.resize(pulsig, (m, self.ndecimate)), axis=1) - self.curves = (t_axis, iqd[0], iqd[1], pulsig) - # print(times) - return [self.box(iqf, *r) for r in roi] + result = ([self.box(iqf, *r) for r in roi], # gates + (t_axis, iqd[0], iqd[1], pulsig)) # curves + self.timer('result') + # self.timer.show() + # ns = len(self.rawsignal[0]) * self.number_of_records + # print(f'{ns} {ns / 2e6} ms') + return result class Namespace: @@ -290,24 +328,40 @@ class RUSdata: self.inp = Namespace(idx=0, name='input') self.out = Namespace(idx=1, name='output') self.channels = (self.inp, self.out) + self.timer = Timer() def retrieve(self, adq): + self.timer('start retrieve') npts = self.samples_per_record - self.delay_samples - complex_sinusoid = np.exp(1j * 2 * np.pi * self.freq / self.sample_rate * np.arange(npts)) + nbin = max(1, npts // (self.periods * 60)) # for performance reasons, do the binning first + nreduced = npts // nbin + ft = 2 * np.pi * self.freq * nbin / self.sample_rate * np.arange(nreduced) + self.timer('create time axis') + # complex_sinusoid = np.exp(1j * ft) # do not use this, below is 33 % faster + complex_sinusoid = 1j * np.sin(ft) + np.cos(ft) + self.timer('sinusoid') + + rawsignal = [] # for raw plot for chan in self.channels: # looping over input and output - # although the ADC is only 14 bit it is representet as unsigend 16 bit numbers, + # although the ADC is only 14 bit it is represented as unsigend 16 bit numbers, # and due to some calculations (calibration) the last 2 bits are not zero - isignal = np.frombuffer(adq.target_buffers[chan.idx].contents, dtype=np.int16)[self.delay_samples:] - # importatn convert to float first, before doing any calculations. - # calculations with int16 may silently overflow - chan.signal = isignal / float(2 ** 16) # in V -> peak to peak 1 V ~ +- 0.5 V - chan.ovr_rate = (np.abs(isignal) > 32000).sum() / npts # fraction of points near end of range + beg = self.delay_samples + isignal = np.frombuffer(adq.target_buffers[chan.idx].contents, dtype=np.int16)[beg:beg+nreduced * nbin] + self.timer('isignal') + reduced = isignal.reshape((-1, nbin)).mean(axis=1) # this converts also int16 to float + self.timer('reduce') + rawsignal.append(reduced) + chan.signal = signal = reduced * 2 ** -16 # in V -> peak to peak 1 V ~ +- 0.5 V + self.timer('divide') # calculate RMS * sqrt(2) -> peak sinus amplitude. # may be higher than the input range by a factor 1.4 when heavily clipped - signal = chan.signal - np.median(chan.signal) chan.amplitude = np.sqrt((signal ** 2).mean()) * RMS_TO_VPP + self.timer('amp') chan.mixed = signal * complex_sinusoid + self.timer('mix') chan.mean = chan.mixed.mean() + self.timer('mean') + self.rawsignal = rawsignal if self.inp.mean: self.iq = self.out.mean / self.inp.mean else: @@ -324,15 +378,19 @@ class RUSdata: the imaginary part indicates a turning phase (rad/sec) the real part indicates changes in amplitude (0.01 ~= 1%/sec) """ + self.timer('get_quality') npts = len(self.channels[0].signal) nper = npts // self.periods - nbin = 50 # 50 samples, 25 ns for chan in self.channels: mean = chan.mixed.mean() chan.reduced = chan.mixed[:self.periods * nper].reshape((-1, nper)).mean(axis=1) / mean - chan.binned = chan.signal[:npts // nbin * nbin].reshape((-1, nbin)).mean(axis=1) timeaxis = np.arange(len(self.out.reduced)) * self.sample_rate / self.freq - return Namespace( - input_stddev = self.inp.reduced.std(), - output_slope = np.polyfit(timeaxis, self.out.reduced, 1)[0]) + result = Namespace( + input_stddev=self.inp.reduced.std(), + output_slope=np.polyfit(timeaxis, self.out.reduced, 1)[0]) + self.timer('got_quality') + self.timer.show() + ns = len(self.rawsignal[0]) + print(f'{ns} {ns / 2e6} ms') + return result diff --git a/frappy_psi/iqplot.py b/frappy_psi/iqplot.py index 43a48d3..280a7ed 100644 --- a/frappy_psi/iqplot.py +++ b/frappy_psi/iqplot.py @@ -7,10 +7,12 @@ Created on Tue Feb 4 11:07:56 2020 import numpy as np import matplotlib.pyplot as plt +NAN = float('nan') + + def rect(x1, x2, y1, y2): return np.array([[x1,x2,x2,x1,x1],[y1,y1,y2,y2,y1]]) -NAN = float('nan') def rects(intervals, y12): result = [rect(*intervals[0], *y12)] @@ -19,13 +21,14 @@ def rects(intervals, y12): result.append(rect(*x12, *y12)) return np.concatenate(result, axis=1) + class Plot: def __init__(self, maxy): self.lines = {} self.yaxis = ((-2 * maxy, maxy), (-maxy, 2 * maxy)) self.first = True self.fig = None - + def set_line(self, iax, name, data, fmt, **kwds): """ plot or update a line @@ -54,6 +57,9 @@ class Plot: self.fig = None self.first = True + def on_close(self, event): + self.fig = None + def plot(self, curves, rois=None, average=None): boxes = rects(rois[1:], self.yaxis[0]) pbox = rect(*rois[0], *self.yaxis[1]) @@ -68,6 +74,7 @@ class Plot: if self.first: plt.ion() self.fig, axleft = plt.subplots(figsize=(15,7)) + self.fig.canvas.mpl_connect('close_event', self.on_close) plt.title("I/Q", fontsize=14) axleft.set_xlim(0, curves[0][-1]) self.ax = [axleft, axleft.twinx()] @@ -95,7 +102,7 @@ class Plot: plt.tight_layout() finally: self.first = False - + plt.draw() self.fig.canvas.draw() self.fig.canvas.flush_events() diff --git a/frappy_psi/ultrasound.py b/frappy_psi/ultrasound.py index e0ace10..1a50669 100644 --- a/frappy_psi/ultrasound.py +++ b/frappy_psi/ultrasound.py @@ -26,9 +26,11 @@ import numpy as np from frappy_psi.adq_mr import Adq, PEdata, RUSdata from frappy.core import Attached, BoolType, Done, FloatRange, HasIO, StatusType, \ - IntRange, Module, Parameter, Readable, Writable, Drivable, StringIO, StringType, \ - IDLE, BUSY, DISABLED, WARN, ERROR, TupleOf, ArrayOf, Command, Attached, EnumType + IntRange, Module, Parameter, Readable, Writable, StatusType, StringIO, StringType, \ + IDLE, BUSY, DISABLED, WARN, ERROR, TupleOf, ArrayOf, Command, Attached, EnumType ,\ + Drivable from frappy.properties import Property +from frappy.lib import clamp # from frappy.modules import Collector Collector = Readable @@ -46,11 +48,12 @@ def fname_from_time(t, extension): class Roi(Readable): main = Attached() + a = Attached(mandatory=False) # amplitude Readable + p = Attached(mandatory=False) # phase Readable + i = Attached(mandatory=False) # i Readable + q = Attached(mandatory=False) # amplitude Readable - value = Parameter('amplitude', FloatRange(), default=0) - phase = Parameter('phase', FloatRange(unit='deg'), default=0) - i = Parameter('in phase', FloatRange(), default=0) - q = Parameter('out of phase', FloatRange(), default=0) + value = Parameter('i, q', TupleOf(FloatRange(), FloatRange()), default=(0, 0)) time = Parameter('start time', FloatRange(unit='nsec'), readonly=False) size = Parameter('interval (symmetric around time)', FloatRange(unit='nsec'), readonly=False) enable = Parameter('calculate this roi', BoolType(), readonly=False, default=True) @@ -80,6 +83,49 @@ class Roi(Readable): return Done +class ControlRoi(Roi, Writable): + freq = Attached() + target = Parameter(datatype=Roi.value.datatype) + maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False, + default=10000) + minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'), + readonly=False, default=4000) + slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False, + default=1e6) + control_active = Parameter('are we controlling?', BoolType(), readonly=False) + + _freq_target = None + _skipctrl = 2 + _old = None + + def doPoll(self): + inphase = self.value[0] + freq = self.freq.target + if freq != self._freq_target: + self._freq_target = freq + # do no control 2 times after changing frequency + self._skipctrl = 2 + if self.control_active: + if self._old: + newfreq = freq + inphase * self.slope + fdif = freq - self._old[0] + if abs(fdif) >= self.minstep: + idif = inphase - self._old[1] + self.slope = - fdif / idif + else: + # do a 'test' step + newfreq = freq + self.minstep + self.old = (freq, inphase) + if self._skipctrl > 0: # do no control for some time after changing frequency + self._skipctrl -= 1 + elif self.control_active: + self._freq_target = self.freq.write_target(clamp(freq - self.maxstep, newfreq, freq + self.maxstep)) + + def write_target(self, value): + self.control_active = True + return 0 + + class Pars(Module): description = 'relevant parameters from SEA' @@ -90,44 +136,75 @@ class Pars(Module): class FreqStringIO(StringIO): - end_of_line = '\r' + end_of_line = '\r\n' -class Frequency(HasIO, Writable): +class Frequency(HasIO, Drivable): value = Parameter('frequency', unit='Hz') amp = Parameter('amplitude (VPP)', FloatRange(unit='V'), readonly=False) output = Parameter('output: L or R', EnumType(L=1, R=0), readonly=False, default='L') + last_change = 0 ioClass = FreqStringIO dif = None - _freq = None + _started = 0 + _within_write_target = False + _nopoll_until = 0 + + def doPoll(self): + super().doPoll() + if self.isBusy() and time.time() > self._started + 5: + self.status = WARN, 'acquisition timeout' def register_dif(self, dif): self.dif = dif def read_value(self): - if self._freq is None: - self._freq = float(self.communicate('FREQ?')) - return self._freq + if time.time() > self._nopoll_until or self.value == 0: + self.value = float(self.communicate('FREQ?')) + if self.dif: + self.dif.read_value() + return self.value + + def set_busy(self): + """called by an acquisition module + + from a callback on value within write_target + """ + if self._within_write_target: + self._started = time.time() + self.log.info('set busy') + self.status = BUSY, 'waiting for acquisition' + + def set_idle(self): + if self.isBusy(): + self.status = IDLE, '' def write_target(self, value): - self._freq = float(self.communicate('FREQ %.15g;FREQ?' % value)) - self.last_change = time.time() - if self.dif: - self.dif.read_value() - self.read_value() - return self._freq + self._nopoll_until = time.time() + 10 + try: + self._within_write_target = True + # may trigger busy=True from an acquisition module + self.value = float(self.communicate('FREQ %.15g;FREQ?' % value)) + self.last_change = time.time() + if self.dif: + self.dif.read_value() + return self.value + finally: + self._within_write_target = False def write_amp(self, amp): - reply = self.communicate(f'AMP{self.output.name} {amp} VPP;AMP{self.output.name}? VPP') - return float(reply) + self._nopoll_until = time.time() + 10 + self.amp = float(self.communicate(f'AMP{self.output.name} {amp} VPP;AMP{self.output.name}? VPP')) + return self.amp def read_amp(self): - reply = self.communicate(f'AMP{self.output.name}? VPP') - return float(reply) + if time.time() > self._nopoll_until or self.amp == 0: + return float(self.communicate(f'AMP{self.output.name}? VPP')) + return self.amp -class FrequencyDif(Readable): +class FrequencyDif(Drivable): freq = Attached(Frequency) base = Parameter('base frequency', FloatRange(unit='Hz'), default=0) value = Parameter('difference to base frequency', FloatRange(unit='Hz'), default=0) @@ -136,25 +213,54 @@ class FrequencyDif(Readable): super().initModule() self.freq.register_dif(self) + def write_value(self, target): + self.freq.write_target(target + self.base) + return self.value # this was updated in Frequency + def read_value(self): - return self.freq - self.base + return self.freq.value - self.base + + def read_status(self): + return self.freq.read_status() class Base: freq = Attached() sr = Parameter('samples per record', datatype=IntRange(1, 1E9), default=16384) adq = None + _rawsignal = None + _fast_poll = 0.001 def shutdownModule(self): if self.adq: self.adq.deletecu() self.adq = None + @Command(argument=TupleOf(FloatRange(unit='ns'), FloatRange(unit='ns'), IntRange(0,99999)), + result=TupleOf(FloatRange(), + ArrayOf(ArrayOf(IntRange(-0x7fff, 0x7fff), 0, 99999)))) + def get_signal(self, start, end, npoints): + """get signal + + :param start: start time (ns) + :param end: end time (ns) + :param npoints: hint for number of data points + :return: (, array of array of y) + + for performance reasons the result data is rounded to int16 + """ + # convert ns to samples + sr = self.adq.sample_rate * 1e-9 + istart = round(start * sr) + iend = min(self.sr, round(end * sr)) + nbin = max(1, round((iend - istart) / npoints)) + iend = iend // nbin * nbin + return (nbin / sr, + [np.round(ch[istart:iend].reshape((-1, nbin)).mean(axis=1)) for ch in self._rawsignal]) -class PulseEcho(Base): - value = Parameter("t, i, q, pulse curves", - TupleOf(*[ArrayOf(FloatRange(), 0, 16283) for _ in range(4)]), default=[[]] * 4) +class PulseEcho(Base, Readable): + value = Parameter(default=0) nr = Parameter('number of records', datatype=IntRange(1, 9999), default=500) bw = Parameter('bandwidth lowpassfilter', datatype=FloatRange(unit='Hz'), default=10E6) control = Parameter('control loop on?', BoolType(), readonly=False, default=True) @@ -163,6 +269,8 @@ class PulseEcho(Base): size = Parameter('pulse length (starting from time)', FloatRange(unit='nsec'), readonly=False) pulselen = Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1) + # curves = Attached(mandatory=False) + pollinterval = Parameter('poll interval', datatype=FloatRange(0,120)) _starttime = None @@ -171,6 +279,49 @@ class PulseEcho(Base): self.adq = Adq() self.adq.init(self.sr, self.nr) self.roilist = [] + self.setFastPoll(True, self._fast_poll) + + def doPoll(self): + try: + data = self.adq.get_data() + except Exception as e: + self.status = ERROR, repr(e) + return + if data is None: + if self.adq.busy: + return + self.adq.start(PEdata(self.adq)) + self.setFastPoll(True, self._fast_poll) + return + + roilist = [r for r in self.roilist if r.enable] + freq = self.freq.read_value() + if not freq: + self.log.info('freq=0') + return + gates, curves = data.gates_and_curves( + freq, (self.time, self.time + self.size), + [(r.time, r.time + r.size) for r in roilist], self.bw) + for i, roi in enumerate(roilist): + a = gates[i][0] + b = gates[i][1] + roi.value = a, b + if roi.i: + roi.i.value = a + if roi.q: + roi.q.value = b + if roi.a: + roi.a.value = math.sqrt(a ** 2 + b ** 2) + if roi.p: + roi.p.value = math.atan2(a, b) * 180 / math.pi + self._curves = curves + self._rawsignal = data.rawsignal + + @Command(result=TupleOf(*[ArrayOf(FloatRange(), 0, 99999) + for _ in range(4)])) + def get_curves(self): + """retrieve curves""" + return self._curves def write_nr(self, value): self.adq.init(self.sr, value) @@ -184,46 +335,6 @@ class PulseEcho(Base): def register_roi(self, roi): self.roilist.append(roi) - # TODO: fix - # def go(self): - # self._starttime = time.time() - # self.adq.start() - - def read_value(self): - # TODO: data = self.get_data() - if self.get_rawdata(): # new data available - roilist = [r for r in self.roilist if r.enable] - freq = self.freq.value - gates = self.adq.gates_and_curves(self._data, freq, - (self.time, self.time + self.size), - [r.interval for r in roilist]) - for i, roi in enumerate(roilist): - roi.i = a = gates[i][0] - roi.q = b = gates[i][1] - roi.value = math.sqrt(a ** 2 + b ** 2) - roi.phase = math.atan2(a, b) * 180 / math.pi - return self.adq.curves - - # TODO: CONTROL - # inphase = self.roilist[0].i - # if self.control: - # newfreq = freq + inphase * self.slope - self.basefreq - # # step = sorted((-self.maxstep, inphase * self.slope, self.maxstep))[1] - # if self.old: - # fdif = freq - self.old[0] - # idif = inphase - self.old[1] - # if abs(fdif) >= self.minstep: - # self.slope = - fdif / idif - # else: - # fdif = 0 - # idif = 0 - # newfreq = freq + self.minstep - # self.old = (freq, inphase) - # if self.skipctrl > 0: # do no control for some time after changing frequency - # self.skipctrl -= 1 - # elif self.control: - # self.freq = sorted((self.freq - self.maxstep, newfreq, self.freq + self.maxstep))[1] - CONTINUE = 0 GO = 1 @@ -235,26 +346,21 @@ class RUS(Base, Collector): freq = Attached() imod = Attached(mandatory=False) qmod = Attached(mandatory=False) - input_signal = Attached(mandatory=False) - output_signal = Attached(mandatory=False) value = Parameter('averaged (I, Q) tuple', TupleOf(FloatRange(), FloatRange())) status = Parameter(datatype=StatusType(Readable, 'BUSY')) - periods = Parameter('number of periods', IntRange(1, 9999), default=12) + periods = Parameter('number of periods', IntRange(1, 999999), default=12, readonly=False) input_delay = Parameter('throw away everything before this time', FloatRange(unit='ns'), default=10000, readonly=False) - input_range = Parameter('input range (taking in to account attenuation)', FloatRange(unit='V'), + input_range = Parameter('input range (taking into account attenuation)', FloatRange(unit='V'), default=10, readonly=False) output_range = Parameter('output range', FloatRange(unit='V'), default=1, readonly=False) - input_phase_stddev = Parameter('input signal quality', FloatRange(unit='rad'), default=0) - output_phase_slope = Parameter('output signal phase slope', FloatRange(unit='rad/sec'), default=0) - output_amp_slope = Parameter('output signal amplitude change', FloatRange(unit='1/sec'), default=0) input_amplitude = Parameter('input signal amplitude', FloatRange(unit='V'), default=0) output_amplitude = Parameter('output signal amplitude', FloatRange(unit='V'), default=0) phase = Parameter('phase', FloatRange(unit='deg'), default=0) amp = Parameter('amplitude', FloatRange(), default=0) continuous = Parameter('continuous mode', BoolType(), readonly=False, default=True) - pollinterval = Parameter(datatype=FloatRange(0, 120), default=1) + pollinterval = Parameter(datatype=FloatRange(0, 120), default=5) _starttime = None _iq = 0 @@ -262,16 +368,26 @@ class RUS(Base, Collector): _action = CONTINUE # one of CONTINUE, GO, DONE_GO, WAIT_GO _status = IDLE, 'no data yet' _busy = False # waiting for end of aquisition (not the same as self.status[0] == BUSY) + _requested_freq = None def initModule(self): super().initModule() self.adq = Adq() - self._ovr_rate = {} self.freq.addCallback('value', self.update_freq) + self.freq.addCallback('target', self.update_freq_target) # self.write_periods(self.periods) + def update_freq_target(self, value): + self.go() + def update_freq(self, value): - self.setFastPoll(True, 0.001) + self.setFastPoll(True, self._fast_poll) + self._requested_freq = value + self.freq.set_busy() # is only effective when the update was trigger within freq.write_target + + def get_quality_info(self, data): + """hook for RESqual""" + data.timer.show() def doPoll(self): try: @@ -283,22 +399,10 @@ class RUS(Base, Collector): self.wait_until = time.time() + 2 return - self.setFastPoll(False) if data: # this is new data - self._data = data - for chan in data.channels: - if chan.ovr_rate: - self._ovr_rate[chan.name] = chan.ovr_rate * 100 - else: - self._ovr_rate.pop(chan.name, None) - qual = data.get_quality() - if self.input_signal: - self.input_signal.value = np.round(data.channels[0].binned, 3) - if self.output_signal: - self.output_signal.value = np.round(data.channels[1].binned, 3) - self.input_phase_stddev = qual.input_stddev.imag - self.output_phase_slope = qual.output_slope.imag - self.output_amp_slope = qual.output_slope.real + self._busy = False + self.get_quality_info(data) # hook for RUSqual + self._rawsignal = data.rawsignal self.input_amplitude = data.inp.amplitude * self.input_range self.output_amplitude = data.out.amplitude * self.output_range self._iq = iq = data.iq * self.output_range / self.input_range @@ -306,18 +410,33 @@ class RUS(Base, Collector): self.amp = np.abs(iq) self.read_value() self.set_status(IDLE, '') + if self.freq.isBusy(): + if data.freq == self._requested_freq: + self.log.info('set freq idle %.3f', time.time() % 1.0) + self.freq.set_idle() + else: + self.log.warn('freq does not match: requested %.14g, from data: %.14g', + self._requested_freq, data.freq) + else: + self.log.info('freq not busy %.3f', time.time() % 1.0) + if self._action == CONTINUE: + self.setFastPoll(False) + self.log.info('slow') + return elif self._busy: - self._busy = False if self._action == DONE_GO: + self.log.info('busy') self.set_status(BUSY, 'acquiring') else: self.set_status(IDLE, 'acquiring') - return + return if self._action == CONTINUE and self.continuous: + print('CONTINUE') self.start_acquisition() self.set_status(IDLE, 'acquiring') return if self._action == GO: + print('pending GO') self.start_acquisition() self._action = DONE_GO self.set_status(BUSY, 'acquiring') @@ -339,8 +458,6 @@ class RUS(Base, Collector): self.read_status() def read_status(self): - if self._ovr_rate and self._status[0] < WARN: - return WARN, 'overrange on %s' % ' and '.join(self._ovr_rate) return self._status def read_value(self): @@ -352,7 +469,8 @@ class RUS(Base, Collector): @Command def go(self): - """start aquisition""" + """start acquisition""" + self.log.info('go %.3f', time.time() % 1.0) if self._busy: self._action = GO else: @@ -362,54 +480,25 @@ class RUS(Base, Collector): self.read_status() def start_acquisition(self): + self.log.info('start %.3f', time.time() % 1.0) freq = self.freq.read_value() self.sr = round(self.periods * self.adq.sample_rate / freq) delay_samples = round(self.input_delay * self.adq.sample_rate * 1e-9) self.adq.init(self.sr + delay_samples, 1) self.adq.start(RUSdata(self.adq, freq, self.periods, delay_samples)) self._busy = True - self.setFastPoll(True, 0.001) + self.setFastPoll(True, self._fast_poll) -class Signal(Readable): - value = Parameter('pulse', ArrayOf(FloatRange(), maxlen=9999)) +class RUSqual(RUS): + """version with additional info about quality of input and output signal""" + input_phase_stddev = Parameter('input signal quality', FloatRange(unit='rad'), default=0) + output_phase_slope = Parameter('output signal phase slope', FloatRange(unit='rad/sec'), default=0) + output_amp_slope = Parameter('output signal amplitude change', FloatRange(unit='1/sec'), default=0) -class ControlLoop(Module): - roi = Attached(Roi) - maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False, - default=10000) - minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'), - readonly=False, default=4000) - slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False, - default=1e6) - - -# class Frequency(HasIO, Readable): -# pars = Attached() -# curves = Attached(mandatory=False) -# maxy = Property('plot y scale', datatype=FloatRange(), default=0.5) -# -# value = Parameter('frequency@I,q', datatype=FloatRange(unit='Hz'), default=0) -# basefreq = Parameter('base frequency', FloatRange(unit='Hz'), readonly=False) -# nr = Parameter('number of records', datatype=IntRange(1,10000), default=500) -# sr = Parameter('samples per record', datatype=IntRange(1,1E9), default=16384) -# freq = Parameter('target frequency', FloatRange(unit='Hz'), readonly=False) -# bw = Parameter('bandwidth lowpassfilter', datatype=FloatRange(unit='Hz'),default=10E6) -# amp = Parameter('amplitude', FloatRange(unit='dBm'), readonly=False) -# control = Parameter('control loop on?', BoolType(), readonly=False, default=True) -# rusmode = Parameter('RUS mode on?', BoolType(), readonly=False, default=False) -# time = Parameter('pulse start time', FloatRange(unit='nsec'), -# readonly=False) -# size = Parameter('pulse length (starting from time)', FloatRange(unit='nsec'), -# readonly=False) -# pulselen = Parameter('adjusted pulse length (integer number of periods)', FloatRange(unit='nsec'), default=1) -# maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False, -# default=10000) -# minstep = Parameter('min frequency step for slope calculation', FloatRange(unit='Hz'), -# readonly=False, default=4000) -# slope = Parameter('inphase/frequency slope', FloatRange(), readonly=False, -# default=1e6) -# plot = Parameter('create plot images', BoolType(), readonly=False, default=True) -# save = Parameter('save data', BoolType(), readonly=False, default=True) -# pollinterval = Parameter(datatype=FloatRange(0,120)) + def get_quality_info(self, data): + qual = data.get_quality() + self.input_phase_stddev = qual.input_stddev.imag + self.output_phase_slope = qual.output_slope.imag + self.output_amp_slope = qual.output_slope.real