Activity Example#

This notebook demonstrates the Binary Activity Theory (BAT) model application, crucial for calculating the activity of water and organic compounds in mixtures and understanding phase separation. This model, as detailed in Gorkowski, K., Preston, T. C., & Zuend, A. (2019), provides critical insights into aerosol particle behavior, essential in environmental and climate change research.

Reference: Gorkowski, K., Preston, T. C., & Zuend, A. (2019). Relative-humidity-dependent organic aerosol thermodynamics Via an efficient reduced-complexity model. Atmospheric Chemistry and Physics https://doi.org/10.5194/acp-19-13383-2019

import numpy as np  # For numerical operations
import matplotlib.pyplot as plt  # For plotting graphs
# Specific functions from the particula package for activity calculations
from particula.activity import binary_activity, phase_separation, species_density

Activity Calculation#

Define the parameters required by the activity module to calculate the activity of water and organic compounds in a mixture, as well as phase separation. These parameters include organic mole fraction, density, molecular weight ratio [water/organic], and the density of the organic compound. Using these parameters helps in accurately modeling the behavior of aerosol particles in various environmental conditions.

# Define a range of organic mole fractions for the calculation
organic_mole_fraction = np.linspace(0.001, 1, 1000)

# Define other necessary parameters
oxygen2carbon = 0.225  # Oxygen to carbon ratio
molar_mass_ratio = 18.016 / 100  # Water to organic molecular weight ratio
density = species_density.organic_density_estimate(
    18.016 / molar_mass_ratio,
    oxygen2carbon)  # Estimate of organic compound density

# Calculate activity coefficients using the binary_activity function
activity_water, activity_organic, mass_water, mass_organic, gamma_water, gamma_organic = \
    binary_activity.activity_coefficients(
        molar_mass_ratio,
        organic_mole_fraction,
        oxygen2carbon,
        density,
        functional_group=None)

Plotting the Activity and Phase Separation#

Here we plot the activity of water and the organic compound as a function of the organic mole fraction. Visualizing these activities helps in identifying phase separation or miscibility gaps, crucial for understanding the behavior of aerosols under different environmental conditions. Phase separation is indicated by activities greater than 1.0 or non-monotonic behavior in the activity curve, as shown below.

fig, ax = plt.subplots()
ax.plot(
    1 - organic_mole_fraction,
    activity_water,
    label="water",
    linestyle='dashed'
)
ax.plot(
    1 - organic_mole_fraction,
    activity_organic,
    label="organic",
)
ax.set_ylim()
ax.set_xlabel("water mole fraction")
ax.set_ylabel("activity")
ax.legend()
plt.show()

fig, ax = plt.subplots()
ax.plot(
    1 - organic_mole_fraction,
    gamma_water,
    label="water",
    linestyle='dashed'
)
ax.plot(
    1 - organic_mole_fraction,
    gamma_organic,
    label="organic",
)
ax.set_ylim()
ax.set_xlabel("water mole fraction")
ax.set_ylabel("activity coefficient")
ax.legend()
plt.show()
../../_images/242168dd06b23a2f9ccf93ac01e1ffd607cd8c30e2bb369d5ceeb0739b9b6682.png ../../_images/aab0851a0ebd9653238f9ee9311337fe490f9c27224e1c636dc50696349a10c3.png

\( q^\alpha \)#

The \(q^\alpha\) parameter signifies the transition from an organic-rich phase to a water-rich phase. This transition is crucial for understanding the phase behavior of aerosol particles. It can be calculated using the particula.activity.phase_separation function. The plot below illustrates \(q^\alpha\) based on the activity calculations performed earlier.

# Finding phase separation points and calculating q_alpha
phase_sep_aw = phase_separation.find_phase_separation(
    activity_water, activity_organic)
q_alpha = phase_separation.q_alpha(
    seperation_activity=phase_sep_aw['upper_seperation'],
    activities=activity_water)

