LaminAir¶

Here I introduce the LaminAir. Safer indoor dining. Safer working in the office. Double-digit exposure reduction factors, while breathing 12 inches away from the device. At least 14 times better performance than SmartAir's QT3 at that distance in reducing inhalation of bioaerosols like SARS-CoV-2 and other respiratory pathogens.

No description has been provided for this image
Front
No description has been provided for this image
Back
No description has been provided for this image
Side: Rechargeable, with USB-C charging
No description has been provided for this image
Other Side
No description has been provided for this image
Top
No description has been provided for this image
Top-Side view

Much thanks to @CleanAirStars and @ee_kelsey for doing the initial experiments that became the inspiration for LaminAir.

Available for purchase at https://breathesafe-llc.myshopify.com/products/laminair.

How well does it work?¶

I conducted a bunch of OSHA exercises to assess exposure reduction. I had the LaminAir 12 inches away from the face.

Overall, going through the set of OSHA exercises, I found that LaminAir has at least double-digit exposure reduction factors, which makes it a viable addition to one's multilayers of protection. Here are the results of my experiments:

No description has been provided for this image

Here's a way to compare different mitigations in terms of their exposure reduction factor: No description has been provided for this image

A lot of it is over the N95 lower bound of 20. An exposure reduction of 20 for an N95 typically means that the air is 20 times cleaner in terms of number of particles. i.e. If there are 100 particles to begin with, 95% get filtered out and only 5 pass through. $100 / 5 = 20$.

Note: Instead of using the number of particles, however, I measured using mass concentration. If the distribution of particles consists of mostly ultrafine particles (tiny, low mass particles) than fine particles, then a good mass concentration reduction can be achieved by mostly removing the bigger particles even while not making a dent on the smaller ultrafine particles. However, since I'm using a HEPA filter, which should filter with high efficiency across the board, I don't think the distinction matters that much.

How does it work?¶

To appreciate how it works, let's first dive into a discussion of lowering inhaled dose, and the exposure reduction factor.

Lowering the dose¶

Dose is essentially concentration times time. Lowering the inhaled dose leads to a lower probability of infection.

$$ \begin{equation} \begin{aligned} \text{Dose} = \text{Concentration} \times \text{time} \end{aligned} \end{equation} $$

To get a lower dose, one could:

  • remove the infector. This would mean concentration of dirty air will be 0.
  • if removing the infector is not possible (i.e. concentration is greater than 0), one could
    • spend less time in a room with the infector
    • clean the air to varying degrees before it is inhaled by an individual, via masking and air cleaning tools

When there is a constant generation rate of infectious aerosols, and there is a constant generation rate of clean air, it can be shown that in the long run, the concentration of a contaminant is the ratio between the rate of dirty air generation divided by the rate of clean air generation. See Appendix for the derivation, if interested.

$$ \begin{equation} \begin{aligned} \lim_{t \rightarrow \infty}\text{Concentration(t)} &= \frac{\text{generation rate of dirty air}}{\text{clean air delivery rate}} \end{aligned} \end{equation} $$

Concentration can be measured in a number of ways. One way is mass concentration, measured in micrograms per cubic meter (µg / m$^3$). Another is number of particles (e.g. parts per million -- ppm).

Exposure Reduction Factors¶

Another concept that will be useful in understanding how air cleaners and masks affect risk is the exposure reduction factor (also known as fit factor). In general:

$$ \begin{equation} \begin{aligned} \text{Exposure Reduction Factor(device)} &= \frac{\text{Concentration in the breathing area with the device turned off}}{\text{Concentration in the breathing area with the device turned on}} \end{aligned} \end{equation} $$

For masks, when the air is well-mixed (i.e. fans are turned on mixing the air), then in theory, sampling location doesn't affect concentration. One can sample anywhere and the concentration wouldn't change. For a respirator or a mask, we can simultaneously sample with two sensors, one in the breathing area (within the mask), and one near the breathing area (outside the mask):

$$ \begin{equation} \begin{aligned} \text{Exposure Reduction Factor(mask)} &= \frac{\text{Concentration not in the breathing area (outside the mask)}}{\text{Concentration in the breathing area (within the mask)}} \end{aligned} \end{equation} $$

The higher the exposure reduction factor, the more protective the device is for an individual. Different exercises can have different exposure reduction factors for the same individual. Exposure reduction factors help us understand how protective masks and respirators and room air cleaners, and personal air cleaners. Some will be a little bit more protective than others, while some much more so than others.

Case Study¶

In the NIOSH study of the Effectiveness of DIY Air Cleaners, researchers investigated how much DIY Air Cleaners such as Corsi-Rosenthal boxes decreased the concentration of contaminants in the air.

No description has been provided for this image

They had a classroom with 1.89 ACH of ventilation, with 6357 cubic feet as the volume of the room. Let's find out what the exposure reduction factor is when adding 2 CR boxes to a classroom with minimal ventilation.

$$ \begin{equation} \begin{aligned} \text{ACH} &= \frac{\text{CADR} (\frac{ \text{ ft}^3}{\text{hr}}) }{ \text{Volume} (\text{ft}^3)} \end{aligned} \end{equation} $$

Steps:

  • Figure out the clean air delivery rate in the minimal ventilation case, so we can compute concentration for that.
  • Figure out the clean air delivery rate after adding 2 CR boxes, so we can compute concentration for that.
  • Divide the two to get the exposure reduction factor.

We'll also take a look at the experimental results to see what exposure reduction factor the researchers got.

Solving for Clean Air Delivery Rate¶

Solving for the clean air delivery rate, which initially was only made of ventilation, we find that they had 200 cubic feet per minute of ventilation:

$$ \begin{equation} \begin{aligned} 1.89 (\frac{1}{\text{hr}}) &= \frac{\text{CADR } (\frac{\text{ft}^3}{\text{hr}})}{6357 \text{ ft}^3} \\ 12014.73 (\frac{\text{ft}^3}{\text{hr}}) &= \text{CADR } (\frac{\text{ft}^3}{\text{hr}}) \\ 12014.73 (\frac{\text{ft}^3}{\text{hr}}) \times \frac{1 \text{ hr}}{60 \text{ min}} &= \text{CADR } (\frac{\text{ft}^3}{hr}) \times \frac{1 \text{ hr}}{60 \text{ min}} \\ 200 (\frac{\text{ft}^3}{\text{min}})&= \text{CADR } (\frac{\text{ft}^3}{\text{min}}) \\ \end{aligned} \end{equation} $$

Concentration With Minimal Ventilation¶

Let's say we don't know the generation rate of aerosols $g$. Then the concentration in the long run will be:

$$ \begin{equation} \begin{aligned} \lim_{t \rightarrow \infty}\text{Concentration(t)} &= \frac{g \text{ CFM}}{200 \text{ CFM}} \\ &= \frac{g}{200} \\ \end{aligned} \end{equation} $$

Concentration With Minimal Ventilation + 2 CR boxes¶

In the study, the best performing CR box the NIOSH researchers created had an estimated clean air delivery rate of 655.5 CFM. See Appendix A. supplementary data for details. What if we added 2 CR boxes, each outputting 655.5 CFM? What would the concentration be?

$$ \begin{equation} \begin{aligned} \lim_{t \rightarrow \infty}\text{Concentration(t)} &= \frac{g \text{ CFM}}{200 \text{ CFM} + (655.5 \times 2) { CFM}} \\ &= \frac{g}{1511} \\ \end{aligned} \end{equation} $$

Exposure Reduction Factor for adding 2 CR boxes to minimum ventilation¶

How much cleaner is the air after adding the 2 CR boxes, relative to without the CR boxes?

$$ \begin{equation} \begin{aligned} \text{Exposure Reduction Factor} &= \frac{\text{Concentration of air with 200 CFM ventilation only}}{\text{Concentration of air with 200 CFM ventilation and 1311 CFM filtration}} \\ &= \frac{g / 200 }{g / 1511} \\ &= 7.415 \end{aligned} \end{equation} $$

In theory, assuming the room is well-mixed, people in the room will be decreasing their exposure by a factor of 7.4 by adding the 2 CR boxes, while keeping everything else constant.

What does the exposure reduction factor of 7.4 mean? It means that adding the 2 CR boxes to the room with minimal ventilation will result in 7.4 times cleaner air. For example, let's say the room with minimal ventilation had 100 micrograms per cubic meter (µg / m3). Then

$$ \begin{equation} \begin{aligned} \text{Exposure Reduction Factor} &= \frac{\text{Concentration with just 200 CFM ventilation}}{\text{Concentration with 2 CR boxes on max speed + 200 CFM ventilation}} \\ &= 7.4 \\ &= \frac{100 \frac{ \text{ µg}}{ \text{ m3}}}{13.5 \frac{ \text{ µg}}{ \text{ m3}}} \end{aligned} \end{equation} $$

We expect the concentration in the 2 CR boxes + 200 CFM regime to be around 13 micrograms per cubic meter, a reduction by a factor of 7.4.

Exposure Reduction Factor from Experimental Results¶

How does this compare to the experimental results of the NIOSH study on the Effectiveness of DIY Air Cleaners?

In NIOSH's experimental study, the addition of the two best-performing CR boxes that outputed about 1311 CFM resulted in a reduction of 78%, or an exposure reduction factor of 4.5, which is slightly lower than the 7.4 estimated earlier here. Assuming 100 µg / m3 to begin with, a reduction of 78% means that 22 µg / m3 would be left over, or a exposure reduction factor of 4.5:

