Particula tour#

We have designed particula around object-oriented programming principles where physics entities inherit from each other.

  1. It all starts with an Environment object (class) where temperature, pressure, and other derived properties are defined. For now, the two main derived properties are the dynamic viscosity and mean free path.

  2. Then, the Vapor object inherits from Environment and adds its own properties, mainly vapor properties like radius and density, but also derived properties like the driving force of condensation.

  3. The most involved object Particle builds on Vapor (and thus Environment). It is split into steps to isolate different components (e.g. making a distribution, making particle instances, calculating condensation, and calculating coagulation are all split into different objects that build on each other). In Particle, the idea is to form a fully equipped particle distribution, whose properties are readily available and calculated.

  4. Up to Particle, there is no sense of a time dimension. To add one, we create the Rates object who takes as input the Particle object, thus explicitly allowing dynamics-specific calculations, or “rates”.

  5. Finally, a dynamic Solver object builds on Rates and propagates the Particle object in time.

Setup#

Let’s first get the needed packages and set up our computational environment.

try:
    import particula, matplotlib
except ImportError:
    print("Setting up computational environment...")
    %pip install -U particula -qqq
    %pip install matplotlib -qqq

from particula.particle import Particle
from particula.rates import Rates
from matplotlib import pyplot as plt
import numpy as np

Defining a distribution#

Defining a distribution in particula is easy! Simply import the Particle class and pass some keyword arguments to it. Here we interested in a bimodal particle distribution, with different modes and different geometric standard deviations (gsigma) and different amplitude (particle_number). While at it, we might as well calculate rates and disabling the lazy execution so that we do not recalculate the rates at every call.

deets = {
    "mode": [10e-9, 70e-9],
    "gsigma": [1.6, 2.0],
    "nbins": 4000,
    "particle_number": [17/20, 3/20],
}
part_dist = Particle(**deets)
r = Rates(particle=part_dist, lazy=False)

We define a simple plot utility to plot some of the properties of this distribution.

def plot_some(x, y, grid="semilogx", title=None, label=None):
    """ plot y vs x with grid, providing title and label """
    if grid == "loglog":
        plt.loglog(x.m, y.m, label=label)
    else:
        plt.semilogx(x.m, y.m, label=label);
    if label is not None:
        plt.legend()
    plt.title(title)
    plt.xlabel(f"{x.u}"); plt.ylabel(f"{y.u}");

Particle number concentration density#

One way to think about this is to integrate the curve, that yields exactly 1e5 /cc which is the total number concentration. Try it! (Copy the code block below and execute it after or before the next cell.)

np.trapz(part_dist.particle_distribution(), part_dist.particle_radius)
plot_some(
    x=part_dist.particle_radius,
    y=part_dist.particle_distribution(),
    title="Particle distribution density",
)
../_images/c6cfc2d79c2bbd73fc1a4a4005cd669352597994a602c3132baa86e67f367e05.png

The coagulation kernel#

plot_some(
    x=part_dist.particle_radius,
    y=part_dist.coagulation(),
    title="Particle coagulation kernel",
    grid="loglog"
)
../_images/20ee11b9d31b7c139c6220a9ea768c0c52f7dba7ba2ba3b8997be7c2a0ddd2df.png

The coagulation loss rate#

plot_some(
    x=part_dist.particle_radius,
    y=-r.coagulation_loss(),
    title="Particle coagulation loss rate",
)
../_images/45dfa1d5f8086b9539c034eeed3f035c3809fce092c451e01558cd5bc8fbf8ea.png

The coagulation gain rate#

plot_some(
    x=part_dist.particle_radius,
    y=r.coagulation_gain(),
    title="Particle coagulation gain rate",
)
../_images/b4190ca6c318fb13aec641bde1f2f721ce1239f5360ddae189feec96ee93c83d.png

The coagulation net rate#

plot_some(
    x=part_dist.particle_radius,
    y=r.coagulation_rate(),
    title="Particle coagulation net rate",
)
../_images/2f712c4dba5c9395578edc82f51bd927599036dda84c89c349360d0025368853.png

The condensation growth speed#

plot_some(
    x=part_dist.particle_radius,
    y=r.condensation_growth_speed(),
    title="Particle condensation growth speed",
)
../_images/886b190d81095cfce4f5434fe58250ea907b56fb7b6a226317004d9e683c442d.png

The condensation growth rate#

plot_some(
    x=part_dist.particle_radius,
    y=r.condensation_growth_rate(),
    title="Particle condensation growth rate",
)
../_images/9ad39435930df019e5bc1197ce0575eb0cd002eb0c2865269a410880c225b2f4.png

The condensation growth rate (volume-wise)#

plot_some(
    x=part_dist.particle_radius,
    y=r.condensation_growth_rate()*part_dist.particle_radius**3,
    title="Particle condensation growth rate (volume)",
)
../_images/eab312e436d93700cc21990af5cc7c13cd6d6a937a008d8ac0e5e7fafc1f1ce9.png

The coagulation net rate (volume-wise)#

Note the integration yields a small number, meaning the mass is conserved!

plot_some(
    x=part_dist.particle_radius,
    y=r.coagulation_rate()*part_dist.particle_radius**3,
    title="Particle coagulation net rate (volume)",
    label=f"Integral: {np.trapz(r.coagulation_rate()*part_dist.particle_radius**3, part_dist.particle_radius)}"
)
../_images/2ce671d75e4d0fe0f8569383fa82dc6f8d98d12243ca16f1ddd27ab4d0f64197.png

A solver#

If you are adventurous, you can even propagate the define distribution in time! We use a rather simple solver here, but it still takes time as it has to recalculate all the rates at every time step. We reduce the nbins to make it slightly faster, but it may not even run on a super tiny (free) machine.

from particula.dynamics import Solver
deets = {
    "mode": [10e-9, 70e-9],
    "gsigma": [1.6, 2.0],
    "nbins": 2000,
    "particle_number": [17/20, 3/20],
}
part_dist = Particle(**deets)
r = Rates(particle=part_dist, lazy=True)
time_span = [0, 60]
s = Solver(particle=part_dist, time_span=time_span)
sols = s.solution()
plt.semilogx(part_dist.particle_radius.m, sols[0].m, label=f"Initial at {time_span[0]} seconds")
plt.semilogx(part_dist.particle_radius.m, sols[-1].m, label=f"Final after {time_span[-1]} seconds")
plt.xlabel(f"{part_dist.particle_radius.u}")
plt.ylabel(f"{sols[0].u}")
plt.title("The evolution of the size distribution")
plt.legend();
../_images/31ed590a769a6292cde34e0e5dcf56ec7d44760d10fdad6b5874eaf95578a8ae.png
plt.semilogx(
    part_dist.particle_radius.m,
    sols[0].m*part_dist.particle_radius.m**3,
    label=f"Initial at {time_span[0]} seconds"
)
plt.semilogx(
    part_dist.particle_radius.m,
    sols[-1].m*part_dist.particle_radius.m**3,
    label=f"Final after {time_span[-1]} seconds"
)
plt.legend()
plt.xlabel(f"{part_dist.particle_radius.u}")
plt.ylabel(f"{sols[0].u*part_dist.particle_radius.u**3}")
plt.title("The evolution of the size distribution (volume)")
plt.xlim([1e-7, 5e-7]); plt.ylim([35e-5, 37e-5]);
../_images/c31393086fff809bbe77c71a207ffb75e8d06f4c9946bfcba5c00616742946d8.png