Processing math: 100%

Monday, January 20, 2014

SR850 lock-in amplifier, with PyVISA

It's a breeze, as always, to connect instruments to Python. This is a PyVISA sub-class that you can use with an SR850 lock-in amplifier.


# Talk to SR850 lockin amplifier
# version 1, 2014-01-20
# Amar
import visa
import numpy as np
from datetime import date
import time
class LIA(visa.SerialInstrument):
# Lock-in amplifier, SR850
def __init__(self,portName):
visa.SerialInstrument.__init__(self,portName)
self.timeout = 3
self.id = self.ask("*IDN?")
def characterize(self):
warnings = ""
print self.id
print "Sensitivity = ", self.ask("SENS?")
print "Reserve = ", self.ask("RSRV?")
if self.ask("RSRV?")!="0": warnings+= "** Warning! Dynamic reserve is not 0 dB ** \n"
print "Time constant = ", self.ask("OFLT?")
print "Traces = ", self.ask("TRCD?1"), self.ask("TRCD?2"), self.ask("TRCD?3"), self.ask("TRCD?4")
FMOD = self.ask("FMOD?")
if FMOD == "0": FMODString = "internal"
elif FMOD == "1": FMODString = "internal sweep"
elif FMOD == "2": FMODString = "external"
print "Ref. source = ", FMODString
print "Ref. frequency = ", self.ask("FREQ?")
print "Ref. phase = ", self.ask("PHAS?")
print "Ref. sine level = ", self.ask("SLVL?")
print
print warnings
print
def setf(self,fi):
self.write("FREQ " + `fi`)
def setphase(self,p):
self.write("PHAS " +`p`)
def getAverage(self,channel=1,N=400):
# channel #s: 1 = X, 2 = Y, 3 = R, 4 = Theta
S = []
for i in range(N): S.append( float(self.ask("OUTP?" + `channel`)) )
S_avg = np.average(S)
S_std = np.std(S)
return S_avg, S_std
def scanP(self,f,channel=1,N=100,std=False):
# channels: 1 = X, 2 = Y, 3 = R, 4 = Theta
output, err = [], []
for fi in f:
self.setf(fi)
print self.ask("FREQ?"),
x = []
for i in range(N): x.append( float(self.ask("OUTP?"+`channel`)) )
x_avg = np.average(x)
output.append( x_avg )
x_std = np.std(x)
err.append( x_std )
if std: return output, err
else: return output
def scanR(self,f,trace=1,N=100,std=False):
# traces: 1,2,3,4
output, err = [], []
for fi in f:
self.setf(fi)
print self.ask("FREQ?"),
x = []
for i in range(N): x.append( float(self.ask("OUTR?"+`trace`)) )
x_avg = np.average(x)
output.append( x_avg )
x_std = np.std(x)
err.append( x_std )
if std: return output, err
else: return output
view raw SR850-lockin.py hosted with ❤ by GitHub

 Here is how to use these utility functions to measure a noise spectrum with the SR850. (Watch out for the dynamic reserve when making noise measurements!)