$$ \begin{equation} \begin{aligned} \text{Exposure Reduction Factor} &= \frac{\text{Concentration with just 200 CFM ventilation}}{\text{Concentration with 2 CR boxes on max speed + 200 CFM ventilation}} \\ &= \frac{100 \frac{ \text{ µg}}{ \text{ m3}}}{22 \frac{ \text{ µg}}{ \text{ m3}}} \\ &= 4.5 \end{aligned} \end{equation} $$

I think the discrepancy could be caused by a multitude of factors -- only sampling from three positions and the room not actually being well-mixed. A fully well-mixed room is an ideal and doesn't happen in practice. However, the takeaway is the following. In terms of order of magnitude, the exposure reduction factor as predicted by theory and as computed in an actual experiment are in the single digits.

What if the susceptible wears a fit-tested N95?¶

An N95 respirator that is fit-tested and well-engineered (so that it retains the structural integrity even after jaw movements) should at least remove 95% of (non-oil) particles. If 95% are removed, then 5% remain. This results in a lower bound exposure reduction factor of 20:

$$ \begin{equation} \begin{aligned} \text{Exposure Reduction Factor(N95)} &= \frac{1}{1 - 0.95} \\ &= 20 \end{aligned} \end{equation} $$

How does it work, generally speaking?

The high filtration efficiency of an N95 plus no leakage means that the concentration being breathed in by the user is at least 20 times cleaner. So key ingredients are:

  • High filtration efficiency filter
  • some way to ensure that the clean air being produced by the N95 isn't mixing that much with the dirty air right before someone breathes in the clean air.

How can we develop an air cleaning device that produces a clean air bubble?¶

No description has been provided for this image

For the high filtration efficiency component, we can emulate what an N95 does by using a HEPA filter, which filters 99.97% of particulates.

If we were to breathe 0 inches away from the device, we should in theory get the following exposure reduction factor:

$$ \begin{equation} \begin{aligned} \text{Exposure Reduction Factor(HEPA at 0 inches away)} &= \frac{ 1 }{ 1 - 99.97\%} \\ &= 3,333 \end{aligned} \end{equation} $$

However, it is impractical to breathe 0 inches away from the device. In the next sections we cover a technique to preserve as much of the clean air as possible without a solid physical barrier, which is the laminar flow technique. It decreases mixing of the highly concentrated clean air with surrounding air (i.e. reduces entrainment).

What is Laminar Flow?¶

Air with laminar flow means that the particles of air are moving straight. This is in contrast to turbulent flow, where particles of air aren't flowing in the same direction.

No description has been provided for this image

The image above shows a fluid moving through a pipe in a laminar way vs. a turbulent way. Why does this distinction matter for air cleaning and risk reduction? Laminar flow leads to less entrainment than turbulent flow. Entrainment is the process of surrounding air mixing with some other air. If we have a clean air source and we are trying to get high exposure reduction factors (i.e. very low concentration of dirty air), we want it to keep it as clean as possible by decreasing entrainment from surrounding air which may contain infectious aerosols.

Traditional CR Box design produces turbulent flow¶

Turbulent flow leads to more mixing of clean air with the dirty air. If we were to inhale directly from a device without some physical barrier (e.g. a hood), then turbulent flow will increase the concentration of dirty air in what is being breathed.

The traditional "pull configuration" of the CR box is known to produce turbulent air. The box fan pulls air from the filters and then pushes it out in a swirly way, because of the rotation of the fan and because there isn't some material straightening the air as it goes out. The swirl motion pulls in a bunch of dirty air from the sides.

No description has been provided for this image

A more optimal configuration for risk reduction for an individual would be to switch to a push configuration.

Push configuration produces more laminar flow¶

Having the fan pull air first, and then pushing it through a filter, produces more laminar flow. Why so? The filter is a bunch of randomly scattered fibers. When the swirly air from a fan hits the filter, the air gets straightened. While some entrainment still exists, it's not as bad as that from a pull configuration.

No description has been provided for this image

TODO: use yellow color for what's coming out of the device, to denote that people are breathing in dirtier air because the filter behind a CR box is MERV-13, not HEPA.

Slower speeds lead to less entrainment¶

Air moves from high pressure to low pressure. High pressure areas have more particles (more dense) bumping against one another, pushing other particles towards low pressure areas, which have less particles.

No description has been provided for this image

There is lower pressure in air moving at high speed versus air moving at lower speed. Therefore if one is using the high speed setting for an personal air cleaning device, the breathing area will have lower air pressure than in the case of using a lower speed setting. The pressure differential between the surroundings and the breathing area causes entrainment. There will be a higher pressure differential with the high speed setting compared to the low speed setting, and thus, more entrainment in the former than in the latter.

No description has been provided for this image
Slower speeds, less entrained air
No description has been provided for this image
Higher speeds, more entrained air

Filters with a wide area are better than filters with a smaller area¶

When trying to breathe in highly concentrated clean air from a device, a wider area filter is most likely to give better results than a smaller area filter, given the same distance away from the device. When the air is generally still, entrainment is happening in the perimeter of the filter. If one is breathing in from the center of the device, using a wider filter versus a narrower filter, makes it so that the sides are farther from the center, so the less entrained air will be in the middle for the wider filter vs. the narrower filter.

Recap¶

The following design principles are behind the LaminAir, which make it much more effective for personal protection than other personal air cleaners:

  • High filtration efficiency (HEPA) filter.
  • Push configuration (as opposed to pull configuration), leading to more Laminar flow, and less entrainment.
  • Relatively wide filter compared to the existing competitors, leading to less entrainment in the middle, given a reasonable distance away from the device.
  • Slow airflow coming out of the filter, leading to less entrainment.

Limitations ⚠️¶

One can get very clean air when breathing 12 inches away from the device. However, there are caveats. Since there is no solid physical barrier enclosing the clean air before it is breathed in, laminar flow is sensitive to other air currents. Generally speaking, a good rule of thumb is if you can feel some sort of wind from other sources, you might not be getting the concentrated clean air.

  • Stay away from other devices that mix the air, especially box fans, CR boxes, vornados, etc.
  • If there is a room air cleaner present, sit as far away from it as possible. Have it not directly pointing towards you and the device.
  • If using the LaminAir in an airplane, turn off the gasper above you, or at least have it pointed away from you and decrease the speed.
  • If using this in a car (e.g. during a Lyft ride), close the vents near you, or at the very least, have it pointed away from the area between you and the device.

Methods¶

Production of Aerosols¶

Aqueous Solution of Salt as an Aerosol Source¶

I used Rob Wissman's instructions to create the mixture. He mentioned that in the Dal Porto, Cappa, Corsi paper, they used an aqueous salt solution of 100g / 1L to test Corsi-Rosenthal boxes. In the blog post linked in Rob's instructions, he mentioned that using regular salt has iodine, which makes it harder to aerosolize. Instead he recommends using a Kosher salt and distilled water to make the mixture as an option, and this is what I did.

\begin{equation} \begin{aligned} 100 g / L &= 100 g / L \cdot 1L \text{ }H_{2}O / 4.22675 \text{cups }H_{2}O \\ &= 100g \text{ salt} / 4.22675 \text{ cups water} \\ &= 23.658g \text{ salt} / 1 \text{ cup water} \end{aligned} \end{equation}

I didn't have access to weight scale, so I looked up the conversion of mass to volume for Kosher salt. There is 288 grams of salt per 1 cup. \begin{equation} \begin{aligned} 23.658g \text{ salt} / 1 \text{ cup water} \cdot 1 \text{ cup} / 288 g &= 0.08215 \text{ cups salt} / 1 \text{ cup water} \\ &= 0.08215 \text{ cups salt} / 1 \text{ cup water} \cdot 48 \text{ tsp} / 1 \text{ cup} \\ &= 3.9432 \text{ tsp salt} / 1 \text{ cup water} \end{aligned} \end{equation}

I rounded up and used 4 teaspoons of Kosher salt for the 1 cup of water.

Aerosolization of Salt¶

Instead of the Wellue nebulizer described in the Dal Porto, Cappa, Corsi paper paper and in Rob Wissman's instructions, I used an ultrasonic humidifier. The humidifier releases the salt solution in the air. The water evaporates, which then creates fine particulate matter, which can be suspended in the air for hours.

Detection of Aerosols¶

No description has been provided for this image

I used the Sensirion SPS-30 device. It gives readings of mass concentrations (micrograms per cubic meters: µg / m3) in several bins of sizes: PM 0.3-PM 1.0, PM 0.3-PM 2.5, PM 0.3-PM 4, and PM 0.3-PM 10. I used the PM 0.3-PM 1.0 for measurements because SPS30 is known to be very accurate for readings under PM 1.0 for detecting aerosolized salt. Here's a graph taken from Sousan et al, 2021, showing that SPS30 is very accurate for PM 1.0 readings across high concentrations, unlike other devices. It matches the $30,000 USD GRIMM MiniWRAS reference instrument quite well for PM 1.0:

No description has been provided for this image

Suite of Exercises¶

The suite of exercises I ran were mostly from OSHA to test the efficacy of the devices:

(1) Normal breathing. In a normal standing position, without talking, the subject shall breathe normally.

(2) Deep breathing. In a normal standing position, the subject shall breathe slowly and deeply, taking caution so as not to hyperventilate.

(3) Turning head side to side. Standing in place, the subject shall slowly turn his/her head from side to side between the extreme positions on each side. The head shall be held at each extreme momentarily so the subject can inhale at each side.

(4) Moving head up and down. Standing in place, the subject shall slowly move his/her head up and down. The subject shall be instructed to inhale in the up position (i.e., when looking toward the ceiling).

(5) Talking. The subject shall talk out loud slowly and loud enough so as to be heard clearly by the test conductor. The subject can read from a prepared text such as the Rainbow Passage, count backward from 100, or recite a memorized poem or song.

