# 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
4) Draw a region of interest (ROI) around the growing particle mode
5) Fit representative diameters using the mode fit method (or maxconc method)
6) Save the ROI as a JSON file (`npf_roi.json`)

![](aerosol_dist.png)
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

```python
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

```python
# 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`.

```python
# 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

```python
# 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
```








