NPF Analysis Tutorial

Introduction

Our aim here is to calculate the growth rate (GR) and formation rate (J) of particles in the size range 7-20 nm from aerosol number size distribution data.

This tutorial roughly follows: Kulmala, M. et al. Measurement of the nucleation of atmospheric aerosol particles. Nat. Protocols 7, 1651–1667 (2012).

Find representative diameters by mode fitting

  1. Run the bokeh app aerosol-analyzer from the terminal

  2. Load the particle number size distribution data (data.csv)

  • The number size distribution should be a csv file with the following format:

    • First column: time for example in the format “YYYY-mm-dd HH:MM:SS”

    • Header: particle diameters in meters

    • Rest of the columns: particle number size distribution (dN/dlogDp) in cm-3

  1. Draw a region of interest (ROI) around the growing particle mode

  2. Fit representative diameters using the mode fit method (or maxconc method)

  3. Save the ROI as a JSON file (npf_roi.json)

Figure 1: The number size distribution as a heat map with ROI drawn around the growing particle mode along with representative diameters/times found using mode fitting.

Calculate GR

Next we write a script that does the following:

  1. Load the JSON file

  2. Extract the representative diameters/times and convert them to nanometers/hours.

  3. Only select the representative diameters in the size range and fit a line through them

  4. GR is calculated as the slope of the line fit in nm/h

import json
import pandas as pd
import numpy as np
import aerosol.functions as af

# Read a JSON file from disk
with open('npf_roi.json', 'r') as f:
    data = json.load(f)

# Now `data` is a Python dict or list depending on the file
time = []
diam = []
for i in range(len(data[0]["fit_mode_params"])):
    time.append(
        float(data[0]["fit_mode_params"][i]["time"])/(1000 * 60 * 60)) # hour
    diam.append(
        data[0]["fit_mode_params"][i]["diam"]*1e9) # nm

# Convert to numpy array
time = np.array(time)
diam = np.array(diam)

# Calculate the GR
idx1 = np.argwhere(((diam>7.) & (diam<20.))).flatten()
gr = np.polyfit(time[idx1],diam[idx1],1)[0] # nm/h

print(f'{gr:.2f} nm h-1')

# OUTPUT: 5.73 nm h-1

Calculate the formation rate

The formation rate can be approximated by

\[ J_{d_{min}-d_{max}} \approx \frac{n_{d_{min}-d_{max}}}{\Delta t} + \sum_{d_i=d_{min}}^{d_{max}} CoagS_{d_i} n_{d_i} + \frac{GR} {d_{max}-d_{min}} n_{d_{min}-d_{max}} \]

The terms on the right-hand-side from left to right are the concentration term, the sink term and the growth term.

Calculating the sink term

# Load the number size distribution data
dndlogdp = pd.read_csv("data.csv",parse_dates=True,index_col=0)

# Convert from normalized concentrations to concentrations
dn = af.dndlogdp2dn(dndlogdp)

# Calculate the total sink term in the size range
diams = dndlogdp.columns.astype(float).values
idx2 = np.argwhere((diams>7e-9) & (diams<20e-9)).flatten()

sink_terms = []
for i in idx2:
    sink_term = (af.calc_coags(
        dndlogdp, 
        diams[i]).values.flatten() * 
    dn.iloc[:,i].values.flatten())
    sink_terms.append(sink_term)

total_sink = pd.DataFrame(
    index = dndlogdp.index, 
    data = {0: np.sum(sink_terms, axis=0)})

Converting GR to dataframe

The GR is calculated only during the NPF event, otherwise it is assigned NaN.

# Convert the GR to Dataframe
gr_df = pd.DataFrame(
    index = pd.to_datetime(time[idx1], unit="h"), data = {0: gr})

# Reindex to the number size distribution data
gr_df = gr_df.reindex(dndlogdp.index)

Calculating the formation rate

# Calculate the formation rate
J = af.calc_formation_rate(
    dndlogdp,
    7e-9,
    20e-9,
    sink_term=total_sink,
    gr=gr_df)

# Calculate and print the mean formation rate
print(f'{np.mean(J["J"]):.2f} cm-3 s-1')

# OUTPUT: 0.48 cm-3 s-1