Rainbow Passage
When the sunlight strikes raindrops in the air, they act like a prism and form a rainbow. The rainbow is a division of white light into many beautiful colors. These take the shape of a long round arch, with its path high above, and its two ends apparently beyond the horizon. There is, according to legend, a boiling pot of gold at one end. People look, but no one ever finds it. When a man looks for something beyond reach, his friends say he is looking for the pot of gold at the end of the rainbow.

(6) Grimace. The test subject shall grimace by smiling or frowning. (This applies only to QNFT testing; it is not performed for QLFT)

(7) Bending over. The test subject shall bend at the waist as if he/she were to touch his/her toes. Jogging in place shall be substituted for this exercise in those test environments such as shroud type QNFT or QLFT units that do not permit bending over at the waist.

(8) Repeat #1

I skipped #7 and #8 for LaminAir. Number 7 doesn't apply since it's meant for someone sitting down. In theory I could have done #8 but I felt like other exercises are essentially like normal breathing, too. For example, grimace for LaminAir shouldn't actually make a difference because it doesn't depend on a tight seal to the face in order for it to work.

Use Two SPS30 Sensors to Simultaneously Measure Breathing Area and Ambient Concentrations¶

I used two sensors connected to a Raspberry Pi to simultaneously estimate the concentration in the breathing area as the LaminAir is blowing clean air to it, and the concentration in the surrounding area, around the hip, to estimate the concentration in the breathing area had the device not been turned on. See previous work for details.

Sampling Procedure & Calculations¶

  1. Run Humidifier and Mixing Fan for 2 minutes.
  2. Turn off Humidifier and let Mixing Fan run for 15 seconds.
  3. Turn on device (e.g. LaminAir). Wait for 15 seconds.
  4. Start measuring the breathing area concentration and the ambient area concentration for 1 minute.
  5. Turn off the device. Return to Step 1 but use 15 seconds instead.

Step 1 is to substantially increase the concentration to hundreds of micrograms per cubic meter so that we can large exposure reduction factors.

Step 2 is to see what happens when the room should be close to well-mixed. Are the two sensors reporting the same results? If not, there should be a calibration process so that the exposure reduction factors calculated actually make sense.

Step 3 is to get to the steady state concentration as reported by the sensors. It takes about 15 seconds for the readings to stabilize, probably due to the weak fan within the SPS30.

Step 4 is to measure long enough as deemed by OSHA for fit testing.

Step 5 is like Step 1 but shorter, so that we don't increase the concentrations in the air so much, just have it be stable enough across multiple exercises.

Appendix¶

Derivation of the Concentration Curve

Earlier, I claimed that the concentration in the long run is essentially the rate of dirty air generation divided by the clean air delivery rate. Below, I show how we got to this result.

