From 5d175b89ca1eeda3a1fe14668a19a5c88624cdff Mon Sep 17 00:00:00 2001 From: Markus Zolliker Date: Wed, 19 Mar 2025 15:29:17 +0100 Subject: [PATCH] frappy_psi.ultrasound: add input_delay and other improvments Change-Id: I6cb5690d82d96d6775fcb649fc633c4039932463 --- cfg/RUS.py | 62 ---------------------------------------- frappy_psi/adq_mr.py | 55 +++++++++++++++++++---------------- frappy_psi/ultrasound.py | 48 +++++++++++++++++++++---------- 3 files changed, 64 insertions(+), 101 deletions(-) delete mode 100644 cfg/RUS.py diff --git a/cfg/RUS.py b/cfg/RUS.py deleted file mode 100644 index 9b5f235..0000000 --- a/cfg/RUS.py +++ /dev/null @@ -1,62 +0,0 @@ -Node(equipment_id = 'r_ultrasound.psi.ch', - description = 'resonant ultra sound setup', - interface = 'tcp://5000', -) - -Mod('f', - cls = 'frappy_psi.ultrasound.Frequency', - description = 'ultrasound frequency and acquisition loop', - uri = 'serial:///dev/ttyS1', - pars = 'pars', - pollinterval = 0.1, - time = 900, # start time - size = 5000, - freq = 1.e+03, - basefreq = 1.E+3, - control = False, - rusmode = False, - amp = 2.5, - nr = 1, #500 #300 #100 #50 #30 #10 #5 #3 #1 #1000 #500 #300 #100 #50 #30 #10 #5 #3 #1 #500 - sr = 1E8, #16384 - plot = True, - maxstep = 100000, - bw = 10E6, #butter worth filter bandwidth - maxy = 0.7, # y scale for plot - curves = 'curves', # module to transmit curves: - ) - -Mod('curves', - cls = 'frappy_psi.ultrasound.Curves', - description = 't, I, Q and pulse arrays for plot', - ) - -Mod('roi0', - cls = 'frappy_psi.ultrasound.Roi', - description = 'I/Q of region in the control loop', - time = 300, # this is the center of roi: - size = 5000, - main = f, - ) - -Mod('roi1', - cls = 'frappy_psi.ultrasound.Roi', - description = 'I/Q of region 1', - time = 100, # this is the center of roi: - size = 300, - main = f, - ) - -Mod('delay', - cls = 'frappy__psi.dg645.Delay', - description = 'delay line with 2 channels', - uri = 'serial:///dev/ttyS2', - on1 = 1e-9, - on2 = 1E-9, - off1 = 400e-9, - off2 = 600e-9, - ) - -Mod('pars', - cls = 'frappy_psi.ultrasound.Pars', - description = 'SEA parameters', - ) diff --git a/frappy_psi/adq_mr.py b/frappy_psi/adq_mr.py index f40e858..803c42a 100644 --- a/frappy_psi/adq_mr.py +++ b/frappy_psi/adq_mr.py @@ -47,6 +47,7 @@ ADQ_TRANSFER_MODE_NORMAL = 0x00 ADQ_CHANNELS_MASK = 0x3 GHz = 1e9 +RMS_TO_VPP = 2 * np.sqrt(2) class Adq: @@ -88,19 +89,15 @@ class Adq: rev = ADQAPI.ADQ_GetRevision(self.adq_cu, self.adq_num) revision = ct.cast(rev, ct.POINTER(ct.c_int)) - print('\nConnected to ADQ #1') - # Print revision information - print('FPGA Revision: {}'.format(revision[0])) + out = [f'Connected to ADQ #1, FPGA Revision: {revision[0]}'] if revision[1]: - print('Local copy') + out.append('Local copy') else: - print('SVN Managed') if revision[2]: - print('Mixed Revision') + out.append('SVN Managed - Mixed Revision') else: - print('SVN Updated') - print('') - + out.append('SVN Updated') + print(', '.join(out)) ADQAPI.ADQ_SetClockSource(self.adq_cu, self.adq_num, ADQ_CLOCK_EXT_REF) ########################## @@ -126,9 +123,10 @@ class Adq: elif self.trigger == EXT_TRIG_1: if not ADQAPI.ADQ_SetExternTrigEdge(self.adq_cu, self.adq_num, 2): raise RuntimeError('ADQ_SetLvlTrigEdge failed.') - # if not ADQAPI.ADQ_SetTriggerThresholdVoltage(self.adq_cu, self.adq_num, trigger, ct.c_double(0.2)): - # raise RuntimeError('SetTriggerThresholdVoltage failed.') - print("CHANNEL:"+str(ct.c_int(ADQAPI.ADQ_GetLvlTrigChannel(self.adq_cu, self.adq_num)))) + # if not ADQAPI.ADQ_SetTriggerThresholdVoltage(self.adq_cu, self.adq_num, trigger, ct.c_double(0.2)): + # raise RuntimeError('SetTriggerThresholdVoltage failed.') + # proabably the folloiwng is wrong. + # print("CHANNEL:" + str(ct.c_int(ADQAPI.ADQ_GetLvlTrigChannel(self.adq_cu, self.adq_num)))) def init(self, samples_per_record=None, number_of_records=None): """initialize dimensions and store result object""" @@ -145,16 +143,14 @@ class Adq: def deletecu(self): cu = self.__dict__.pop('adq_cu', None) if cu is None: - print('deletecu already called') return - print('deletecu called') + print('shut down ADQ') # Only disarm trigger after data is collected ADQAPI.ADQ_DisarmTrigger(cu, self.adq_num) ADQAPI.ADQ_MultiRecordClose(cu, self.adq_num) - print('delete cu') # Delete ADQControlunit ADQAPI.DeleteADQControlUnit(cu) - print('cu deleted') + print('ADQ closed') def start(self, data): # Start acquisition @@ -165,6 +161,7 @@ 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""" @@ -182,6 +179,7 @@ class Adq: if not ready: self.busy = True return None + t = time.time() # Get data from ADQ if not ADQAPI.ADQ_GetData( self.adq_cu, self.adq_num, self.target_buffers, @@ -283,25 +281,31 @@ class Namespace: class RUSdata: - def __init__(self, adq, freq, periods): + def __init__(self, adq, freq, periods, delay_samples): self.sample_rate = adq.sample_rate self.freq = freq self.periods = periods + self.delay_samples = delay_samples self.samples_per_record = adq.samples_per_record self.inp = Namespace(idx=0, name='input') self.out = Namespace(idx=1, name='output') self.channels = (self.inp, self.out) def retrieve(self, adq): - npts = self.samples_per_record + npts = self.samples_per_record - self.delay_samples complex_sinusoid = np.exp(1j * 2 * np.pi * self.freq / self.sample_rate * np.arange(npts)) for chan in self.channels: # looping over input and output - signal = np.frombuffer(adq.target_buffers[chan.idx].contents, dtype=np.int16) - chan.ovr_rate = (np.abs(signal) > 8000).sum() / npts # fraction of points outside range + # although the ADC is only 14 bit it is representet 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 # calculate RMS * sqrt(2) -> peak sinus amplitude. # may be higher than the input range by a factor 1.4 when heavily clipped - # we need to convert signal from int16 to float -> use 2.0 as exponent does the trick - chan.amplitude = np.sqrt((signal ** 2.0).mean()) + signal = chan.signal - np.median(chan.signal) + chan.amplitude = np.sqrt((signal ** 2).mean()) * RMS_TO_VPP chan.mixed = signal * complex_sinusoid chan.mean = chan.mixed.mean() if self.inp.mean: @@ -320,10 +324,13 @@ class RUSdata: the imaginary part indicates a turning phase (rad/sec) the real part indicates changes in amplitude (0.01 ~= 1%/sec) """ - nper = self.samples_per_record // self.periods + 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=0) / 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( diff --git a/frappy_psi/ultrasound.py b/frappy_psi/ultrasound.py index e817c2c..e0ace10 100644 --- a/frappy_psi/ultrasound.py +++ b/frappy_psi/ultrasound.py @@ -27,9 +27,9 @@ 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 + IDLE, BUSY, DISABLED, WARN, ERROR, TupleOf, ArrayOf, Command, Attached, EnumType from frappy.properties import Property -#from frappy.modules import Collector +# from frappy.modules import Collector Collector = Readable @@ -95,8 +95,8 @@ class FreqStringIO(StringIO): class Frequency(HasIO, Writable): value = Parameter('frequency', unit='Hz') - amp = Parameter('amplitude', FloatRange(unit='dBm'), readonly=False) - + 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 @@ -115,14 +115,15 @@ class Frequency(HasIO, Writable): self.last_change = time.time() if self.dif: self.dif.read_value() + self.read_value() return self._freq def write_amp(self, amp): - reply = self.communicate('AMPR %g;AMPR?' % amp) + reply = self.communicate(f'AMP{self.output.name} {amp} VPP;AMP{self.output.name}? VPP') return float(reply) def read_amp(self): - reply = self.communicate('AMPR?') + reply = self.communicate(f'AMP{self.output.name}? VPP') return float(reply) @@ -146,9 +147,7 @@ class Base: def shutdownModule(self): if self.adq: - print('shutdownModule') self.adq.deletecu() - print('shutdoneModule done') self.adq = None @@ -236,11 +235,17 @@ 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) - input_range = Parameter('input range (taking in to account attenuation)', FloatRange(unit='V'), default=0.1, readonly=False) - output_range = Parameter('output range', FloatRange(unit='V'), default=0.1, 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'), + 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) @@ -249,7 +254,7 @@ class RUS(Base, Collector): 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(default=1) + pollinterval = Parameter(datatype=FloatRange(0, 120), default=1) _starttime = None _iq = 0 @@ -262,8 +267,12 @@ class RUS(Base, Collector): super().initModule() self.adq = Adq() self._ovr_rate = {} + self.freq.addCallback('value', self.update_freq) # self.write_periods(self.periods) + def update_freq(self, value): + self.setFastPoll(True, 0.001) + def doPoll(self): try: data = self.adq.get_data() @@ -283,11 +292,15 @@ class RUS(Base, Collector): 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.input_amplitude = data.inp.amplitude / 2 ** 15 * self.input_range - self.output_amplitude = data.out.amplitude / 2 ** 15 * self.output_range + 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 self.phase = np.arctan2(iq.imag, iq.real) * 180 / np.pi self.amp = np.abs(iq) @@ -351,12 +364,17 @@ class RUS(Base, Collector): def start_acquisition(self): freq = self.freq.read_value() self.sr = round(self.periods * self.adq.sample_rate / freq) - self.adq.init(self.sr, 1) - self.adq.start(RUSdata(self.adq, freq, self.periods)) + 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) +class Signal(Readable): + value = Parameter('pulse', ArrayOf(FloatRange(), maxlen=9999)) + + class ControlLoop(Module): roi = Attached(Roi) maxstep = Parameter('max frequency step', FloatRange(unit='Hz'), readonly=False,