# Plotting q_alpha
fig, ax = plt.subplots()
plt.plot(activity_water, q_alpha)
plt.xlabel('Water Activity')
plt.ylabel('$q^{\\alpha}$ [Organic Rich to Water Rich]')
plt.show()
../../_images/faeb800834c0fd3f01dbc394e4623cf436653cba064595fdfa904bcf90b51d11.png

Water Activity Focus#

In atmospheric aerosol modeling, water activity is often a more critical parameter than mole fraction. This is because water activity is typically a controllable or known variable in atmospheric conditions, unlike the exact mole fractions in a solution. To correlate water activity with the mole fraction required to achieve it, we utilize functions from the particula.activity module.

# select the water activity desired
water_activity_desired = np.linspace(0.5, 1, 100)
oxygen2carbon = 0.25

# calculate the mass fraction of water in the alpha and beta phases
# for each water activity
alpha, beta, q_alpha = binary_activity.fixed_water_activity(
        water_activity=water_activity_desired,
        molar_mass_ratio=molar_mass_ratio,
        oxygen2carbon=oxygen2carbon,
        density=density
        )

# plot the results vs water activity
fig, ax = plt.subplots()
ax.plot(
    water_activity_desired,
    alpha[2],
    label="alpha phase mass fraction water",
)
ax.plot(
        water_activity_desired,
        q_alpha,
        label="q_alpha",
        )
if beta is not None:
    ax.plot(
        water_activity_desired,
        beta[2],
        label="beta phase mass fraction water",
    )
ax.set_ylim()
ax.set_xlabel("water activity (Relative Humidity/100)")
ax.set_ylabel("mass fraction of water")
plt.legend()
plt.show()
../../_images/8a3faf81b4602e9eedf13f1b0c6a1cf87f74a16581335c6457a656c31dc83225.png

Higher Oxygen to Carbon Ratios#

Higher oxygen to carbon ratios in the mixture tend to inhibit phase separation. The following analysis demonstrates this effect. This observation is crucial in predicting the behavior of aerosol particles under varying chemical compositions (more or less ‘aged’).

# select the water activity desired
water_activity_desired = np.linspace(0.5, 1, 100)
# select the oxygen to carbon ratio
oxygen2carbon = 0.6

# calculate the mass fraction of water in the alpha and beta phases
# for each water activity
alpha, beta, q_alpha = binary_activity.fixed_water_activity(
    water_activity=water_activity_desired,
    molar_mass_ratio=molar_mass_ratio,
    oxygen2carbon=oxygen2carbon,
    density=density
)

# plot the results vs water activity
fig, ax = plt.subplots()
ax.plot(
    water_activity_desired,
    alpha[2],
    label="alpha phase mass fraction water",
)
ax.plot(
    water_activity_desired,
    q_alpha,
    label="q_alpha",
)
if beta is not None:
    ax.plot(
        water_activity_desired,
        beta[2],
        label="beta phase mass fraction water",
    )
ax.set_ylim()
ax.set_xlabel("water activity (Relative Humidity/100)")
ax.set_ylabel("mass fraction of water")
plt.legend()
plt.show()
../../_images/eeb4fd2dced211470e3c5e69d56c10603bbf91f25c26b4d1c83838b85acc92a9.png

Summary#

This notebook demonstrates how to use the activity module for calculating the activity of water and organic compounds in a mixture and assessing phase separation. The insights gained are vital for applications in aerosol thermodynamics, cloud condensation nuclei, and cloud microphysics.

This is an implementation of the Binary Activity Theory (BAT) model developed in Gorkowski, K., Preston, T. C., & Zuend, A. (2019).

Further Documentation#

For more in-depth understanding and additional functionalities, refer to the documentation, or call the help function on any of the functions in the particula.activity module.

help(binary_activity)
Help on module particula.activity.binary_activity in particula.activity:

NAME
    particula.activity.binary_activity - Binary activity coefficient model for organic-water mixtures.

DESCRIPTION
    Gorkowski, K., Preston, T. C., & Zuend, A. (2019).
    Relative-humidity-dependent organic aerosol thermodynamics
    Via an efficient reduced-complexity model.
    Atmospheric Chemistry and Physics
    https://doi.org/10.5194/acp-19-13383-2019