The scenario we are considering is when the infector and susceptibles come in into the room at the same time, and that there were no infectors prior to the location (and therefore the initial concentration of dirty air is 0. The change in concentration over time is then dictated by the following equation:

$$\frac{dC}{dt} = \frac{-Q \cdot C_{t} + g}{V}$$

where

  • $Q$ is the non-infectious air delivery rate,
  • $C_t$ is the concentration at time $t$,
  • $g$ is the quanta generation rate, and
  • $V$ is the volume of the room.

What the above says is that some amount of air $g$ is being generated by the infector. We then divide it by the volume of the room $V$ to get the added concentration. In other words, $g / V$ is the proportion of dirty air that's being added. However, some non-infectious air is being delivered at a rate $Q$, made up of concentration $C_{t}$. In other words, $Q$ with a concentration of $C_t$ is being removed.

$$ \begin{equation} \begin{aligned} V \cdot \frac{dC}{dt} &= -Q \cdot C_t + g \\ B_t &= -Q \cdot C_t + g \\ B_0 &= -Q \cdot C_0 + g \\ \frac{dB_t}{dt} &= -Q \frac{dC_t}{dt} \\ -\frac{1}{Q} \cdot \frac{dB_t}{dt} &= \frac{dC_t}{dt} \\ -\frac{1}{Q} \cdot \frac{dB_t}{dt} &= \frac{B_t}{V} \\ -\frac{V}{Q} \cdot \frac{dB_t}{dt} &= B_t \\ -\frac{V}{Q} \cdot \frac{dB_t}{B_t} &= dt \\ \frac{dB_t}{B_t} &= -\frac{Q}{V} \cdot dt \\ \int_0^t \frac{1}{B_t} \cdot dB_t &= -\frac{Q}{V} \int_0^t dt \\ ln(B_t) - ln(B_0) &= -\frac{Q}{V} \cdot t \\ e^{ln(B_t) - ln(B_0)} &= e^{-\frac{Q}{V} \cdot t} \\ e^{ln(B_t)} /e^{ln(B_0)} &= e^{-\frac{Q}{V} \cdot t} \\ B_t / B_0 &= e^{-\frac{Q}{V} \cdot t} \\ B_t &= B_0 \cdot e^{-\frac{Q}{V} \cdot t} \\ -Q \cdot C_t + g &= (-Q \cdot C_0 + g) \cdot e^{-\frac{Q}{V} \cdot t} \\ -Q \cdot C_t &= -g + (-Q \cdot C_0 + g) \cdot e^{-\frac{Q}{V} \cdot t} \\ C_t &= g/Q + (C_0 - g/Q) \cdot e^{-\frac{Q}{V} \cdot t} \\ \end{aligned} \end{equation} $$

Given the scenario we are considering, where there were no infectors in the space before, so $C_0 = 0$, we have:

$$ \begin{equation} \begin{aligned} C_t &= g/Q - g/Q \cdot e^{-\frac{Q}{V} \cdot t} \\ C_t &= g/Q \cdot (1 - e^{-\frac{Q}{V} \cdot t}) \\ \end{aligned} \end{equation} $$

In the long run, we have:

$$ \begin{equation} \begin{aligned} lim_{t \rightarrow \infty} C_t &= g/Q \cdot lim_{t \rightarrow \infty} (1 - e^{-\frac{Q}{V} \cdot t}) \\ &= g / Q \end{aligned} \end{equation} $$

which is basically the rate of dirty air generation divided by clean air delivery rate. Thanks to Kow Akepanidtaworn for much help in this derivation.

Computations¶

In [1]:
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns

from utilities import between, plot, plot_one_graph, read_csv, \
    get_fit_factor_between_two_events, get_fit_factors, read_2_sensors, hmff, bar_plot_exposure_reduction_factors, \
    compute_cadr_via_anemometer_grid
In [2]:
dfs = read_2_sensors(template='../milestone_eoy_2023_{}.csv', columns=['pm1', 'n05'])
In [3]:
dfs
Out[3]:
{'breathing_area_df':                            pm1   pm4  pm25  pm10  n05    n1   n25    n4   n10  \
 timestamp                                                                       
 2023-12-15 19:45:40+00:00  0.8   8.4   5.0  10.1  0.0   1.0   5.3   6.2   6.4   
 2023-12-15 19:45:41+00:00  0.0   0.0   0.0   0.0  0.0   0.0   0.0   0.0   0.0   
 2023-12-15 19:45:42+00:00  0.4   4.2   2.5   5.0  0.0   0.5   2.6   3.1   3.2   
 2023-12-15 19:45:43+00:00  0.4   4.1   2.5   5.0  0.0   0.5   2.6   3.1   3.2   
 2023-12-15 19:45:45+00:00  0.4   4.1   2.4   4.9  0.0   0.5   2.6   3.0   3.1   
 ...                        ...   ...   ...   ...  ...   ...   ...   ...   ...   
 2023-12-16 03:24:54+00:00  2.6   9.9   6.7  11.5  6.7  16.1  20.1  21.0  21.2   
 2023-12-16 03:24:55+00:00  2.6   9.9   6.7  11.5  6.7  16.0  20.1  20.9  21.1   
 2023-12-16 03:24:56+00:00  2.6   9.9   6.7  11.5  6.8  16.1  20.1  21.0  21.2   
 2023-12-16 03:24:57+00:00  2.7  10.0   6.8  11.6  6.8  16.2  20.3  21.2  21.4   
 2023-12-16 03:24:58+00:00  2.7  10.2   6.9  11.8  7.0  16.6  20.8  21.7  21.9   
 
                            tps  pm1 breathing_area  n05 breathing_area  
 timestamp                                                               
 2023-12-15 19:45:40+00:00  1.6                 0.8                 0.0  
 2023-12-15 19:45:41+00:00  1.6                 0.0                 0.0  
 2023-12-15 19:45:42+00:00  1.6                 0.4                 0.0  
 2023-12-15 19:45:43+00:00  1.6                 0.4                 0.0  
 2023-12-15 19:45:45+00:00  1.6                 0.4                 0.0  
 ...                        ...                 ...                 ...  
 2023-12-16 03:24:54+00:00  1.1                 2.6                 6.7  
 2023-12-16 03:24:55+00:00  1.1                 2.6                 6.7  
 2023-12-16 03:24:56+00:00  1.1                 2.6                 6.8  
 2023-12-16 03:24:57+00:00  1.1                 2.7                 6.8  
 2023-12-16 03:24:58+00:00  1.1                 2.7                 7.0  
 
 [2999 rows x 12 columns],
 'ambient_area_df':                            pm1   pm4  pm25  pm10   n05    n1   n25    n4  \
 timestamp                                                                  
 2023-12-15 19:45:40+00:00  2.2   8.1   5.4   9.4   6.7  13.6  17.0  17.5   
 2023-12-15 19:45:41+00:00  0.0   0.0   0.0   0.0   0.0   0.0   0.0   0.0   
 2023-12-15 19:45:42+00:00  1.2   2.3   1.9   2.6   7.0   9.2   9.8   9.9   
 2023-12-15 19:45:43+00:00  1.2   2.5   1.9   2.7   6.9   9.2   9.8   9.9   
 2023-12-15 19:45:45+00:00  1.2   2.3   1.8   2.5   7.0   9.1   9.7   9.8   
 ...                        ...   ...   ...   ...   ...   ...   ...   ...   
 2023-12-16 03:24:54+00:00  2.9   8.8   6.2  10.0  11.8  19.4  22.7  23.3   
 2023-12-16 03:24:55+00:00  3.1   9.3   6.5  10.6  12.5  20.6  24.1  24.7   
 2023-12-16 03:24:57+00:00  3.2   9.6   6.8  11.0  13.0  21.4  25.0  25.6   
 2023-12-16 03:24:58+00:00  3.3  10.0   7.1  11.4  13.6  22.4  26.1  26.8   
 2023-12-16 03:24:59+00:00  3.4  10.1   7.1  11.6  13.8  22.7  26.4  27.1   
 
                             n10  tps  pm1 ambient_area  n05 ambient_area  
 timestamp                                                                 
 2023-12-15 19:45:40+00:00  17.6  1.0               2.2               6.7  
 2023-12-15 19:45:41+00:00   0.0  1.6               0.0               0.0  
 2023-12-15 19:45:42+00:00   9.9  0.7               1.2               7.0  
 2023-12-15 19:45:43+00:00  10.0  0.9               1.2               6.9  
 2023-12-15 19:45:45+00:00   9.8  0.9               1.2               7.0  
 ...                         ...  ...               ...               ...  
 2023-12-16 03:24:54+00:00  23.4  1.1               2.9              11.8  
 2023-12-16 03:24:55+00:00  24.8  1.1               3.1              12.5  
 2023-12-16 03:24:57+00:00  25.8  1.1               3.2              13.0  
 2023-12-16 03:24:58+00:00  26.9  1.0               3.3              13.6  
 2023-12-16 03:24:59+00:00  27.2  1.1               3.4              13.8  
 
 [2999 rows x 12 columns]}
In [4]:
dfs_2 = read_2_sensors(template='../milestone_eoy_2023_part_2_{}.csv')
In [5]:
dfs_2
Out[5]:
{'breathing_area_df':                            pm1   pm4  pm25  pm10  n05    n1   n25    n4   n10  \
 timestamp                                                                       
 2023-12-16 03:25:00+00:00  2.7  10.3   6.9  11.9  7.1  16.7  20.9  21.8  22.0   
 2023-12-16 03:25:01+00:00  2.7  10.3   7.0  11.9  7.1  16.8  21.0  21.9  22.1   
 2023-12-16 03:25:02+00:00  2.7  10.3   7.0  12.0  7.1  16.8  21.0  21.9  22.1   
 2023-12-16 03:25:03+00:00  2.7  10.3   7.0  12.0  7.1  16.8  21.0  21.9  22.1   
 2023-12-16 03:25:04+00:00  2.7  10.2   6.9  11.9  7.0  16.6  20.8  21.7  21.9   
 ...                        ...   ...   ...   ...  ...   ...   ...   ...   ...   
 2023-12-16 20:17:45+00:00  1.1   3.7   2.6   4.2  4.0   7.5   8.9   9.2   9.2   
 2023-12-16 20:17:47+00:00  1.2   3.8   2.7   4.4  4.3   7.8   9.3   9.6   9.7   
 2023-12-16 20:17:48+00:00  1.2   3.9   2.7   4.4  4.3   7.9   9.4   9.7   9.8   
 2023-12-16 20:17:49+00:00  1.2   3.9   2.7   4.5  4.4   8.0   9.5   9.8   9.9   
 2023-12-16 20:17:50+00:00  1.2   3.9   2.8   4.5  4.5   8.2   9.7  10.0  10.0   
 
                            tps  pm1 breathing_area  
 timestamp                                           
 2023-12-16 03:25:00+00:00  1.1                 2.7  
 2023-12-16 03:25:01+00:00  1.1                 2.7  
 2023-12-16 03:25:02+00:00  1.1                 2.7  
 2023-12-16 03:25:03+00:00  1.1                 2.7  
 2023-12-16 03:25:04+00:00  1.1                 2.7  
 ...                        ...                 ...  
 2023-12-16 20:17:45+00:00  1.0                 1.1  
 2023-12-16 20:17:47+00:00  1.0                 1.2  
 2023-12-16 20:17:48+00:00  1.0                 1.2  
 2023-12-16 20:17:49+00:00  1.0                 1.2  
 2023-12-16 20:17:50+00:00  1.0                 1.2  
 
 [1469 rows x 11 columns],
 'ambient_area_df':                            pm1   pm4  pm25  pm10   n05    n1   n25    n4  \
 timestamp                                                                  
 2023-12-16 03:25:00+00:00  3.4  10.2   7.2  11.6  13.9  22.8  26.6  27.3   
 2023-12-16 03:25:01+00:00  3.4  10.1   7.1  11.6  13.9  22.7  26.5  27.2   
 2023-12-16 03:25:02+00:00  3.4  10.0   7.1  11.5  13.9  22.6  26.4  27.0   
 2023-12-16 03:25:03+00:00  3.4  10.0   7.0  11.4  13.8  22.5  26.2  26.9   
 2023-12-16 03:25:05+00:00  3.3   9.9   7.0  11.3  13.7  22.4  26.0  26.7   
 ...                        ...   ...   ...   ...   ...   ...   ...   ...   
 2023-12-16 20:17:45+00:00  1.6   4.3   3.1   4.8   7.3  11.1  12.6  12.9   
 2023-12-16 20:17:46+00:00  1.6   4.3   3.1   4.9   7.4  11.2  12.7  13.0   
 2023-12-16 20:17:47+00:00  1.6   4.3   3.1   4.9   7.5  11.3  12.8  13.1   
 2023-12-16 20:17:48+00:00  1.6   4.3   3.1   4.9   7.6  11.4  12.9  13.2   
 2023-12-16 20:17:49+00:00  1.6   4.3   3.1   4.9   7.5  11.3  12.8  13.1   
 
                             n10  tps  pm1 ambient_area  
 timestamp                                               
 2023-12-16 03:25:00+00:00  27.4  1.1               3.4  
 2023-12-16 03:25:01+00:00  27.3  1.0               3.4  
 2023-12-16 03:25:02+00:00  27.2  1.0               3.4  
 2023-12-16 03:25:03+00:00  27.0  1.0               3.4  
 2023-12-16 03:25:05+00:00  26.8  1.0               3.3  
 ...                         ...  ...               ...  
 2023-12-16 20:17:45+00:00  12.9  0.9               1.6  
 2023-12-16 20:17:46+00:00  13.1  0.9               1.6  
 2023-12-16 20:17:47+00:00  13.2  0.9               1.6  
 2023-12-16 20:17:48+00:00  13.2  0.9               1.6  
 2023-12-16 20:17:49+00:00  13.1  0.9               1.6  
 
 [1467 rows x 11 columns]}
In [6]:
combined = {
    'breathing_area_df': pd.concat([dfs['breathing_area_df'], dfs_2['breathing_area_df']]),
    'ambient_area_df': pd.concat([dfs['ambient_area_df'], dfs_2['ambient_area_df']]),
}
In [7]:
normal_breathing_start_offset = pd.to_timedelta('15 seconds')
start_datetime = pd.to_datetime('2023-12-15 21:41:12 -05:00')
start_datetime_2 = pd.to_datetime('2023-12-15 22:11:30 -05:00')
start_datetime_3 = pd.to_datetime('2023-12-16 14:56:23 -05:00')


metadata = [
    {
        'title': 'Part I',
        'window': {
            'start': start_datetime, 
            'end': start_datetime + pd.to_timedelta('30 minutes 5 seconds')
        },
        'events': [
            {
                'event': 'start fan & humidifier',
                'timedelta': pd.to_timedelta('0 minutes 12 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('2 minutes 0 seconds')
            },
            {
                'event': 'turn off fan',
                'timedelta': pd.to_timedelta('2 minutes 20 seconds')
            },
            {
                'event': 'turn on AirGo',
                'timedelta': pd.to_timedelta('2 minutes 34 seconds')
            },
            {
                'event': 'AirGo - with visor - normal breathing',
                'timedelta': pd.to_timedelta('2 minutes 45 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('3 minutes 46 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('4 minutes 28 seconds')
            },
            {
                'event': 'turn off fan, turn on AirGo',
                'timedelta': pd.to_timedelta('4 minutes 51 seconds')
            },
            
            
        ]
    },
     {
        'title': 'Part II',
        'window': {
            'start': start_datetime, 
            'end': start_datetime + pd.to_timedelta('30 minutes 5 seconds')
        },
        'events': [
            {
                'event': 'AirGo - with visor - deep breathing',
                'timedelta': pd.to_timedelta('5 minutes 14 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('6 minutes 3 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('6 minutes 23 seconds')
            },
            {
                'event': 'turn off fan',
                'timedelta': pd.to_timedelta('6 minutes 36 seconds')
            },
            {
                'event': 'turn on AirGo',
                'timedelta': pd.to_timedelta('6 minutes 36 seconds')
            },
            {
                'event': 'AirGo - with visor - grimace',
                'timedelta': pd.to_timedelta('7 minutes 3 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('8 minutes 6 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('8 minutes 25 seconds')
            },
            {
                'event': 'turn on AirGo',
                'timedelta': pd.to_timedelta('8 minutes 52 seconds')
            },
        ]
    },
    {
        'title': 'Part III',
        'window': {
            'start': start_datetime, 
            'end': start_datetime + pd.to_timedelta('30 minutes 5 seconds')
        },
        'events': [
            {
                'event': 'AirGo - with visor - talking',
                'timedelta': pd.to_timedelta('9 minutes 0 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('9 minutes 56 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('10 minutes 15 seconds')
            },
            {
                'event': 'turn off fan, turn on AirGo',
                'timedelta': pd.to_timedelta('10 minutes 46 seconds')
            }, 
            {
                'event': 'AirGo - with visor - bend-over',
                'timedelta': pd.to_timedelta('11 minutes 8 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('12 minutes 3 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('12 minutes 27 seconds')
            },
            {
                'event': 'turn on AirGo',
                'timedelta': pd.to_timedelta('12 minutes 55 seconds')
            },
            {
                'event': 'turn on AirGo',
                'timedelta': pd.to_timedelta('12 minutes 55 seconds')
            },
        ]
    },
    {
        'title': 'Part IV',
        'window': {
            'start': start_datetime, 
            'end': start_datetime + pd.to_timedelta('30 minutes 5 seconds')
        },
        'events': [
            {
                'event': 'AirGo - with visor - side-to-side',
                'timedelta': pd.to_timedelta('13 minutes 3 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('14 minutes 14 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('14 minutes 30 seconds')
            },
            {
                'event': 'turn off fan, turn on AirGo',
                'timedelta': pd.to_timedelta('14 minutes 48 seconds')
            }, 
            {
                'event': 'AirGo - with visor - up-and-down',
                'timedelta': pd.to_timedelta('15 minutes 0 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('16 minutes 5 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('16 minutes 30 seconds')
            },
            {
                'event': 'turn on AirGo',
                'timedelta': pd.to_timedelta('16 minutes 40 seconds')
            },
        ]
    },
    {
        'title': 'Part V',
        'window': {
            'start': start_datetime, 
            'end': start_datetime + pd.to_timedelta('30 minutes 5 seconds')
        },
        'events': [
            {
                'event': 'AirGo - with visor - walking',
                'timedelta': pd.to_timedelta('16 minutes 53 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('17 minutes 59 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('18 minutes 7 seconds')
            },
            {
                'event': 'turn on Laminair',
                'timedelta': pd.to_timedelta('19 minutes 50 seconds')
            }, 
            {
                'event': 'LaminAir - normal breathing',
                'timedelta': pd.to_timedelta('20 minutes 5 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('21 minutes 5 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('21 minutes 15 seconds')
            },
            {
                'event': 'turn off fan',
                'timedelta': pd.to_timedelta('21 minutes 50 seconds')
            },
            {
                'event': 'turn on Laminair',
                'timedelta': pd.to_timedelta('22 minutes 8 seconds')
            },
        ]
    },
    {
        'title': 'Part VI',
        'window': {
            'start': start_datetime, 
            'end': start_datetime + pd.to_timedelta('30 minutes 5 seconds')
        },
        'events': [
            {
                'event': 'LaminAir - talking',
                'timedelta': pd.to_timedelta('22 minutes 15 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('23 minutes 13 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('23 minutes 38 seconds')
            },
            {
                'event': 'turn on Laminair',
                'timedelta': pd.to_timedelta('24 minutes 7 seconds')
            }, 
            {
                'event': 'LaminAir - side-to-side',
                'timedelta': pd.to_timedelta('24 minutes 33 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('25 minutes 34 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('26 minutes 3 seconds')
            },
            {
                'event': 'AirGo & LaminAir - turn on',
                'timedelta': pd.to_timedelta('26 minutes 25 seconds')
            },
            {
                'event': 'AirGo & LaminAir - normal breathing',
                'timedelta': pd.to_timedelta('26 minutes 57 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('28 minutes 3 seconds')
            },
        ]
    },
    {
        'title': 'Part VII',
        'window': {
            'start': start_datetime, 
            'end': start_datetime + pd.to_timedelta('30 minutes 5 seconds')
        },
        'events': [
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('28 minutes 29 seconds')
            },
            {
                'event': 'turn off fan',
                'timedelta': pd.to_timedelta('28 minutes 49 seconds')
            },
            {
                'event': 'AirGo & LaminAir - turn on',
                'timedelta': pd.to_timedelta('29 minutes 0 seconds')
            },
            {
                'event': 'AirGo & LaminAir - side-to-side',
                'timedelta': pd.to_timedelta('29 minutes 10 seconds')
            }, 
            {
                'event': 'AirGo & LaminAir - side-to-side - end',
                'timedelta': pd.to_timedelta('30 minutes 5 seconds')
            }
        ]
    },
    {
        'title': 'Part VII',
        'window': {
            'start': start_datetime_2, 
            'end': start_datetime_2 + pd.to_timedelta('2 minutes 4 seconds')
        },
        'events': [
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('0 minutes 18 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('0 minutes 28 seconds')
            },
            {
                'event': 'AirGo & LaminAir - turn on',
                'timedelta': pd.to_timedelta('0 minutes 44 seconds')
            },
            {
                'event': 'AirGo & LaminAir - talking',
                'timedelta': pd.to_timedelta('0 minutes 57 seconds')
            }, 
            {
                'event': 'AirGo & LaminAir - talking end',
                'timedelta': pd.to_timedelta('2 minutes 4 seconds')
            }
        ]
    },
    {
        'title': 'Part VIII',
        'window': {
            'start': start_datetime_3, 
            'end': start_datetime_3 + pd.to_timedelta('17 minutes 34 seconds')
        },
        'events': [
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('0 minutes 4 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('2 minutes 0 seconds')
            },
            {
                'event': 'LaminAir - turn on',
                'timedelta': pd.to_timedelta('2 minutes 30 seconds')
            },
            {
                'event': 'LaminAir - deep breathing',
                'timedelta': pd.to_timedelta('2 minutes 45 seconds')
            }, 
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('4 minutes 4 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('4 minutes 4 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('4 minutes 30 seconds')
            },
            {
                'event': 'turn off fan',
                'timedelta': pd.to_timedelta('4 minutes 54 seconds')
            },
            {
                'event': 'LaminAir - turn on',
                'timedelta': pd.to_timedelta('4 minutes 59 seconds')
            },
            {
                'event': 'LaminAir - grimace',
                'timedelta': pd.to_timedelta('5 minutes 18 seconds')
            },
            {
                'event': 'LaminAir - grimace - end',
                'timedelta': pd.to_timedelta('6 minutes 8 seconds')
            },
        ]
    },
    {
        'title': 'Part IX',
        'window': {
            'start': start_datetime_3, 
            'end': start_datetime_3 + pd.to_timedelta('17 minutes 34 seconds')
        },
        'events': [
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('6 minutes 8 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('6 minutes 35 seconds')
            },
            {
                'event': 'turn off fan',
                'timedelta': pd.to_timedelta('6 minutes 52 seconds')
            },
            {
                'event': 'LaminAir - turn on',
                'timedelta': pd.to_timedelta('7 minutes 3 seconds')
            }, 
            {
                'event': 'LaminAir - up-and-down',
                'timedelta': pd.to_timedelta('7 minutes 21 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('8 minutes 19 seconds')
            },
            {
                'event': 'LaminAir - turn on',
                'timedelta': pd.to_timedelta('9 minutes 16 seconds')
            },
            {
                'event': 'AirGo & LaminAir - deep breathing',
                'timedelta': pd.to_timedelta('9 minutes 29 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('10 minutes 33 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('11 minutes 6 seconds')
            },
            {
                'event': 'AirGo & LaminAir - turn on',
                'timedelta': pd.to_timedelta('11 minutes 27 seconds')
            },
        ]
    },
    {
        'title': 'Part X',
        'window': {
            'start': start_datetime_3, 
            'end': start_datetime_3 + pd.to_timedelta('17 minutes 34 seconds')
        },
        'events': [
            {
                'event': 'AirGo & LaminAir - grimace',
                'timedelta': pd.to_timedelta('11 minutes 46 seconds')
            },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('12 minutes 52 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('13 minutes 29 seconds')
            },
            {
                'event': 'turn off fan',
                'timedelta': pd.to_timedelta('13 minutes 47 seconds')
            }, 
            # {
            #     'event': 'AirGo & LaminAir - side-to-side',
            #     'timedelta': pd.to_timedelta('14 minutes 0 seconds')
            # },
            {
                'event': 'turn on fan and humidifier',
                'timedelta': pd.to_timedelta('15 minutes 7 seconds')
            },
            {
                'event': 'turn off humidifier',
                'timedelta': pd.to_timedelta('15 minutes 42 seconds')
            },
            {
                'event': 'turn off fan',
                'timedelta': pd.to_timedelta('16 minutes 0 seconds')
            },
            {
                'event': 'AirGo & LaminAir - turn on',
                'timedelta': pd.to_timedelta('16 minutes 0 seconds')
            },
            {
                'event': 'AirGo & LaminAir - up-and-down',
                'timedelta': pd.to_timedelta('16 minutes 30 seconds')
            },
            {
                'event': 'AirGo & LaminAir - up-and-down',
                'timedelta': pd.to_timedelta('17 minutes 34 seconds')
            }
            
        ]
    },
        
]

Plots¶

Mass Concentration:

In [8]:
plot(
    metadata, 
    breathing_area_data=combined['breathing_area_df'], 
    ambient_data=combined['ambient_area_df'],
    breathing_area_vars=['pm1 breathing_area'],
    ambient_vars=['pm1 ambient_area'],
    row_size=4,
)
No description has been provided for this image
In [9]:
fit_factors = get_fit_factors(
    [
        {
            'metadata': metadata,
            'breathing_area_sensor_data': combined['breathing_area_df'],
            'ambient_sensor_data': combined['ambient_area_df']
        }
    ],
    title='graph',
    breathing_area_column='pm1 breathing_area',
    ambient_column='pm1'
)
/Users/eugaddan/Developer/iaq/utilities.py:120: RuntimeWarning: invalid value encountered in scalar divide
  return numerator / denominator
In [10]:
fit_factors
Out[10]:
graph event fit_factor
0 Part I start fan & humidifier 1.864577
1 Part I turn off humidifier 1.572468
2 Part I turn off fan 2.729688
3 Part I turn on AirGo 16.941429
4 Part I AirGo - with visor - normal breathing 14.341690
... ... ... ...
79 Part X turn on fan and humidifier 1.727325
80 Part X turn off humidifier 1.347302
81 Part X turn off fan NaN
82 Part X AirGo & LaminAir - turn on 5.107849
83 Part X AirGo & LaminAir - up-and-down 284.254222

84 rows × 3 columns

In [11]:
no_na = fit_factors[fit_factors['fit_factor'].notna()].copy()
In [12]:
no_na
Out[12]:
graph event fit_factor
0 Part I start fan & humidifier 1.864577
1 Part I turn off humidifier 1.572468
2 Part I turn off fan 2.729688
3 Part I turn on AirGo 16.941429
4 Part I AirGo - with visor - normal breathing 14.341690
... ... ... ...
78 Part X turn off fan 14.433278
79 Part X turn on fan and humidifier 1.727325
80 Part X turn off humidifier 1.347302
82 Part X AirGo & LaminAir - turn on 5.107849
83 Part X AirGo & LaminAir - up-and-down 284.254222

80 rows × 3 columns

Calibration¶

After turning on the humidifier and the mixing fan, while the device(s) such as the AirGo and LaminAir are turned off, we should expect a fit factor of 1. In other words, when the air is well mixed, the concentration in the breathing area should be the same as the concentration in any other area. However, we don't see this pattern. We see that the concentration in the breathing area is consistently smaller than the concentration in the ambient area. This suggests that there is a statistical bias that needs to be corrected for so that the interpretation of the fit factor is correct. Below, we can see that the fit factor is about 1.4 - 1.6 when it really should be about 1. We'll divide the fit factors by this value to adjust for this discrepancy.

In [13]:
no_na[no_na['event'].str.contains('off humidifier')]
Out[13]:
graph event fit_factor
1 Part I turn off humidifier 1.572468
6 Part I turn off humidifier 1.605354
9 Part II turn off humidifier 1.574754
14 Part II turn off humidifier 1.346522
17 Part III turn off humidifier 1.489655
21 Part III turn off humidifier 1.586929
25 Part IV turn off humidifier 1.480119
29 Part IV turn off humidifier 1.400237
32 Part V turn off humidifier 1.430543
36 Part V turn off humidifier 1.462251
40 Part VI turn off humidifier 1.405388
44 Part VI turn off humidifier 1.385183
47 Part VII turn off humidifier 1.405595
52 Part VII turn off humidifier 1.414311
56 Part VIII turn off humidifier 1.392232
61 Part VIII turn off humidifier 1.293907
66 Part IX turn off humidifier 1.383300
74 Part IX turn off humidifier 1.383234
77 Part X turn off humidifier 1.365158
80 Part X turn off humidifier 1.347302
In [14]:
no_na[no_na['event'].str.contains('off humidifier')]['fit_factor'].plot.hist()
Out[14]:
<Axes: ylabel='Frequency'>
No description has been provided for this image
In [15]:
no_na['adjusted fit factor 1'] = no_na['fit_factor'] / 1.4
no_na['adjusted fit factor 2'] = no_na['fit_factor'] / 1.6
In [ ]:
 
In [ ]:
 
In [16]:
results = no_na[no_na['event'].str.contains('AirGo|LaminAir') & ~no_na['event'].str.contains('turn')].copy()
results
Out[16]:
graph event fit_factor adjusted fit factor 1 adjusted fit factor 2
4 Part I AirGo - with visor - normal breathing 14.341690 10.244064 8.963556
7 Part II AirGo - with visor - deep breathing 16.990390 12.135993 10.618994
12 Part II AirGo - with visor - grimace 12.048281 8.605915 7.530176
15 Part III AirGo - with visor - talking 11.442173 8.172981 7.151358
19 Part III AirGo - with visor - bend-over 14.652330 10.465950 9.157706
23 Part IV AirGo - with visor - side-to-side 1.696143 1.211530 1.060089
27 Part IV AirGo - with visor - up-and-down 3.320207 2.371577 2.075129
30 Part V AirGo - with visor - walking 4.825042 3.446459 3.015652
34 Part V LaminAir - normal breathing 27.692461 19.780329 17.307788
38 Part VI LaminAir - talking 184.936287 132.097348 115.585179
42 Part VI LaminAir - side-to-side 18.587357 13.276684 11.617098
46 Part VI AirGo & LaminAir - normal breathing 82.561398 58.972427 51.600874
50 Part VII AirGo & LaminAir - side-to-side 72.862997 52.044998 45.539373
54 Part VII AirGo & LaminAir - talking 203.132099 145.094356 126.957562
58 Part VIII LaminAir - deep breathing 122.477090 87.483636 76.548181
64 Part VIII LaminAir - grimace 138.285520 98.775371 86.428450
69 Part IX LaminAir - up-and-down 132.497512 94.641080 82.810945
72 Part IX AirGo & LaminAir - deep breathing 255.226344 182.304531 159.516465
75 Part X AirGo & LaminAir - grimace 113.663963 81.188545 71.039977
83 Part X AirGo & LaminAir - up-and-down 284.254222 203.038730 177.658889
In [17]:
results
Out[17]:
graph event fit_factor adjusted fit factor 1 adjusted fit factor 2
4 Part I AirGo - with visor - normal breathing 14.341690 10.244064 8.963556
7 Part II AirGo - with visor - deep breathing 16.990390 12.135993 10.618994
12 Part II AirGo - with visor - grimace 12.048281 8.605915 7.530176
15 Part III AirGo - with visor - talking 11.442173 8.172981 7.151358
19 Part III AirGo - with visor - bend-over 14.652330 10.465950 9.157706
23 Part IV AirGo - with visor - side-to-side 1.696143 1.211530 1.060089
27 Part IV AirGo - with visor - up-and-down 3.320207 2.371577 2.075129
30 Part V AirGo - with visor - walking 4.825042 3.446459 3.015652
34 Part V LaminAir - normal breathing 27.692461 19.780329 17.307788
38 Part VI LaminAir - talking 184.936287 132.097348 115.585179
42 Part VI LaminAir - side-to-side 18.587357 13.276684 11.617098
46 Part VI AirGo & LaminAir - normal breathing 82.561398 58.972427 51.600874
50 Part VII AirGo & LaminAir - side-to-side 72.862997 52.044998 45.539373
54 Part VII AirGo & LaminAir - talking 203.132099 145.094356 126.957562
58 Part VIII LaminAir - deep breathing 122.477090 87.483636 76.548181
64 Part VIII LaminAir - grimace 138.285520 98.775371 86.428450
69 Part IX LaminAir - up-and-down 132.497512 94.641080 82.810945
72 Part IX AirGo & LaminAir - deep breathing 255.226344 182.304531 159.516465
75 Part X AirGo & LaminAir - grimace 113.663963 81.188545 71.039977
83 Part X AirGo & LaminAir - up-and-down 284.254222 203.038730 177.658889
In [18]:
results['exercises'] = results['event'].str.extract('(?<=AirGo - with visor - )(.*)')[0]
In [19]:
results
Out[19]:
graph event fit_factor adjusted fit factor 1 adjusted fit factor 2 exercises
4 Part I AirGo - with visor - normal breathing 14.341690 10.244064 8.963556 normal breathing
7 Part II AirGo - with visor - deep breathing 16.990390 12.135993 10.618994 deep breathing
12 Part II AirGo - with visor - grimace 12.048281 8.605915 7.530176 grimace
15 Part III AirGo - with visor - talking 11.442173 8.172981 7.151358 talking
19 Part III AirGo - with visor - bend-over 14.652330 10.465950 9.157706 bend-over
23 Part IV AirGo - with visor - side-to-side 1.696143 1.211530 1.060089 side-to-side
27 Part IV AirGo - with visor - up-and-down 3.320207 2.371577 2.075129 up-and-down
30 Part V AirGo - with visor - walking 4.825042 3.446459 3.015652 walking
34 Part V LaminAir - normal breathing 27.692461 19.780329 17.307788 NaN
38 Part VI LaminAir - talking 184.936287 132.097348 115.585179 NaN
42 Part VI LaminAir - side-to-side 18.587357 13.276684 11.617098 NaN
46 Part VI AirGo & LaminAir - normal breathing 82.561398 58.972427 51.600874 NaN
50 Part VII AirGo & LaminAir - side-to-side 72.862997 52.044998 45.539373 NaN
54 Part VII AirGo & LaminAir - talking 203.132099 145.094356 126.957562 NaN
58 Part VIII LaminAir - deep breathing 122.477090 87.483636 76.548181 NaN
64 Part VIII LaminAir - grimace 138.285520 98.775371 86.428450 NaN
69 Part IX LaminAir - up-and-down 132.497512 94.641080 82.810945 NaN
72 Part IX AirGo & LaminAir - deep breathing 255.226344 182.304531 159.516465 NaN
75 Part X AirGo & LaminAir - grimace 113.663963 81.188545 71.039977 NaN
83 Part X AirGo & LaminAir - up-and-down 284.254222 203.038730 177.658889 NaN
In [20]:
results['exercises'] = results['exercises'].mask(
    results['event'].str.extract('(?<=LaminAir - )(.*)')[0].notna(), 
    results['event'].str.extract('(?<=LaminAir - )(.*)')[0] 
)
In [21]:
results
Out[21]:
graph event fit_factor adjusted fit factor 1 adjusted fit factor 2 exercises
4 Part I AirGo - with visor - normal breathing 14.341690 10.244064 8.963556 normal breathing
7 Part II AirGo - with visor - deep breathing 16.990390 12.135993 10.618994 deep breathing
12 Part II AirGo - with visor - grimace 12.048281 8.605915 7.530176 grimace
15 Part III AirGo - with visor - talking 11.442173 8.172981 7.151358 talking
19 Part III AirGo - with visor - bend-over 14.652330 10.465950 9.157706 bend-over
23 Part IV AirGo - with visor - side-to-side 1.696143 1.211530 1.060089 side-to-side
27 Part IV AirGo - with visor - up-and-down 3.320207 2.371577 2.075129 up-and-down
30 Part V AirGo - with visor - walking 4.825042 3.446459 3.015652 walking
34 Part V LaminAir - normal breathing 27.692461 19.780329 17.307788 normal breathing
38 Part VI LaminAir - talking 184.936287 132.097348 115.585179 talking
42 Part VI LaminAir - side-to-side 18.587357 13.276684 11.617098 side-to-side
46 Part VI AirGo & LaminAir - normal breathing 82.561398 58.972427 51.600874 normal breathing
50 Part VII AirGo & LaminAir - side-to-side 72.862997 52.044998 45.539373 side-to-side
54 Part VII AirGo & LaminAir - talking 203.132099 145.094356 126.957562 talking
58 Part VIII LaminAir - deep breathing 122.477090 87.483636 76.548181 deep breathing
64 Part VIII LaminAir - grimace 138.285520 98.775371 86.428450 grimace
69 Part IX LaminAir - up-and-down 132.497512 94.641080 82.810945 up-and-down
72 Part IX AirGo & LaminAir - deep breathing 255.226344 182.304531 159.516465 deep breathing
75 Part X AirGo & LaminAir - grimace 113.663963 81.188545 71.039977 grimace
83 Part X AirGo & LaminAir - up-and-down 284.254222 203.038730 177.658889 up-and-down
In [22]:
results['model'] = results['event'].str.extract('(.*)(?= -)')[0]
In [23]:
results
Out[23]:
graph event fit_factor adjusted fit factor 1 adjusted fit factor 2 exercises model
4 Part I AirGo - with visor - normal breathing 14.341690 10.244064 8.963556 normal breathing AirGo - with visor
7 Part II AirGo - with visor - deep breathing 16.990390 12.135993 10.618994 deep breathing AirGo - with visor
12 Part II AirGo - with visor - grimace 12.048281 8.605915 7.530176 grimace AirGo - with visor
15 Part III AirGo - with visor - talking 11.442173 8.172981 7.151358 talking AirGo - with visor
19 Part III AirGo - with visor - bend-over 14.652330 10.465950 9.157706 bend-over AirGo - with visor
23 Part IV AirGo - with visor - side-to-side 1.696143 1.211530 1.060089 side-to-side AirGo - with visor
27 Part IV AirGo - with visor - up-and-down 3.320207 2.371577 2.075129 up-and-down AirGo - with visor
30 Part V AirGo - with visor - walking 4.825042 3.446459 3.015652 walking AirGo - with visor
34 Part V LaminAir - normal breathing 27.692461 19.780329 17.307788 normal breathing LaminAir
38 Part VI LaminAir - talking 184.936287 132.097348 115.585179 talking LaminAir
42 Part VI LaminAir - side-to-side 18.587357 13.276684 11.617098 side-to-side LaminAir
46 Part VI AirGo & LaminAir - normal breathing 82.561398 58.972427 51.600874 normal breathing AirGo & LaminAir
50 Part VII AirGo & LaminAir - side-to-side 72.862997 52.044998 45.539373 side-to-side AirGo & LaminAir
54 Part VII AirGo & LaminAir - talking 203.132099 145.094356 126.957562 talking AirGo & LaminAir
58 Part VIII LaminAir - deep breathing 122.477090 87.483636 76.548181 deep breathing LaminAir
64 Part VIII LaminAir - grimace 138.285520 98.775371 86.428450 grimace LaminAir
69 Part IX LaminAir - up-and-down 132.497512 94.641080 82.810945 up-and-down LaminAir
72 Part IX AirGo & LaminAir - deep breathing 255.226344 182.304531 159.516465 deep breathing AirGo & LaminAir
75 Part X AirGo & LaminAir - grimace 113.663963 81.188545 71.039977 grimace AirGo & LaminAir
83 Part X AirGo & LaminAir - up-and-down 284.254222 203.038730 177.658889 up-and-down AirGo & LaminAir
In [24]:
pivot = results.pivot(
    index='exercises',
    columns='model',
    values='adjusted fit factor 1'
)
In [25]:
pivot.apply(hmff)
Out[25]:
model
AirGo & LaminAir      91.198174
AirGo - with visor     3.899901
LaminAir              36.239239
dtype: float64
In [26]:
transpose = pd.DataFrame(pivot.apply(hmff)).T
In [27]:
transpose['model'] = 'overall'
In [28]:
overall_fit_factor_df = transpose.reset_index().set_index('model')[['AirGo & LaminAir', 'AirGo - with visor', 'LaminAir']]
In [29]:
pivot_with_overall_fit_factor = pd.concat([pivot, overall_fit_factor_df])

Exposure Reduction Factors of LaminAir, AirGo prototype (with visor), and combined¶

In [30]:
fig, ax = plt.subplots(1,1, figsize=(10,5))
sns.heatmap(pivot_with_overall_fit_factor, annot=True, fmt='g', ax=ax)
ax.set_title("Exposure Reduction Factors (mass concentration)")
Out[30]:
Text(0.5, 1.0, 'Exposure Reduction Factors (mass concentration)')
No description has been provided for this image
In [31]:
pivot
Out[31]:
model AirGo & LaminAir AirGo - with visor LaminAir
exercises
bend-over NaN 10.465950 NaN
deep breathing 182.304531 12.135993 87.483636
grimace 81.188545 8.605915 98.775371
normal breathing 58.972427 10.244064 19.780329
side-to-side 52.044998 1.211530 13.276684
talking 145.094356 8.172981 132.097348
up-and-down 203.038730 2.371577 94.641080
walking NaN 3.446459 NaN

LaminAir Performance Over a Suite of Exercises¶

In [32]:
blah = pivot_with_overall_fit_factor[['LaminAir']]
blah[blah.index == 'overall']
Out[32]:
model LaminAir
overall 36.239239
In [33]:
fig, ax = plt.subplots(figsize=(10,5))
laminair = pivot_with_overall_fit_factor[['LaminAir']].dropna().sort_values(by="LaminAir")
laminair.plot.bar(ax=ax)
ax.set_ylabel("Exposure Reduction Factor")
ax.set_title("LaminAir performance over different exercises")
ax.axhline(20, color='r')
ax.axhline(100, color='g')
plt.xticks(rotation=45)
ax.legend(['Lower Bound Exposure Reduction Factor for N95, no leakage', 'Typical Exposure Reduction Factor for N95, no leakage'])

#laminair[laminair.index == 'overall'].plot.bar(color='r', ax=ax)
Out[33]:
<matplotlib.legend.Legend at 0x1327843d0>
No description has been provided for this image
In [34]:
pivot
Out[34]:
model AirGo & LaminAir AirGo - with visor LaminAir
exercises
bend-over NaN 10.465950 NaN
deep breathing 182.304531 12.135993 87.483636
grimace 81.188545 8.605915 98.775371
normal breathing 58.972427 10.244064 19.780329
side-to-side 52.044998 1.211530 13.276684
talking 145.094356 8.172981 132.097348
up-and-down 203.038730 2.371577 94.641080
walking NaN 3.446459 NaN
In [35]:
fit_factor_context = pd.DataFrame([
    {
        'protection': 'well-fitted cloth mask',
        'exposure reduction factor': 2,
        'relative reduction': '50%',
        'reference link': 'https://docs.google.com/spreadsheets/d/16K1OQkLD4BjgBdO8ePj6ytf-RpPMlJ6aXFg3PrIQBbQ/edit#gid=519189277',
        'notes': 'Jose Luis-Jimenez et. al claim that the range is between 30%-50% reduction for inhaled aerosols when wearing a cloth mask,' +
            ' with the upper range more for those that do not have gaps between the mask and the face.',
        'image_url': 'https://d.newsweek.com/en/full/1713771/biden-masks-covid-19.png'
    },
    # {
    #     'protection': 'N95 with no leaks lower bound',
    #     'exposure reduction factor': 20,
    #     'relative reduction': '>= 95%',
    #     'reference link': 'https://www.cdc.gov/niosh/npptl/topics/respirators/disp_part/default.html',
    #     'notes': 'NIOSH',
    #     'image_url': 'https://www.iceclasses.com/wp-content/uploads/2020/05/qualitive-fit-test.jpg'
    # },
    {
        'protection': 'N95 with leakage',
        'exposure reduction factor': 10,
        'relative reduction': '90%',
        'reference link': 'https://docs.google.com/spreadsheets/d/16K1OQkLD4BjgBdO8ePj6ytf-RpPMlJ6aXFg3PrIQBbQ/edit#gid=519189277',
        'notes': 'Jose Luis-Jimenez & Linsey Marr claim that wearing an N95 with some gaps between the face and the respirator decreases'
            + ' the relative reduction to about 90%, though that might be generous for some. This is typical for most users.',
        'image_url': 'https://aidwaycares.com/cdn/shop/products/9502B3_1024x1024@2x.jpg?v=1646244381'
    },
    # {
    #     'protection': 'Typical N95 with no leakage',
    #     'exposure reduction factor': 100,
    #     'relative reduction': '99%',
    #     'reference link': 'https://docs.google.com/spreadsheets/d/16K1OQkLD4BjgBdO8ePj6ytf-RpPMlJ6aXFg3PrIQBbQ/edit#gid=519189277',
    #     'notes': 'Jose Luis-Jimenez & Linsey Marr claim that most N95s are around the 99% range, assuming no leakage.',
    #     'image_url': 'https://shop.demetech.us/cdn/shop/products/MINIATURACIRCULODT-N95-CH2.png?v=1637259871'
    # },

    {
        'protection': 'Universal cloth masking',
        'exposure reduction factor': 4,
        'relative reduction': '75%',
        'reference link': 'https://blogs.cdc.gov/niosh-science-blog/2023/02/03/diy-filtration/',
        'notes': "NIOSH researchers find that when the source uses a cloth mask and the susceptibles also use a cloth mask, we get a multiplicative protective effect"
            + " that aligns with Jose Luis Jimenez's model. The original aerosol amount got reduced by 50% via the cloth mask of the source, which then got further "
            + " reduced by another 50% via the cloth mask of the susceptible. This led to a 75% reduction, or a fit factor of 4.",
        'image_url': 'https://goldarrowcamp.com/app/uploads/2020/05/IMG_1473-825x510.jpeg'
    },

    {
        'protection': '2 Corsi-Rosenthal Boxes running on max, added to 200 CFM ventilation',
        'exposure reduction factor': 5,
        'relative reduction': '80%',
        'reference link': 'https://www.sciencedirect.com/science/article/pii/S0360132322011507#sec2',
        'notes': 'Adding an additional 1311 CFM of clean air to a classroom with 200 CFM ventilation (1.89 ACH) led to an average of 80% reduction of inhaled aerosols across 3 sensors.'
            + ' The 1311 CFM came from 2 CR boxesmade up of Air King model 4CH71G/9723G as the fan, '
            + 'and AIR HANDLER Pleated Air Filter: 20x20x2, MERV 13 (Grainger) as the filters ',
        'image_url': 'https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQGDhR8zBENToo83usQCUIBxeR9HHj5OU5QaQ&usqp=CAU'
    },
    {
        'protection': '2 Corsi-Rosenthal Boxes running on max + Universal cloth-masking, added to 200 CFM ventilation',
        'exposure reduction factor': 16.67,
        'relative reduction': '94%',
        'reference link': 'https://www.sciencedirect.com/science/article/pii/S0360132322011507#sec2',
        'notes': 'Adding an additional 1300 CFM of clean air to a classroom with 200 CFM ventilation (1.89 ACH) led to an 80% reduction using the Air King fan and Grainger filters for CR boxes.'
            + ' When combined with universal masking with 3-ply cloth masks, aerosols were reduced by 94%. This makes sense from a modeling perspective. '
            + 'Assuming a 50% reduction via the source 3-ply cloth mask, the original amount gets reduced by 50%. '
            + '80% reduction of the remaining 50% means that 20% of 50% is left over, which is 10% of the original. '
            + 'That gets further reduced by 50% through the 3-ply cloth mask on the mannequin representing the susceptible, '
            + 'leading to 5% of the original. i.e. 95% reduction, which is close to the experimental result of 94% reduction.',
        'image_url': 'https://breathesafe-airgo.s3.us-east-2.amazonaws.com/images/2-CR-boxes-and-universal-masking.png'
    },
    {
        'protection': 'LaminAir, 12-inches away',
        'exposure reduction factor': 36,
        'relative reduction': '97.3%',
        'reference link': '',
        'image_url': 'https://breathesafe-airgo.s3.us-east-2.amazonaws.com/images/laminair/front.png'
    },
    {
        'protection': 'QT3 on Max Speed, 12-inches away',
        'exposure reduction factor': 2.5,
        'relative reduction': '60%',
        'reference link': 'https://smartairfilters.com/en/blog/qt3-portable-air-purifier-effectiveness/',
        'image_url': 'https://www.smarterhepa.com/cdn/shop/products/shipin040101_1512x.jpg',
        'notes': "FitTestThePlanet's estimate was even lower than SmartAir's own testing. See https://twitter.com/FitTestMyPlanet/status/1685793478918164480/photo/1"
    },
        
])
In [36]:
sorted_ff = fit_factor_context.sort_values(by='exposure reduction factor')
In [37]:
copy_ff = sorted_ff.copy()
In [38]:
copy_ff['exposure reduction factor'] = 0
copy_ff.loc[copy_ff['protection'] == 'LaminAir, 12-inches away', 'exposure reduction factor'] = 36
In [39]:
merge = sorted_ff.merge(copy_ff, on='protection', suffixes=('_actual', '_coloring'))
merge
Out[39]:
protection exposure reduction factor_actual relative reduction_actual reference link_actual notes_actual image_url_actual exposure reduction factor_coloring relative reduction_coloring reference link_coloring notes_coloring image_url_coloring
0 well-fitted cloth mask 2.00 50% https://docs.google.com/spreadsheets/d/16K1OQk... Jose Luis-Jimenez et. al claim that the range ... https://d.newsweek.com/en/full/1713771/biden-m... 0 50% https://docs.google.com/spreadsheets/d/16K1OQk... Jose Luis-Jimenez et. al claim that the range ... https://d.newsweek.com/en/full/1713771/biden-m...
1 QT3 on Max Speed, 12-inches away 2.50 60% https://smartairfilters.com/en/blog/qt3-portab... FitTestThePlanet's estimate was even lower tha... https://www.smarterhepa.com/cdn/shop/products/... 0 60% https://smartairfilters.com/en/blog/qt3-portab... FitTestThePlanet's estimate was even lower tha... https://www.smarterhepa.com/cdn/shop/products/...
2 Universal cloth masking 4.00 75% https://blogs.cdc.gov/niosh-science-blog/2023/... NIOSH researchers find that when the source us... https://goldarrowcamp.com/app/uploads/2020/05/... 0 75% https://blogs.cdc.gov/niosh-science-blog/2023/... NIOSH researchers find that when the source us... https://goldarrowcamp.com/app/uploads/2020/05/...
3 2 Corsi-Rosenthal Boxes running on max, added ... 5.00 80% https://www.sciencedirect.com/science/article/... Adding an additional 1311 CFM of clean air to ... https://encrypted-tbn0.gstatic.com/images?q=tb... 0 80% https://www.sciencedirect.com/science/article/... Adding an additional 1311 CFM of clean air to ... https://encrypted-tbn0.gstatic.com/images?q=tb...
4 N95 with leakage 10.00 90% https://docs.google.com/spreadsheets/d/16K1OQk... Jose Luis-Jimenez & Linsey Marr claim that wea... https://aidwaycares.com/cdn/shop/products/9502... 0 90% https://docs.google.com/spreadsheets/d/16K1OQk... Jose Luis-Jimenez & Linsey Marr claim that wea... https://aidwaycares.com/cdn/shop/products/9502...
5 2 Corsi-Rosenthal Boxes running on max + Unive... 16.67 94% https://www.sciencedirect.com/science/article/... Adding an additional 1300 CFM of clean air to ... https://breathesafe-airgo.s3.us-east-2.amazona... 0 94% https://www.sciencedirect.com/science/article/... Adding an additional 1300 CFM of clean air to ... https://breathesafe-airgo.s3.us-east-2.amazona...
6 LaminAir, 12-inches away 36.00 97.3% NaN https://breathesafe-airgo.s3.us-east-2.amazona... 36 97.3% NaN https://breathesafe-airgo.s3.us-east-2.amazona...
In [40]:
results = bar_plot_exposure_reduction_factors(
    fit_factor_context.sort_values(by='exposure reduction factor'), 
    img_func=lambda fit_factor, i: [ i / 9 + 1.2 / 9.5 , fit_factor / 150, 0.08,0.4], 
    figsize=(15,8),
    ylim=(0, 110)
)

results['ax'].axhline(20, color='r')
results['ax'].axhline(100, color='g')
results['ax'].legend([ 'N95 without leaks, lower bound', 'N95 without leaks, typical', 'exposure reduction factor'], loc='upper center')

#merge.sort_values(by='exposure reduction factor_coloring').plot.bar(ax =results['ax'], color='g')
Out[40]:
<matplotlib.legend.Legend at 0x132a69030>
No description has been provided for this image

Anemometer Grid Measurements to Compute Clean Air Delivery Rate¶

Since the LaminAir uses a HEPA, the amount of clean air being put out is essentially the clean air delivery rate. So we just need to compute how much air is being put out by the device (measured in volume of air per time). Measurements of velocity (m/s) were taken using a Kestrel Anemometer.

In [41]:
anemometer_grid_measurements = pd.DataFrame([
  [0.7, 0.7, 0.6, 0.8, 0.6, 0.8, 0.8],
  [0.8, 0.4, 0.6, 0.8, 0.6, 0.8, 0.8],
  [0.9, 0.5, 0.7, 0.7, 0.7, 0.7, 0.8],
  [1, 0.8, 0.8, 0.8, 0.7, 0.8, 0.8],
  [1.1, 0.8, 0.6, 0.7, 0.7, 0.8, 0.7],
  [1.1, 0.7, 0.7, 0.7, 0.6, 0.5, 0.6],
  [0.9, 0.6, 0.8, 0.7, 0.6, 0.5, 0.7]
])

compute_cadr_via_anemometer_grid(velocity_grid_mps=anemometer_grid_measurements, filter_width_mm=240, filter_length_mm=240)
Out[41]:
{'cubic_meters_per_hour': 150.65338775510202,
 'cubic_feet_per_min': 88.67123636372374}