"""
Created on Thu June 2 2022
@author: J.M. Algarín, MRILab, i3M, CSIC, Valencia
@email: josalggui@i3m.upv.es
@Summary: code to obtain a good combination of tuning/matching
Specific hardware from MRILab @ i3M is required
"""
import copy
import os
import sys
import time
from scipy.interpolate import interp1d
#*****************************************************************************
# Get the directory of the current script
main_directory = os.path.dirname(os.path.realpath(__file__))
parent_directory = os.path.dirname(main_directory)
parent_directory = os.path.dirname(parent_directory)
# Define the subdirectories you want to add to sys.path
subdirs = ['MaRGE', 'marcos_client']
# Add the subdirectories to sys.path
for subdir in subdirs:
full_path = os.path.join(parent_directory, subdir)
sys.path.append(full_path)
#******************************************************************************
import numpy as np
import seq.mriBlankSeq as blankSeq # Import the mriBlankSequence for any new sequence.
import configs.hw_config as hw
import autotuning.autotuning as autotuning
import configs.units as units
[docs]
class AutoTuning(blankSeq.MRIBLANKSEQ):
def __init__(self):
super(AutoTuning, self).__init__()
# Input the parameters
self.freqOffset = None
self.frequency = None
self.statesXm = None
self.statesCm = None
self.statesCt = None
self.statesXs = None
self.states = None
self.statesCs = None
self.test = None
self.matching = None
self.tuning = None
self.series = None
self.seriesTarget = 80
self.tuningTarget = 55
self.state0 = None
self.frequencies = None
self.expt = None
self.threadpool = None
self.s11_hist = []
self.s11_db_hist = []
self.states_hist = [[], [], []]
self.n_aux = [[], [], []]
# Parameters
self.addParameter(key='seqName', string='AutoTuningInfo', val='AutoTuning')
self.addParameter(key='freqOffset', string='Frequency offset(kHz)', val=0.0, units=units.kHz, field='IM')
self.addParameter(key='series', string='Series capacitor', val='11011', field='IM')
self.addParameter(key='tuning', string='Tuning capacitor', val='10000', field='IM')
self.addParameter(key='matching', string='Matching capacitor', val='10011', field='IM')
self.addParameter(key='test', string='Test', val='auto', field='IM',
tip='Choose one option: auto, manual')
self.addParameter(key='xyz', string='xyz', val=0.0, field='IM')
# Connect to Arduino and set the initial state
self.arduino = autotuning.Arduino(name="auto-tuning", serial_number=hw.ard_sn_autotuning)
self.arduino.connect()
self.arduino.send(self.mapVals['series'] + self.mapVals['tuning'] + self.mapVals['matching'] + "11")
[docs]
def sequenceInfo(self):
print("RF automatic impedance matching")
print("Author: Dr. J.M. Algarín")
print("Contact: josalggui@i3m.upv.es")
print("mriLab @ i3M, CSIC, Spain")
print("Look for the best combination of tuning/matching.")
print("Specific hardware from MRILab @ i3M is required.\n")
[docs]
def sequenceTime(self):
return 0 # minutes, scanTime
[docs]
def sequenceRun(self, plotSeq=0, demo=False):
self.demo = demo
self.s11_hist = []
self.s11_db_hist = []
self.states_hist = [[], [], []]
self.n_aux = [[], [], []]
self.frequency = hw.larmorFreq+self.freqOffset*1e-6
if self.arduino.device is None:
print("WARNING: No Arduino found for auto-tuning.")
return False
else:
# Connect autotuning to VNA and turn it on.
self.arduino.send(self.mapVals['series'] + self.mapVals['tuning'] + self.mapVals['matching'] + "00")
print("nanoVNA ON")
time.sleep(1)
# Connect to VNA
self.vna = autotuning.VNA()
self.vna.connect()
if self.vna.device is None:
print("No nanoVNA found for auto-tuning. \n")
print("Only test mode.")
return False
if self.test == 'auto':
return self.runAutoTuning()
elif self.test == 'manual':
return self.runManual()
else:
print("Incorrect test mode.")
return False
[docs]
def sequenceAnalysis(self, mode=None):
self.mode = mode
# Get results
s11 = np.array(self.s11_hist)
s11_opt = self.mapVals['s11']
f_vec = self.vna.getFrequency()
s_vec = self.vna.getData()
# Interpolate s_vec
interp_func = interp1d(f_vec, s_vec, kind='cubic')
f_vec_t = np.linspace(np.min(f_vec), np.max(f_vec), 1000)
s_vec_t = interp_func(f_vec_t)
# Insert s11 into s_vec
index = np.searchsorted(f_vec_t, self.frequency)
f_vec_t = np.insert(f_vec_t, index, self.frequency)
s_vec_t = np.insert(s_vec_t, index, s11_opt)
# Get s in dB
s_vec_db = 20 * np.log10(np.abs(s_vec_t))
# Get quality factor
try:
idx = np.argmin(s_vec_db)
f0 = f_vec_t[idx]
f1 = f_vec_t[np.argmin(np.abs(s_vec_db[0:idx]+3))]
f2 = f_vec_t[idx+np.argmin(np.abs(s_vec_db[idx::]+3))]
q = f0/(f2-f1)
print("Q = %0.0f" % q)
print("BW @ -3 dB = %0.0f kHz" % ((f2-f1)*1e3))
except:
pass
# Create data array in case single point is acquired
if self.test == 'manual':
s11 = np.concatenate((s11, s11), axis=0)
# Plot smith chart
result1 = {'widget': 'smith',
'xData': [np.real(s11), np.real(s_vec_t)],
'yData': [np.imag(s11), np.imag(s_vec_t)],
'xLabel': 'Real(S11)',
'yLabel': 'Imag(S11)',
'title': 'Smith chart',
'legend': ['', ''],
'row': 0,
'col': 0}
# Plot reflection coefficient
result2 = {'widget': 'curve',
'xData': (f_vec_t - self.frequency) * 1e3,
'yData': [s_vec_db],
'xLabel': 'Frequency (kHz)',
'yLabel': 'S11 (dB)',
'title': 'Reflection coefficient',
'legend': [''],
'row': 0,
'col': 1}
self.output = [result1, result2]
self.saveRawData()
if self.mode == 'Standalone':
self.plotResults()
return self.output
[docs]
def runAutoTuning(self):
nCap = 5
# Combinations
self.states = [''] * 2 ** nCap
for state in range(2 ** nCap):
prov = format(state, f'0{nCap}b')
self.states[state] = ''.join('1' if bit == '0' else '0' for bit in prov)
# Series reactance
cs = np.array([np.Inf, 8, 3.9, 1.8, 1]) * 1e-9
self.statesCs = np.zeros(2 ** nCap)
self.statesXs = np.zeros(2 ** nCap)
for state in range(2 ** nCap):
for c in range(nCap):
if int(self.states[state][c]) == 0:
self.statesCs[state] += cs[c]
if self.statesCs[state] == 0:
self.statesXs[state] = np.Inf
else:
self.statesXs[state] = -1 / (2 * np.pi * self.frequency * 1e6 * self.statesCs[state])
# Tuning capacitor
ct = np.array([326, 174, 87, 44, 26]) * 1e-12
self.statesCt = np.zeros(2 ** nCap)
for state in range(2 ** nCap):
for c in range(nCap):
if int(self.states[state][c]) == 0:
self.statesCt[state] += ct[c]
# Matching capacitors
cm = np.array([np.Inf, 500, 262, 142, 75]) * 1e-12
self.statesCm = np.zeros(2 ** nCap)
self.statesXm = np.zeros(2 ** nCap)
for state in range(2 ** nCap):
for c in range(nCap):
if int(self.states[state][c]) == 0:
self.statesCm[state] += cm[c]
if self.statesCm[state] == 0:
self.statesXm[state] = np.Inf
else:
self.statesXm[state] = -1 / (2 * np.pi * self.frequency * 1e6 * self.statesCm[state])
# Get initial state index
stateCs = self.states.index(self.series)
stateCt = self.states.index(self.tuning)
stateCm = self.states.index(self.matching)
# Check current status
self.arduino.send(self.states[stateCs] + self.states[stateCt] + self.states[stateCm] + "00")
s11, impedance = self.vna.getS11(self.frequency)
self.addValues(s11, self.series, self.tuning, self.matching, stateCs, stateCt, stateCm)
s11_db = 20 * np.log10(np.abs(s11))
# Search from input impedance in cas initial state is far from matching
if s11_db > -20:
stateCs = self.getCsZ(16, 0, 16)
stateCt = self.getCtZ(stateCs, 0, 16)
stateCm = self.getCmZ(stateCs, stateCt, 16)
# Get the best state from all measured states
idx = np.argmin(self.s11_db_hist)
stateCs = self.n_aux[0][idx]
stateCt = self.n_aux[1][idx]
stateCm = self.n_aux[2][idx]
s11_db = self.s11_db_hist[idx]
# Move one state the series capacitor in case there is not matching.
if s11_db > -20:
stateCs = stateCs + 1
stateCt = self.getCtZ(stateCs, 0, 16)
stateCm = self.getCmZ(stateCs, stateCt, 16)
# Get the best state from all measured states
idx = np.argmin(self.s11_db_hist)
stateCs = self.n_aux[0][idx]
stateCt = self.n_aux[1][idx]
stateCm = self.n_aux[2][idx]
s11_db = self.s11_db_hist[idx]
# Once s11 < -20 dB, do a final optimization based on s11 in dB
self.finalOptimization2D(stateCs, stateCt, stateCm)
# Get the best state from all measured states
idx = np.argmin(self.s11_db_hist)
stateCs = self.n_aux[0][idx]
stateCt = self.n_aux[1][idx]
stateCm = self.n_aux[2][idx]
s11_db = self.s11_db_hist[idx]
# Check final status
self.arduino.send(self.states[stateCs] + self.states[stateCt] + self.states[stateCm] + "00")
s11, impedance = self.vna.getS11(self.frequency)
self.addValues(s11, self.series, self.tuning, self.matching, stateCs, stateCt, stateCm)
# Print results
print("S11 = %0.1f dB" % s11_db)
# Save parameters to source sequence
try:
sequence = self.sequence_list[self.mapVals['seqName']]
sequence.mapVals['matching'] = self.states[stateCm]
sequence.mapVals['tuning'] = self.states[stateCt]
sequence.mapVals['series'] = self.states[stateCs]
except:
pass
# Save result into the mapVals to save the rawData
self.mapVals['series'] = self.states[stateCs]
self.mapVals['tuning'] = self.states[stateCt]
self.mapVals['matching'] = self.states[stateCm]
self.mapVals['s11'] = self.s11_hist[-1]
self.mapVals['s11_db'] = self.s11_db_hist[-1]
# Connect the system to TxRx switch
self.arduino.send(self.states[stateCs] + self.states[stateCt] + self.states[stateCm] + "11")
print("nanoVNA OFF")
# Data to sweep sequence
self.mapVals['sampledPoint'] = s11_db
return True
[docs]
def runManual(self):
self.arduino.send(self.series + self.tuning + self.matching + "00")
if self.vna.device is not None:
s11, impedance = self.vna.getS11(self.frequency)
self.s11_hist.append(s11)
self.mapVals['s11'] = s11
s11dB = 20 * np.log10(np.abs(s11))
r = impedance.real
x = impedance.imag
print("S11 = %0.2f dB" % s11dB)
print("R = %0.2f Ohms" % r)
print("X = %0.2f Ohms" % x)
self.arduino.send(self.series + self.tuning + self.matching + "11")
print("nanoVNA OFF")
return True
else:
return False
[docs]
def addValues(self, s11, cs, ct, cm, ns, nt, nm):
self.s11_hist.append(s11)
self.s11_db_hist.append(20 * np.log10(np.abs(s11)))
self.states_hist[0].append(cs)
self.states_hist[1].append(ct)
self.states_hist[2].append(cm)
self.n_aux[0].append(ns)
self.n_aux[1].append(nt)
self.n_aux[2].append(nm)
[docs]
def getCsZ(self, n0, stateCt, stateCm):
print("Series sweep...")
n = [n0]
# First measurement
cs = self.states[n[-1]]
ct = self.states[stateCt]
cm = self.states[stateCm]
self.arduino.send(cs + ct + cm + "00")
s11, impedance = self.vna.getS11(self.frequency)
self.addValues(s11, cs, ct, cm, n0, stateCt, stateCm)
r = np.real(impedance)
x = np.imag(impedance)
x0 = [x]
z = [impedance]
# if x0[-1] < self.seriesTarget:
if np.abs(z[-1]) < self.seriesTarget:
step = 1
else:
step = -1
# Sweep series impedance until reactance goes to 50 Ohms
# while step * x0[-1] < step * self.seriesTarget and 0 < n[-1] + step < 16 and self.s11_db_hist[-1] > -20:
while step * np.abs(z[-1]) < step * self.seriesTarget and 0 < n[-1] + step < 16 and self.s11_db_hist[-1] > -20:
n.append(n[-1] + step)
cs = self.states[n[-1]]
self.arduino.send(cs + ct + cm + "00")
s11, impedance = self.vna.getS11(self.frequency)
self.addValues(s11, cs, ct, cm, n[-1], stateCt, stateCm)
r = np.real(impedance)
x = np.imag(impedance)
x0.append(x)
z.append(impedance)
# Select the value with reactance closest to 50 Ohms
if self.s11_db_hist[-1] <= -20:
stateCs = n[-1]
else:
try:
z = np.array(z)
stateCs = n[np.argmin(np.abs(z) - self.seriesTarget)]
if np.imag(z[np.argmin(np.abs(z) - self.seriesTarget)]) < 0:
stateCs += 1
except:
stateCs = n0
return stateCs
[docs]
def getCtZ(self, stateCs, n0, stateCm):
# Sweep tuning capacitances until resistance goes higher than 50 Ohms
print("Tuning sweep...")
n = [n0]
# First measurement
cs = self.states[stateCs]
ct = self.states[n[-1]]
cm = self.states[stateCm]
self.arduino.send(cs + ct + cm + "00")
s11, impedance = self.vna.getS11(self.frequency)
self.addValues(s11, cs, ct, cm, stateCs, n0, stateCm)
r = np.real(impedance)
x = np.imag(impedance)
r0 = [r]
if r0[-1] < self.tuningTarget:
step = 1
else:
step = -1
while step * r0[-1] < step * self.tuningTarget and 0 <= n[-1] + step < 32 and self.s11_db_hist[-1] > -20:
n.append(n[-1] + step)
ct = self.states[n[-1]]
self.arduino.send(cs + ct + cm + "00")
s11, impedance = self.vna.getS11(self.frequency)
self.addValues(s11, cs, ct, cm, stateCs, n[-1], stateCm)
r = np.real(impedance)
x = np.imag(impedance)
r0.append(r)
# Select the value with reactance closest to 50 Ohms
if self.s11_db_hist[-1] <= -20:
stateCt = n[-1]
else:
try:
stateCt = n[np.argmin(np.abs(np.array(r0) - self.seriesTarget))]
except:
stateCt = n0
return stateCt
[docs]
def getCmZ(self, stateCs, stateCt, n0):
print("Matching sweep...")
n = [n0]
# First measurement
cs = self.states[stateCs]
ct = self.states[stateCt]
cm = self.states[n[-1]]
self.arduino.send(cs + ct + cm + "00")
s11, impedance = self.vna.getS11(self.frequency)
self.addValues(s11, cs, ct, cm, stateCs, stateCt, n0)
r = np.real(impedance)
x = np.imag(impedance)
x0 = [x]
if x0[-1] < 0.0:
step = 1
else:
step = -1
# Sweep series impedance until reactance goes to 50 Ohms
while step * x0[-1] < 0.0 and 0 < n[-1] + step < 16 and self.s11_db_hist[-1] > -20:
n.append(n[-1] + step)
cm = self.states[n[-1]]
self.arduino.send(cs + ct + cm + "00")
s11, impedance = self.vna.getS11(self.frequency)
self.addValues(s11, cs, ct, cm, stateCs, stateCt, n[-1])
r = np.real(impedance)
x = np.imag(impedance)
x0.append(impedance.imag)
# Select the value with reactance closest to 50 Ohms
if self.s11_db_hist[-1] <= -20:
stateCm = n[-1]
else:
try:
stateCm = n[np.argmin(np.abs(np.array(x0)))]
except:
stateCm = n0
return stateCm
[docs]
def finalOptimization2D(self, stateCs, stateCt, stateCm):
print("Optimizing...")
cs = stateCs
ct_old = copy.copy(stateCt)
ct_new = copy.copy(stateCt)
cm_old = copy.copy(stateCm)
cm_new = copy.copy(stateCm)
check = True
iteration = 0
result = [[], [], [], []]
while check and iteration < 5:
ctv = [ct_old - 1, ct_old, ct_old + 1]
cmv = [cm_old - 1, cm_old, cm_old + 1]
for ct in ctv:
for cm in cmv:
# Check if current state is inside the boundaries
if cm == 0 or cm == 17 or ct == -1 or ct == 32:
continue
else:
state = self.states[ct]+self.states[cm]
# Get s11 if current state has not been tested before
if state not in result[3]:
cs_bin = self.states[cs]
ct_bin = self.states[ct]
cm_bin = self.states[cm]
self.arduino.send(self.states[cs] + self.states[ct] + self.states[cm] + "00")
s11, impedance = self.vna.getS11(self.frequency)
self.addValues(s11, cs_bin, ct_bin, cm_bin, cs, ct, cm)
result[0].append(self.s11_db_hist[-1])
result[1].append(ct)
result[2].append(cm)
result[3].append(state)
best_state = np.argmin(np.array(result[0]))
ct_new = result[1][best_state]
cm_new = result[2][best_state]
if ct_new == ct_old and cm_new == cm_old:
check = False
else:
ct_old = copy.copy(ct_new)
cm_old = copy.copy(cm_new)
iteration += 1
return ct_new, cm_new
[docs]
def finalOptimization3D(self, stateCs, stateCt, stateCm):
print("Optimizing...")
cs_old = copy.copy(stateCs)
cs_new = copy.copy(stateCs)
ct_old = copy.copy(stateCt)
ct_new = copy.copy(stateCt)
cm_old = copy.copy(stateCm)
cm_new = copy.copy(stateCm)
result = [[], [], [], [], []]
check = True
iteration = 0
while check and iteration < 10:
csv = [cs_old - 1, cs_old, cs_old + 1]
ctv = [ct_old - 1, ct_old, ct_old + 1]
cmv = [cm_old - 1, cm_old, cm_old + 1]
for cs in csv:
for ct in ctv:
for cm in cmv:
cs_bin = self.states[cs]
ct_bin = self.states[ct]
cm_bin = self.states[cm]
state = cs_bin + ct_bin + cm_bin
if state in result[4]:
pass
else:
result[4].append(state)
if cm == 0 or cm == 17 or ct == -1 or ct == 32 or cs == 0 or cs == 17:
result[0].append(0.0)
else:
self.arduino.send(state + "00")
s11, impedance = self.vna.getS11(self.frequency)
self.addValues(s11, cs_bin, ct_bin, cm_bin, cs, ct, cm)
result[0].append(self.s11_db_hist[-1])
result[1].append(cs)
result[2].append(ct)
result[3].append(cm)
best_state = np.argmin(np.array(result[0]))
cs_new = result[1][best_state]
ct_new = result[2][best_state]
cm_new = result[3][best_state]
if ct_new == ct_old and cm_new == cm_old and cs_new == cs_old:
check = False
else:
cs_old = copy.copy(cs_new)
ct_old = copy.copy(ct_new)
cm_old = copy.copy(cm_new)
iteration += 1
return cs_new, ct_new, cm_new
if __name__ == '__main__':
seq = AutoTuning()
seq.sequenceAtributes()
seq.sequenceRun()
seq.sequenceAnalysis(mode='Standalone')