FUNCTIONS
    activity_coefficients(molar_mass_ratio: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], organic_mole_fraction: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], oxygen2carbon: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], density: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], functional_group=None) -> Tuple[numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray, numpy.ndarray]
        Calculate the activity coefficients for water and organic matter in
        organic-water mixtures.
        
        Args:
            - molar_mass_ratio: Ratio of the molecular weight of water to the
                molecular weight of organic matter.
            - organic_mole_fraction: Molar fraction of organic matter in the
                mixture.
            - oxygen2carbon: Oxygen to carbon ratio in the organic compound.
            - density: Density of the mixture.
            - functional_group: Optional functional group(s) of the organic
                compound, if applicable.
        
        Returns:
            A tuple containing the activity of water, activity
            of organic matter, mass fraction of water, and mass
            fraction of organic matter, gamma_water (activity coefficient),
            and gamma_organic (activity coefficient).
    
    bat_blending_weights(molar_mass_ratio: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], oxygen2carbon: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]]) -> numpy.ndarray
        Function to estimate the blending weights for the BAT model.
        
        Args:
            - molar_mass_ratio: The molar mass ratio of water to organic
                matter.
            - oxygen2carbon: The oxygen to carbon ratio.
        
        Returns:
            - blending_weights : List of blending weights for the BAT model
            in the low, mid, and high oxygen2carbon regions.
    
    biphasic_water_activity_point(oxygen2carbon: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], hydrogen2carbon: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], molar_mass_ratio: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], functional_group: Union[list[str], str, NoneType] = None) -> numpy.ndarray
        This function computes the biphasic to single phase
        water activity (RH*100).
        
        Args:
            - oxygen2carbon: The oxygen to carbon ratio.
            - hydrogen2carbon: The hydrogen to carbon ratio.
            - molar_mass_ratio: The molar mass ratio of water to organic
                matter.
            - functional_group: Optional functional group(s) of the organic
                compound, if applicable.
        
        Returns:
            - np.array: The RH cross point array.
    
    coefficients_c(molar_mass_ratio: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], oxygen2carbon: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], fit_values: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]]) -> numpy.ndarray
        Coefficients for activity model, see Gorkowski (2019). equation S1 S2.
        
        Args:
            - molar_mass_ratio: The molar mass ratio of water to organic
                matter.
            - oxygen2carbon: The oxygen to carbon ratio.
            - fit_values: The fit values for the activity model.
        
        Returns:
            - np.ndarray: The coefficients for the activity model.
    
    convert_to_oh_equivalent(oxygen2carbon: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], molar_mass_ratio: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], functional_group: Union[list[str], str, NoneType] = None) -> Tuple[numpy.ndarray, numpy.ndarray]
        just a pass through now, but will
        add the oh equivalent conversion
    
    fixed_water_activity(water_activity: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], molar_mass_ratio: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], oxygen2carbon: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], density: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]]) -> Tuple
        Calculate the activity coefficients of water and organic matter in
        organic-water mixtures.
        
        This function assumes a fixed water activity value (e.g., RH = 75%
        corresponds to 0.75 water activity in equilibrium).
        It calculates the activity coefficients for different phases and
        determines phase separations if they occur.
        
        Parameters:
        water_activity (ArrayLike): An array of water activity values.
        molar_mass_ratio (ArrayLike): Array of molar mass ratios of the components.
        oxygen2carbon (ArrayLike): Array of oxygen-to-carbon ratios.
        density (ArrayLike): Array of densities of the mixture.
        
        Returns:
        Tuple: A tuple containing the activity coefficients for alpha and beta
                phases, and the alpha phase mole fraction.
               If no phase separation occurs, the beta phase values are None.
    
    gibbs_mix_weight(molar_mass_ratio: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], organic_mole_fraction: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], oxygen2carbon: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], density: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], functional_group: Optional[str] = None) -> Tuple[numpy.ndarray, numpy.ndarray]
        Gibbs free energy of mixing, see Gorkowski (2019), with weighted
        oxygen2carbon regions. Only can run one compound at a time.
        
        Args:
            - molar_mass_ratio: The molar mass ratio of water to organic
                matter.
            - organic_mole_fraction: The fraction of organic matter.
            - oxygen2carbon: The oxygen to carbon ratio.
            - density: The density of the mixture.
            - functional_group: Optional functional group(s) of the organic
                compound, if applicable.
        
        Returns:
            - gibbs_mix : Gibbs energy of mixing (including 1/RT)
            - derivative_gibbs : derivative of Gibbs energy with respect to
            - mole fraction of organics (includes 1/RT)
    
    gibbs_of_mixing(molar_mass_ratio: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], organic_mole_fraction: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], oxygen2carbon: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], density: Union[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]], numpy._typing._nested_sequence._NestedSequence[numpy._typing._array_like._SupportsArray[numpy.dtype[Any]]], bool, int, float, complex, str, bytes, numpy._typing._nested_sequence._NestedSequence[Union[bool, int, float, complex, str, bytes]]], fit_dict: dict) -> Tuple[numpy.ndarray, numpy.ndarray]
        Calculate the Gibbs free energy of mixing for a binary mixture.
        
        Args:
            - molar_mass_ratio: The molar mass ratio of water to organic
                matter.
            - organic_mole_fraction: The fraction of organic matter.
            - oxygen2carbon: The oxygen to carbon ratio.
            - density: The density of the mixture.
            - fit_dict: A dictionary of fit values for the low oxygen2carbon region
        
        Returns:
            Tuple[np.ndarray, np.ndarray]: A tuple containing the Gibbs free
            energy of mixing and its derivative.

