Introduction: Python Powered Spectrophotometer!
Welcome to another instructable. In this instructable I will discuss how to make a crude spectrophotometer. I will show you what sensors work best, and how to use Python for dynamically updating and displaying the data.
Overall I will be evaluating how effective or useful this will be to hobbyists, who want to learn more about it. I personally had no idea how one worked so I thought I'd research the idea. At the time I was particularly interested in finding out how much chlorine was in my drinking water (my tap water has a chlorine smell/taste when I use it to make coffee). I will discuss this a little more detail in the next step, and show you what I discovered during my research.
Step 1: What Is Spectrophotometry Good For?
This is a great project to easily demonstrate the benefits of a spectrophotometer in finding concentrations of a known chemical solute. The concept itself is used in highly accuracy testing laboratories, but unfortunately for the hobbyist its severely limited by the things it can measure. I would recommend this as an educational project, not for measuring Chlorine in drinking water.
Limitations of a DIY spectrophotometer
Why this is limited for hobbyists? The main limitation is getting known concerntrations of the solute you want to measure. For example, Chlorine standard solution is very expensive (approx. £40 per 20ml! avalable here). Chemicals like Chlorine are also colourless and not measureable in a spectrophotometer until a reagent is added to colourise it (depth of colour is dependant upon concerntration). For the hobbyist this is prohibitively expensive, and therefore you will be limited to less hazardous chemicals to measure like food colouring or chemicals that are commercially and readily available like potassium permanganate.
For this instructable I will be testing with KMnO4 (potassium permanganate), and some food colouring for those who want to use something easily accessible.
Step 2: What Is a DIY Spectrophotometer?
There are many cool instructables about it. If you just want to make mine skip this step otherwise read on. A polychromatic light source is split into a rainbow and then a portion of that rainbow is emitted through a sample, and the difference between the light entering the sample and exiting the sample is the absorpbance according to the beer-lambert law.
The first one that inspired me to build it was this one:
A simple DIY Spectrophotometer
To build this with precision is not as easy as it looks. When the light passes through the monochromator the angle of the light exiting a prism or diffraction grating is dependant upon its wavelength, the geometry of the grating, the lines per cm of the grating etc. Selecting the wavelength, aligning the sensor requires some careful consideration, if you would like to know more about the trigonometry involved there is a good tutorial here on youtube. I decided I would try something easier to start with an I will revisit this design later. Also the active wavelegnth of the LDR in this instructable was in a narrow range centred on the 540 nm wavelength so it is worth looking at this as a separate instructable/project for the future!
Theremino USB webcam version
A better version you should look at is this one by Theremino:
This one does all the processing for you, all you need is a USB webcam, a diffraction grating,a computer and an enclosure. The program insists you use microsoft .NET environment 3.5 so if you have anything other than that installed you will get an error message when you run it so bear that in mind if you get stuck. I got frustrated with this one too after butchering two webcams, and again not being able to align the webcam to the correct angle. I found the instructions too vague and the alignment was purely empirical. I would of loved to have it working since it scanned the entire spectrum.
A minimal six colour spectrophotometer
I decided to start with something less ambitious and easy to make, and found several instructables such as this one:
Technically, these are not true spectrometers as they only measure selected wavelengths (in the case of this example it was 6-channel). These are known as colourimeters, the sensors are very easy to get hold of too. So I decided to make this particular one.
Step 3: Selecting a Sensor
There are various sensors available, some are really intricate with alot of digital processing, others provide a simple analog signal. Each have their advantages and disadvantages. I evaluated several sensors which you can use to best select the sensor for your spectrophotometer.
Adafruit AS7262 (6 channel colour sensor)
This is an excellent sensor. It has it's own on board microcontroller and communications control including its own UART. You don't need a microcontroller between a computer and the sensor, you can just plug it straight into a serial port. Personally I used the Adafruit library to communicate with my computer via an Arduino nano I2C, just because it worked out of the box. It is probably the best suited for measuring a wide variety of the spectrum, because it measures 6 individual wavelengths, each channel is spaced so that they don't overlap significantly, and span the visible spectrum. The update time is roughly 5 measurements per second so don't expect speed for updating real time graphs. This gave me the best repeatable results when tested with some food colouring in tap water, and with RGB LED's (red, green, blue). I chose this for my sensor, but it is also the most expensive approx. £20
This is also a very good sensor but it only detects three colours RGB. I used it over I2C with Arduino nano using the Adafruit libraries. It also measures LUX and colour temperature, but in this case these functions are really surplus to requirement. Cost is approx £6
This has no dedicated microprocessor inside, it outputs a square wave with a variable frequency according to the colour it detects. I found this the worst for repeatability and I found alot of people on the Arduino forums and elsewhere with issues where it was returning the same values for both Blue and Green. This is probably because the Green and Blue sensitivity curves overlap significantly and the IC finds it hard to differentiate between the two. It also has a heavy temperature dependence,. This is the second cheapest at around £5
LDR Light dependant resistor
The LDR is a resistor who's resistance varies with light intensity. The GL5537 is a common type of LDR which has variable sensitivity to differing wavelengths from 20% to 100% between 400 to 800 nm. It can be easily measured with a digital multi meter so you don't necessarily need a micro controller to use it. The speed of measurements you can take with a microcontroller is only limited by the ADC so this can make a good candidate for fast real time data. It's the cheapest part at just £1.38 for 50!! but you will need some complimentary electronics such as an op-amp, power supply, resistors etc. Also, It is most sensitive in the middle of the range at about 540 nm, so it measures the green portion of the spectrum the best. Not great it you need measure Red or Blue ends of the spectrum, this is better for measuring Absorbency of a single wavelength.
Step 4: Making an Enclosure
You need to make sure that when taking your measurement that the sensor only detects the light from your chosen light source (LED or incandescent lamp). So you will need to enclose your sample in something dark (preferable black and non-reflective) to reduce the errors cause by stray ambient light effecting the measurement. I made a tiny little box that surrounds the curvette and two little holes to let the light pass through the sample from an LED to the sensor. Then I placed that assembly inside another box.
Each box was made from 3mm black PVC (foamboard), The templates were made in inkscape (attached), printed out on paper and cut around them to make slotted pieces to make a tidy enclosure. I used hot glue to adhere all the pieces together. Both the inner box and outer box was made in the same fasion., and made two holes in the box for the insertion of the wires.
Step 5: Electronics & Arduino Code
The electronics are very simple. It consists of a breadboard, momentary push button, sensor and LED all connected with dupont cables. The wiring instructions for the sensor to the arduino are available from this adafruit tutorial
I connected a white LED to the Arduino 5V rail using a momentary push button, which I will hold down to turn on the LED and make a measurement. The wiring is done with jumper cables.
The code is also straight forward however I made a few alterations in order for it to be processed later in Python. Attached are two sketches for Arduino. The first "random_numbers_serial.ino" generates a string of random numbers and prints it in the serial monitor. This is only to test the graphing output of the Python program which I will show you in a later step. The second "AS7262_test.ino" is the code used to interrogate the sensor and prints the results in the serial monitor. Each code outputs values in the following serial format:
Of course it will not have the carriage return character at the end, or the "b" byte character at the start in the serial monitor. However when Python reads this, it will print the whole thing as we will see later. The string represents values between Violet to Red which can then be separated and graphed in the computer.
The code for the AS7262 is just the adafruit example code with a comma added at the start and end of the string. This is to allow us to deliimit the byte character and the carriage return so we can delete them later.
Step 6: Python Program
How the code works
Here I have attached the Python code. It takes the output directly from the Arduino USB and plots it on a live updating Bar graph. Here I will walk you through the code:
Line 11 to 14
This imports the necessary modules. Matplotlib is awesome for plotting things, on this occasion I will be using the animate feature, which has the ability to dynamically update the plot. It also takes advantage of "blitting" which makes the update speed significantly faster (more on this later). Serial allows Python to read the serial output of the arduino.
Line 16 to 23
This paragraph shows you the serial configuration used to interpret the serial port. Most important is the baud rate which must be the same as the specified Serial.begin(); in the arduino code. I have included a timeout to make sure the code times out if it doesn't receive a serial string. Port needs to be the same port used in the arduino IDE. If you go to "Tools > Port" in arduino IDE it will show up there. Lines 25 & 26, simple ensure the port is closed before we try and open it otherwise it will throw up an error.
Line 28 & 30
The x axis data will never change so I have stored the values of colour here. Your could name these whatever you like. You could even use the wavelength which corresponds to each colour. "fig, ax" tells matplotlib to setup a subplot.
Line 32 - 39
This is the animate callback, which also passes the arguement "i". A few important things to note here, it must pass an arguement (in this case "i") and it must return something (in this case "return ax.bar()"). Notice ax.clear is called first to clear the previous set of data before it replots the new data.
Line 41 - 56
This is the body of the code. The object "output" reads the serial input until a carriage return is detected "/r/n". The resulting data is a single string of text which cannot be interpreted yet. It needs to be split into individual numbers. This is done by storing the entire string in "data" and splitting the string into a list. The command ".split(',')" tells python to split the text at each point there is a comma and store the split text in a list. On line 46 the command "del" removes the first character from the list (the 'b' byte character). The "if" statement will try to delete the 6th character in the list (the carriage return) only if there is data in the list. Without this "if" statement the code will crash because there is no data to delete! It will then float the remaining values in the list (now entirely made from numbers) and store them as a new list called "y_data". If there is no data detected during the execution of "if data" then python will assume the y_data list should contain zero's. This simply prevents it from crashing due to half a serial string, or no serial string being detected. "def main()" then returns the y_data string for plotting.
Line 58 - 59
Line 58 is the animate function. It tells python the figure to animate, and the animation callback it needs to use, the time it takes to reflesh the graph (interval), and if to use blitting or not). As mentioned earlier blitting is very important if you want to update a graph very fast, or if you have limited processing power (like on a raspberry pi for example). If you were to ask matplotlib to redraw the entire graph for every new set of data it would take a long time to redraw the titles, axis, etc. What blitting does is only replot the aspects of the graph that need updating (in this case the bars are the only thing that chages). Not having to redraw everything makes it alot snappier. FInally plt.show() is mandatory and is required to tell matplotlib to show the data.
Testing the code
The best way to test the graphing program is to upload the dummy program "random_numbers" to the Arduino so it generates some random data for the graph to plot. If it is working correctly you should see some bars updating with the random data and the numbers being printed in the Python Shell. Once your happy with that you can continue to upload the "AS7262" arduino code and see the data from the sensor.
Step 7: Calibration and Trial Run
Trial with food colouring
Before you calibrate you can check that the project output's the correct value. I initially used an RGB LED to confirm that the sensor was detecting and displaying the values as expected. The above screenshots are from the Python Program which is measuring the RGB LED values. Then I tried with red green and blue food colouring, the 3 grey charts grouped together with the table are some excel graphs. Finally I tried with different concentrations of red food colouring, which is illustrated in the colourful excel graph attached. Once you are happy everything works as expected you can continue to calibrate.w
Creating samples with a known concentration (standards)
Your spectrophotometer will not work correctly without being calibrated. In this instructable I calibrated it to detect differing concentrations of KMnO4. I read on the internet a student setup was able to detect concentrations of 1*10^-4 mols per decilitre to 5*10^-4 mols per decilitre, so I decided to attempt to mix these concentrations. Molar concentration can be measured in mol/L, however preparing such specific amounts as above requires some careful planning, and the resolution of your weighing scales becomes important. In my case to get a Molar concentration of 1x10^-4, I would have to mix just 0.08g of KMnO4 with 5 litres of water! I made 5 "standards" of KMnO4 to measure. In the photographs above you can see 5 curvettes of potassium permanganate mixed (apparently) homogeneously in water. To do this I got 5 litres of water and first mixed it with 0.08g of KMnO4, and decanted approx 3ml into the first curvette. I added another 0.08g to double the concentration in the 5L bottle to get a mix of 0.16g/5L and decanted into the second curvette and so on until I had the 5 curvettes with the concentrations I needed.
You can now measure the standards with the sensor. The sensor will output a number between 0 to approx 60,000. This is an ADC count and is unit-less, which doesnt matter. As long as the unit remains the same (apples bananas, ADC counts) we can make a percentage calculation of transmittance in percent, and in turn calculate absorpbance. Measure each standard solution and plot the values of absorpbance (or transmittance) against concentration of each sample and you will have a linear trend-line which is in consonance with the beer-Lambert law. From the trend-line you can now determine any concentration of KMnO4 between the minimum and maximum calibration values. You can use the trend-line equation y=mx+c to determine this. The attached Excel file contains all my calculations and the measured results from the concerntrations of pottasium manganese.
Step 8: Conclusions
From the excel data it can be demonstrated, even with this DIY setup that the beer-lambert law holds true. The attached graphs reveal that the coefficient of determination is very close to 1.0 indicating a strong linear trend. However If your doing this for yourself it is important to consider uncertainty, as there will undoubtedly be a degree of uncertainty introduced for:
- when preparing and diluting sample solutes.
- The ions in tap water influencing the measurement
- The error when weighing KMnO4
- The homogeneity of the KMnO4 with the water
- Stray light entering the sensor from reflective surfaces inside the case
It's been fun looking into spectrophotometry, but it's best as an educational tool. It's no suprise that most instructables regarding spectrophotometry finishes up with measuring samples of food colouring rather than concentrations of chemical solutes. Hopefully I have demonstrated that you can use it to measure concentrations of solutions if you calibrate it correctly first, and pointed out the limitations.
The project's capability is limited by:
- The sensor - It can only monitor 6 channels, it would be nice to scan the entire spectrum with a monochromator or diffraction grating
- The LED - This is a light source with many missing wavelengths. It would be best to have a "true" polychromatic source like an incandescent light (I did try using one but it got quite hot and melted my plastic enclosure!)
- The complexity & cost - As the project becomes more complex the cost increases dramatically.
If you follow me on instructable's I intend to make an attempt at to making a more representative spectrophotometer with a polychromatic light source, diffraction grating and some stepper motors.