At this point you are already an expert on box model! We will now spice things up.
Ozone plays a key role in Earth's atmosphere. The ozone layer in the stratosphere filters out the harmful UV radiation from the sunlight. Ozone is also a major source of HOx radicals (OH+HO2) which largely controlls the self-cleaning capacity of the atmosphere. At the ground-level, ozone is also a pollutant that can cause a bunch of health issues.
In the troposphere, ozone is produced from photochemistry involving volatile organic compounds (VOCs) and NOx, and both VOCs and NOx are emitted from a variety of natural and anthropogenic sources. In urban areas, to effectively control ambient ozone, we need to reduce VOCs and NOx. However, in different places, the sources of these pollutants and hence the ozone formation may be quite different. What is the most efficient way to reduce ambient ozone levels?
The concept of ozone isopleth, or EKMA plot, is a useful way to decide what may be the more efficient ozone control strategy. The idea is to run the model with different conditions (VOCs and NOx) to see what's the maximum ozone formation potential looks like. Therefore you have to set up the model, and run the model many, many, times.
MCM contains near-explicit oxidation pathways of tens of major VOC species (tens of thousands of reactions perhaps), and all of them contribute to O3 formation in the atmosphere. Running the full MCM mechanism is pretty expensive.
To capture most of O3 formation, you probably don't need to load the full MCM. I would mark C1-C5 alkanes, a couple of alkenes and alkyne, benzene, toluene, xylenes, isoprene, and a-/b-pinenes. Still, a mechanism like this would have more than 10k reactions and hence it will run slow, and in this example, it would be awful as we have to run the model many times.
For the sake of simplicity, in this Chapter we'll use a small subset of MCM, the KPP file generated from the previous Chapter, which contains C1-C4 alkanes (link).
Grab the all-in-one Jupyter Notebook (YAMCHA_tutorial_ch6) from GitHub. Dump the KPP file in Colab. And you know how to load KPP file in the model!
There are many ways to do the ozone isopleth. In this example we run it in a very simple and crude way.
Firstly, we'll setup one simulation, which is initialized with 1 ppb NO, and 10 ppb of VOCs (6 ppb C2H6, 2 ppb C3H8, 1 ppb of i-C4H10 and n-C4H10 each). Then run the model for 2 days. All other conditions are held constant over the course of the simulation: T = 298 K, SZA = 0 deg, RH = 60. We'll call it "Base Model".
Then we'll scale the NOs and VOCs in this simulation up and down, using scaling factors. The scaling factor for NOx ranges from 0.1 to 100, i.e. NOx ranges from 0.1 ppb to 100 ppb in all the simulations. The scaling factor for VOCs ranges from 0.1 to 10, since the "Base Model" has 10 ppb VOCs already, so together with the scaling factors, VOCs in all the simulations ranges from 1 ppb to 100 ppb.
If you want you can set up diurnal variations etc to make it a bit more realistic, be my guest ;)
Alternatively, people sometimes vary NOx and VOC emission rates instead. Well, you know how to deal with emissions ;) Feel free to give it a shot.
At this point you must be very familiar with the model already. So I'm gonna jump to the section with user inputs. You also don't need to set any time-dependent parameters because we don't need.
# --- time stamps
total_run_time_days = 2. # how long you want to run the model?
output_frequency_seconds = 7200 # how often do you save the output?
...
# --- this is the solar zenith angle for the j-value parameterization (MCM)
SZA = 0. * np.pi / 180.
# --- environmental conditions
temp = 298 # Kelvin
press = 101325. # Pa
RH = 60.
...
# --- initial condition. need to convert to molec/cm3
conc_init['O3'] = (20.) *press*(6.0232E+8)/(8.314*temp)
conc_init['NO'] = (5.) *press*(6.0232E+8)/(8.314*temp)
conc_init['NO2'] = (10.) *press*(6.0232E+8)/(8.314*temp)
conc_init['CO'] = (100.) *press*(6.0232E+8)/(8.314*temp)
conc_init['CH4'] = (1800.) *press*(6.0232E+8)/(8.314*temp)
conc_init['C2H6'] = (6.) *press*(6.0232E+8)/(8.314*temp)
conc_init['C3H8'] = (2.) *press*(6.0232E+8)/(8.314*temp)
conc_init['NC4H10'] = (1.) *press*(6.0232E+8)/(8.314*temp)
conc_init['IC4H10'] = (1.) *press*(6.0232E+8)/(8.314*temp)
Run the model. It shouldn't take long for the model to finish. Make a few plots and see what you got.
Proceed to Cell #7, which has a wrapper function that takes two inputs: in_f_VOC and in_f_NOx. These are the scaling factors for VOCs and NOx.
Inside this wrapper function, the initial concentrations of NOx and VOCs are then scaled using these scaling factors:
# --- initial condition. need to convert to molec/cm3
conc_init['NO'] = in_f_NOx*(1.) *press*(6.0232E+8)/(8.314*temp)
conc_init['CO'] = (100.) *press*(6.0232E+8)/(8.314*temp)
conc_init['CH4'] = (1800.) *press*(6.0232E+8)/(8.314*temp)
conc_init['C2H6'] = in_f_VOC*(6.) *press*(6.0232E+8)/(8.314*temp)
conc_init['C3H8'] = in_f_VOC*(2.) *press*(6.0232E+8)/(8.314*temp)
conc_init['NC4H10'] = in_f_VOC*(1.) *press*(6.0232E+8)/(8.314*temp)
conc_init['IC4H10'] = in_f_VOC*(1.) *press*(6.0232E+8)/(8.314*temp)
As a result, the initial concentration of NOx is just in_f_NOx*1 ppb, while the initial concentraion of total VOCs would be in_f_VOC*10 ppb.
Then in Cell #8, I have two nested loops, one looping over all NOx scaling factors, and the other looking over all VOC scaling factors:
EKMA_NOx_ppb,EKMA_VOC_ppb,EKMA_O3 = [], [], []
timing = []
N = 10
n = 0
for f_NOx in np.linspace(0.1,100,N):
for f_VOC in np.linspace(0.1,10,N):
start_time = time.time()
conc_temp = wrapper_EKMA(f_VOC,f_NOx)
timing.append(time.time() - start_time)
O3_temp = list(conc_temp['O3'])[-1]/(press*(6.0232E+8)/(8.314*temp))
EKMA_O3.append(O3_temp)
EKMA_NOx_ppb.append(f_NOx)
EKMA_VOC_ppb.append(f_VOC*10.)
print('%d/%d, NOx=%.2f ppb, VOC=%.2fppb, max O3=%.2fppb' % (n+1,N**2,f_NOx,f_VOC*10.,O3_temp))
del conc_temp
n+=1
You can see here that the wrapper function is called 100 times, i.e. we'll do 100 simulations, which will take some time! Sit back and relax.
Once it's done, you can make the contour plot. I'm attaching that plot on the right.
Look at the top left corner: in this region, the more NOx you have, the less O3 you got. This is because VOC levels are too low to make any new O3, but the large amount of fresh NO would quickly consume O3 to make NO2. This is often referred to as "NO titration" regime. In this regime, the sum of O3 and NO2 wouldn't change much but O3 would decrease with increasing NO.
Still in the top left side but below the NO titration regime, where you have quite some NOx but low VOC. In this regime O3 formation is sensitive to VOCs but less so for NOx. This is call "VOC sensitive regime".
The bottom right corner is interesting, where VOCs are high but NOx is low. In this regime O3 formation is very sensitive to NOx levels but less sensitive to VOC levels. This is often called NOx sensitive regime.
With the O3 isopleth, or EKMA plot, we can decide what the most efficient way of ozone control is!
If you're in the NOx sensitive regime, you have too much VOCs. Reducing VOCs will barely have any effect on O3, therefore the the most efficient O3 control strategy is to reduce NOx.
On the contrary, if you're in the VOC sensitive regime, you already have too much NOx. The most efficient O3 control strategy is to reduce VOCs.
In the NO titration regime, reducing NOx would actually lead to a temporary increase in O3! Similarly, increasing NOx would lead to a decrease in O3. If this is the case, do you think it would be a good idea to release more NOx in the atmosphere to reduce O3? Nope! Because NOx is a pollutant with negative health impact as well!
In this chapter we demonstrated that sometimes it can be quite useful to run the model many times in a systematic manner. We also learnt about ozone isopleth and different ozone formation sensitivity regimes. If you have your own ambient measurements, especially VOCs, you may make the ozone isopleth based on your condition!
Again there are multiple ways to construct the ozone isopleth. For example, you may vary emissions (instead of initial concentrations like we did in this example); you may also set diurnal variations; in this example we perturb anthropogenic non-methane hydrocarbons, but in reality the perturbation may occur differently, i.e. perhaps CH4, OVOCs, CO would vary as well.