DATA
    ArrayLike = typing.Union[numpy._typing._array_like._Supports...ng.Unio...
    FIT_HIGH = {'a1': [5.92155, -2.528295, -3.883017, -7.898128], 'a2': [-...
    FIT_LOW = {'a1': [7.089476, -7.71186, -38.85941, -100.0], 'a2': [-0.62...
    FIT_MID = {'a1': [5.872214, -4.535007, -5.129327, -28.09232], 'a2': [-...
    INTERPOLATE_WATER_FIT = 500
    LOWEST_ORGANIC_MOLE_FRACTION = 1e-12
    Optional = typing.Optional
        Optional[X] is equivalent to Union[X, None].
    
    Tuple = typing.Tuple
        Deprecated alias to builtins.tuple.
        
        Tuple[X, Y] is the cross-product type of X and Y.
        
        Example: Tuple[T1, T2] is a tuple of two elements corresponding
        to type variables T1 and T2.  Tuple[int, float, str] is a tuple
        of an int, a float and a string.
        
        To specify a variable-length tuple of homogeneous type, use Tuple[T, ...].
    
    Union = typing.Union
        Union type; Union[X, Y] means either X or Y.
        
        On Python 3.10 and higher, the | operator
        can also be used to denote unions;
        X | Y means the same thing to the type checker as Union[X, Y].
        
        To define a union, use e.g. Union[int, str]. Details:
        - The arguments must be types and there must be at least one.
        - None as an argument is a special case and is replaced by
          type(None).
        - Unions of unions are flattened, e.g.::
        
            assert Union[Union[int, str], float] == Union[int, str, float]
        
        - Unions of a single argument vanish, e.g.::
        
            assert Union[int] == int  # The constructor actually returns int
        
        - Redundant arguments are skipped, e.g.::
        
            assert Union[int, str, int] == Union[int, str]
        
        - When comparing unions, the argument order is ignored, e.g.::
        
            assert Union[int, str] == Union[str, int]
        
        - You cannot subclass or instantiate a union.
        - You can use Optional[X] as a shorthand for Union[X, None].

FILE
    /opt/hostedtoolcache/Python/3.11.9/x64/lib/python3.11/site-packages/particula/activity/binary_activity.py