# SR850 as a spectrum analyzer
# version 1, 2014-01-21
# Amar
import os
os.chdir("c:/data/googledrive/code")
import SR850
import numpy as np
import pylab as plt
lia= SR850.LIA("COM7")
lia.characterize()
lia.write("TRCD 1,5,0,0,0") # set trace 1 to Xn (rms noise in X quadrature, V/rt-Hz)
lia.write("TRCD 2,6,0,0,0") # set trace 2 to Yn (rms noise in Y quadrature, V/rt-Hz)
background = True
Navg = 1000
f = 10**np.arange(0,4,0.2)
Xn, Xn_err = np.array( lia.scanR(f,trace=1,N=Navg,std=True) )
if background:
print
foo = raw_input("Ready for background measurement?")
Xn_bg, Xn_bg_err = np.array( lia.scanR(f,trace=1,N=Navg,std=True) )
lia.close()
plt.figure(1)
plt.title("Current noise spectrum with lock-in \n G = 1e9 V/A, 10 mV FS, 0 dB reserve")
plt.xlabel("Frequency, f [Hz]")
plt.ylabel("Current noise, \sqrt{W_I} [A/\sqrt{\mathrm{Hz}}]")
plt.xscale('log')
plt.yscale('log')
plt.errorbar(f,np.sqrt(2) * Xn/1e9,yerr = 20 * np.sqrt(2) * Xn_err/1e9,color='b',marker='o',lw=2,label="Beam on")
if background:
plt.errorbar(f,np.sqrt(2) * Xn_bg/1e9,yerr = 20 * np.sqrt(2) * Xn_bg_err/1e9,color='g',marker='^',lw=2,label="Background")
plt.plot(f,np.sqrt(2*1.6e-19*1.7e-9)*np.ones(len(f)),'b--',lw=4, alpha=0.5,label="Shot noise")
plt.legend(loc='upper right')
plt.show()

Thursday, January 16, 2014

RLC circuit tuner

I got bored of calculating resonances by hand, so here's a tuner for an RLC circuit. The transfer function is the ratio of the voltage across R_L to the input voltage.

 

 RLC circuit tuner

import matplotlib as mpl
#mpl.rc('text', usetex = True)
#mpl.rc('font',**{'family':'serif','size':16,'weight':'bold','serif':['Palatino']})
from __future__ import division
import numpy as np
from numpy import pi
import matplotlib.pyplot as plt
from matplotlib.widgets import Slider, Button, RadioButtons
def ser(A,B): return A+B
def par(A,B): return A*B/(A+B)
def H(f,L,C,R,RL):
# Resonant circuit
omega = 2*pi*f
X_L = 1j*omega*L
X_C = 1/(1j*omega*C)
Z0 = par(par(X_L,X_C),RL)
return Z0/(R+Z0)
# Random initial values
L = 1e-3 # H
C = 1e-3 # F
R = 1e1 # Ohms
RL = 1e3 # Ohms
f = np.arange(0.1,1000,0.1)
fig = plt.figure(figsize=(9,4.5))
ax1 = fig.add_subplot(111)
ax2 = ax1.twinx()
lmag, = ax1.plot(f,np.abs(H(f,L,C,R,RL)))
lphase, = ax2.plot(f,np.angle(H(f,L,C,R,RL)),'g')
ax2.grid(which='major',axis='y')
ax2.set_ylabel("Phase[H(f)] [rad]",color='g')
ax1.set_ylabel("Magnitude, |H(f)|",color='b')
ax1.set_xlabel("Frequency, f [Hz]")
#plt.tight_layout()
fig.subplots_adjust(bottom=0.11*4)
#plt.subplots_adjust(left=0.10)
#plt.subplots_adjust(right=1.00)
ax = []
slider = []
axcolor = 'lightgoldenrodyellow'
class Parameter:
def __init__(self,label,limit,value):
self.label = label
self.limit = limit
self.value = value
params = [ Parameter('log10 L',[-6,-1],-3), \
Parameter('log10 C',[-9,-2],-4), \
Parameter('log10 R',[0,8],2), \
Parameter('log10 RL',[0,9],6)]
for i in range(len(params)):
ax.append( plt.axes([0.25, 0.26-0.05*i, 0.65, 0.03], axisbg=axcolor) )
slider.append( Slider(ax[i],params[i].label,params[i].limit[0],params[i].limit[1],valinit=params[i].value) )
def update(val):
L = 10**slider[0].val
C = 10**slider[1].val
R = 10**slider[2].val
RL = 10**slider[3].val
lmag.set_ydata( np.abs(H(f,L,C,R,RL)) )
lphase.set_ydata( np.angle(H(f,L,C,R,RL)) )
fig.canvas.draw_idle()
for i in range(len(params)): slider[i].on_changed(update)
update(0)
view raw RLCtuner.py hosted with ❤ by GitHub