Software PID controller
| Links:

A diode based temperature sensor is used for measuring, and a voltage regulator was repurposed (misused) as a heating element . A python script converts the voltage values read from the sensor into temperature via a fitting function based on a look up table provided by the manufacturer. It then decides the the amount of current to send to the heater based on the standard PID equation.
Results
Stability of around 25mK was achieved
Final stability of the PID controller
Source Code
import Gpib,time,string #python GPIB wrapper used
from Tkinter import * #used for graphics
#import eyeplot2 as eyeplot, eyemath,sys,math
import pygrace
#y = 512.93 - 424.79 * x + 60.2 * x^2 - 60.75 * x^3
WIDTH = 800 # width of drawing canvas
HEIGHT = 400 # height
#pid constants
c1=500 #500
c2=200.0
c3=-400.0 #-400
integral=1 #updated later
derivative=1 #updated later
pid_mA=300
droop=0
temp='?'
def temperature(x): #function returns corresponding temperature
return 512.93 - 424.79 * x + 60.2 * x*x - 60.75 * x*x*x #using fitted data equation
b=Gpib.Gpib('voltmeter') #create instances to instruments
a=Gpib.Gpib('sourcemeter')
def readval(): #function to measure resistance
b.clear()
b.write(":DATA?") #query data
data=b.read(28) #read data
return data
pg = pygrace.grace()
a.clear()
a.write(":OUTP OFF") #turn off output
a.write(":SOUR:FUNC CURR") #set output to constant current mode
a.write(":SOUR:CURR:MODE FIX")
a.write(":SOUR:CURR:RANG 1") #set 1A current max
start=time.time()
x=[]
y=[]
set_temp=93.0
fi=open('pid100ma.txt','wt') #data file
def initialize():
global start
start = time.time()
def xmgrace():
global x,y,set_temp,pg,temp,droop
pg.clear()
pg.plot(x,y)
pg.xlabel('Time')
pg.ylabel('Temperature')
pg.title('set to '+str(set_temp)+'K, now at %.4f'%(temp)+'droop=%.6f'%(droop) )
def record():
global set_temp,x,y,integral,derivative,temp,droop,overshoot
root.after(500,record) #automatically repeat function after 500ms
if not rec.get(): #check status of checkbutton
return #return if false
#x=string.split(val,',')
#volt=string.atof(x[0])
volt=string.atof(readval()) #ASCII to float conversion
temp=temperature(volt)
dt=time.time()-start
dtemp=set_temp+droop-temp
difference=set_temp+droop-temp
pid_mA=300
if len(x)>30:
sd=0
thirty_points=y[-30:]
for r in thirty_points:
sd+=(abs(r-thirty_points[0]))/30.0
print 'deviation',sd
if sd<0.02: #apply droop correction
if(y[-1]<set_temp ):
if(overshoot==True):
droop+=(set_temp-y[-1])/10.0
overshoot=False
msgwin.config(col='green')
elif(y[-1]>set_temp):
droop-= 0.1
msgwin.config(col='red')
print 'droop offset = ',droop
a.clear()
if len(x)>8:
last_five_points=y[-5:]
time_taken=x[-1]-x[-5]
integral=0
derivative=0
for r in last_five_points:
integral+=(r-last_five_points[0])/time_taken
derivative=(last_five_points[4]-last_five_points[0])
print difference*c1,integral*c2,derivative*c3,'heat out=',\
difference*c1+integral*c2+derivative*c3
pid_mA=c1*difference+c2*integral+c3*derivative
if(pid_mA>800):pid_mA=800
if(pid_mA<0):pid_mA=1
a.clear()
q=":SOUR:CURR:LEV "+str(int(pid_mA))+"E-3"
print q
a.write(q)
a.clear()
if(difference<-1):
a.write(":OUTP OFF")
else:
a.write(":OUTP ON")
x.append(dt)
y.append(temp)
if(temp>set_temp and y[-3]<set_temp):
overshoot=True
xmgrace()
fi.write("%s %s\n"%(dt,temp))#write to file
root=Tk() #create a window
root.lift()
#g = eyeplot.graph(root, width=WIDTH, height=HEIGHT) # make plot objects using draw.disp
rec=IntVar()
c = Checkbutton(root, text="RECORD DATA", bd=5 ,cursor='spider',relief='raised' ,\
variable=rec,command=initialize )
c.pack(side = LEFT, anchor = N) #disply a button
msgwin = Label(root,text='droop correction', fg = 'blue')
msgwin.pack(side=BOTTOM, anchor = S, fill=BOTH, expand=1)
root.title('temperature')
root.after(100,record)
root.mainloop() #loop infinitely