Using OPCSIM to Simulate a Nephelometer

This section of the tutorial will walk you through how we model Nephelometers, how you can build/model a Nephelometer, and how we can evaluate Nephelometers across a wide range of conditions using this tool.

# Make imports
import opcsim
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.ticker as mticks
import seaborn as sns

%matplotlib inline

# turn off warnings temporarily
import warnings
warnings.simplefilter('ignore')

# Let's set some default seaborn settings
sns.set(context='notebook', style='ticks', palette='dark', font_scale=1.75,
        rc={'figure.figsize': (12,6), **opcsim.plots.rc_log})

Nephelometer Representation

In OPCSIM, we define a Nephelometer using two parameters: the wavelength of light used in the device and its viewing angle. Unlike photometers and some optical particle counters, most low-cost commercial nephelometers gather light across as wide a range of angles as possible. This minimizes some of the uncertainty associated with the Mie resonance and allows manufacturers to use cheap photo-detectors while still gathering enough signal to distinguish from noise.

To build a Nephelometer, simply initialize using the opcsim.Nephelometer class:

# init a nephelometer with a 658 nm laser, gathering light from between 7-173 degrees
neph = opcsim.Nephelometer(wl=0.658, theta=(7., 173))

neph
<opcsim.models.Nephelometer at 0x7efe1950d4f0>

Calibration

Nephelometers gather the total scattered light from many anglees across an entire aerosol distribution. Typically, users of low-cost nephelometers co-locate their device with a reference device of higher (or known) quality and simply compare the output signal from the nephelometer to the integrated mass value (i.e. \(PM_1\), \(PM_{2.5}\), or \(PM_{10}\)) from the reference device. To keep things as simple and realistic as possible, we follow this approach.

To calibrate a nephelometer in OPCSIM, you provide an aerosol distribution to the calibrate method - the actual mass values for \(PM_1\), \(PM_{2.5}\), and \(PM_{10}\) are calculated exactly and the total scattered light is computed as well. The ratio between the total scattered light and each of the mass loadings are stored as calibration factors and are used again when evaluating previously unseen distributions.

To calibrate our nephelometer above to a synthetic distribution of Ammonium Sulfate:

d1 = opcsim.AerosolDistribution("AmmSulf")

d1.add_mode(n=1e4, gm=125e-3, gsd=1.5, refr=complex(1.521, 0), kappa=0.53, rho=1.77)

# calibrate the nephelometer at 0% RH
neph.calibrate(d1, rh=0.)

We can explore the calibration factors that were just determined - the units are a bit arbitrary, since we don’t consider the intensity/power of the laser as we assume it is constant. Thus, these units are something like \(cm^2/(\mu g/ m^3)\)

neph.pm1_ratio
1.1744058563022677e-08

Similarly, we get ratio’s for \(PM_{2.5}\) and \(PM_{10}\):

neph.pm25_ratio
1.174352137965417e-08
neph.pm10_ratio
1.1743521375694491e-08

Evaluating a Nephelometer for New Aerosol Distributions

The entire point of this tool is to be able to simulate what would happen under different circumstances. To do so, we use the evaluate method, which takes an AerosolDistribution as an argument (as well as an optional relative humidity) and returns the total scattered light, \(PM_1\), \(PM_{2.5}\), and \(PM_{10}\).

# evaluate the same distribution we used to calibrate
neph.evaluate(d1, rh=0.)
(4.4544608528839e-07, 37.92948433439533, 37.93121934108556, 37.9312193538752)
# evaluate the same distribution we used to calibrate, but at a higher RH
neph.evaluate(d1, rh=85.0)
(2.0830803505720823e-06,
 177.3731235580573,
 177.38123712884374,
 177.3812371886531)

What if we went ahead and tried to evaluate on a totally unseen distribution? Let’s go ahead and evaluate on an urban distribution:

d2 = opcsim.load_distribution("urban")

d2
AerosolDistribution: urban

First, let’s determine the actual \(PM_1\), \(PM_{2.5}\), and \(PM_{10}\) loadings for this distribution:

print ("PM1 = {:.2f} ug/m3".format(d2.cdf(dmin=0., dmax=1., weight='mass', rho=1.65)))
print ("PM2.5 = {:.2f} ug/m3".format(d2.cdf(dmin=0., dmax=2.5, weight='mass', rho=1.65)))
print ("PM10 = {:.2f} ug/m3".format(d2.cdf(dmin=0., dmax=10., weight='mass', rho=1.65)))
PM1 = 8.97 ug/m3
PM2.5 = 9.00 ug/m3
PM10 = 9.00 ug/m3

Next, let’s evaluate the Nephelometer:

neph.evaluate(d2, rh=0.)
(1.7166105544467465e-07,
 14.61684259521375,
 14.617511212784953,
 14.617511217713684)

So, we’re off by about a factor of 2, in part due to differences in assumed density and in part due to the fact the urban distribution scatters less light per unit mass than our calibration aerosol.