EN | JP | Last sync: 2026-01-21

Chapter 1: Fundamentals of Nanoparticle Agglomeration

Understanding the Driving Forces Behind Particle Clustering

Reading Time: 30-35 minutes Difficulty: Intermediate Code Examples: 6

Learning Objectives

1.1 Nanoparticle Characteristics and Surface Effects

Nanoparticles (typically 1-100 nm) exhibit unique properties due to their high surface-to-volume ratio. As particle size decreases, an increasing fraction of atoms reside at the surface, fundamentally changing the material's behavior.

Why Nanoparticles Agglomerate

The high surface energy of nanoparticles makes them thermodynamically unstable. The system tends to minimize surface energy by reducing total surface area through agglomeration. For a 10 nm particle, approximately 30% of atoms are at the surface!

Example 1: Surface Atom Ratio Calculation

We calculate and visualize how the fraction of surface atoms changes with particle size.

import numpy as np
import matplotlib.pyplot as plt

# ===================================
# Example 1: Surface Atom Ratio vs Particle Size
# ===================================

def surface_atom_fraction(d_nm, atom_diameter_nm=0.3):
    """
    Calculate the fraction of atoms at the surface of a spherical nanoparticle.

    Parameters:
        d_nm: Particle diameter in nm
        atom_diameter_nm: Approximate atomic diameter (default 0.3 nm for metals)

    Returns:
        Fraction of surface atoms
    """
    # Simple shell model approximation
    r = d_nm / 2
    r_atom = atom_diameter_nm / 2

    # Number of atoms (volume-based estimate)
    V_particle = (4/3) * np.pi * r**3
    V_atom = (4/3) * np.pi * r_atom**3
    n_total = V_particle / V_atom

    # Surface atoms (shell thickness ~ one atomic diameter)
    r_inner = max(0, r - atom_diameter_nm)
    V_inner = (4/3) * np.pi * r_inner**3
    n_inner = V_inner / V_atom
    n_surface = n_total - n_inner

    return n_surface / n_total if n_total > 0 else 1.0

def specific_surface_area(d_nm, density_g_cm3):
    """
    Calculate specific surface area (m²/g) for spherical particles.

    Parameters:
        d_nm: Particle diameter in nm
        density_g_cm3: Material density in g/cm³

    Returns:
        Specific surface area in m²/g
    """
    d_m = d_nm * 1e-9
    density_kg_m3 = density_g_cm3 * 1000

    # SSA = 6 / (d * rho) for spheres
    ssa = 6 / (d_m * density_kg_m3)
    return ssa

# Calculate for range of particle sizes
diameters = np.logspace(0, 3, 100)  # 1 nm to 1000 nm
surface_fractions = [surface_atom_fraction(d) for d in diameters]

# Common materials for SSA calculation
materials = {
    'Gold (Au)': 19.3,
    'Silver (Ag)': 10.5,
    'Silica (SiO2)': 2.2,
    'Titania (TiO2)': 4.2,
    'Iron Oxide (Fe3O4)': 5.2
}

# Create figure
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Plot 1: Surface atom fraction
ax1 = axes[0]
ax1.semilogx(diameters, np.array(surface_fractions) * 100, 'b-', linewidth=2.5)
ax1.axhline(y=50, color='r', linestyle='--', alpha=0.7, label='50% surface atoms')
ax1.axvline(x=10, color='g', linestyle='--', alpha=0.7, label='10 nm')
ax1.fill_between(diameters, 0, np.array(surface_fractions) * 100, alpha=0.2)
ax1.set_xlabel('Particle Diameter (nm)', fontsize=12)
ax1.set_ylabel('Surface Atom Fraction (%)', fontsize=12)
ax1.set_title('Surface Atom Ratio vs Particle Size', fontsize=13, fontweight='bold')
ax1.set_xlim(1, 1000)
ax1.set_ylim(0, 100)
ax1.grid(True, alpha=0.3)
ax1.legend()

# Plot 2: Specific Surface Area
ax2 = axes[1]
colors = plt.cm.viridis(np.linspace(0, 0.8, len(materials)))
for (material, density), color in zip(materials.items(), colors):
    ssa = [specific_surface_area(d, density) for d in diameters]
    ax2.loglog(diameters, ssa, linewidth=2, label=material, color=color)

ax2.axhline(y=100, color='gray', linestyle=':', alpha=0.7, label='100 m²/g')
ax2.set_xlabel('Particle Diameter (nm)', fontsize=12)
ax2.set_ylabel('Specific Surface Area (m²/g)', fontsize=12)
ax2.set_title('Specific Surface Area by Material', fontsize=13, fontweight='bold')
ax2.set_xlim(1, 1000)
ax2.grid(True, alpha=0.3, which='both')
ax2.legend(loc='upper right', fontsize=9)

plt.tight_layout()
plt.savefig('surface_effects.png', dpi=150, bbox_inches='tight')
plt.show()

# Print key values
print("=== Surface Characteristics at Key Sizes ===")
for d in [5, 10, 20, 50, 100]:
    frac = surface_atom_fraction(d)
    ssa_au = specific_surface_area(d, 19.3)
    ssa_sio2 = specific_surface_area(d, 2.2)
    print(f"d = {d:3d} nm: Surface atoms = {frac*100:5.1f}%, "
          f"SSA(Au) = {ssa_au:6.1f} m²/g, SSA(SiO2) = {ssa_sio2:6.1f} m²/g")
Example Output:
=== Surface Characteristics at Key Sizes ===
d = 5 nm: Surface atoms = 78.4%, SSA(Au) = 62.2 m²/g, SSA(SiO2) = 545.5 m²/g
d = 10 nm: Surface atoms = 48.8%, SSA(Au) = 31.1 m²/g, SSA(SiO2) = 272.7 m²/g
d = 20 nm: Surface atoms = 27.1%, SSA(Au) = 15.5 m²/g, SSA(SiO2) = 136.4 m²/g
d = 50 nm: Surface atoms = 11.5%, SSA(Au) = 6.2 m²/g, SSA(SiO2) = 54.5 m²/g
d = 100 nm: Surface atoms = 5.9%, SSA(Au) = 3.1 m²/g, SSA(SiO2) = 27.3 m²/g

Practical Implications

At 10 nm, nearly half the atoms are at the surface! This explains why nanoparticle catalysts are so active (high surface area) but also why they tend to agglomerate (high surface energy drives minimization of surface area).

1.2 Van der Waals Force-Driven Agglomeration

Van der Waals (vdW) forces are the primary attractive forces between nanoparticles. These arise from fluctuating dipoles (London dispersion) and are always attractive. The Hamaker constant \(A_H\) characterizes the strength of vdW interactions for a given material pair.

Hamaker Constant

The Hamaker constant \(A_H\) typically ranges from \(10^{-21}\) to \(10^{-19}\) J:

  • Metals: 30-50 × 10⁻²⁰ J (strong attraction)
  • Oxides: 5-15 × 10⁻²⁰ J (moderate)
  • Polymers: 3-10 × 10⁻²⁰ J (weaker)

Van der Waals Interaction Energy

For two spherical particles of radii \(R_1\) and \(R_2\) separated by surface distance \(h\):

Equal spheres (R₁ = R₂ = R), small separation (h << R):

$$V_{vdW} = -\frac{A_H R}{12 h}$$

General expression:

$$V_{vdW} = -\frac{A_H}{6} \left[ \frac{2R_1 R_2}{h(h + 2R_1 + 2R_2)} + \frac{2R_1 R_2}{(h + 2R_1)(h + 2R_2)} + \ln\frac{h(h + 2R_1 + 2R_2)}{(h + 2R_1)(h + 2R_2)} \right]$$

Example 2: Van der Waals Attraction Between Nanoparticles

import numpy as np
import matplotlib.pyplot as plt

# ===================================
# Example 2: Van der Waals Attraction Between Spherical Nanoparticles
# ===================================

def vdw_energy_spheres(h, R1, R2, A_H):
    """
    Calculate van der Waals interaction energy between two spheres.

    Parameters:
        h: Surface-to-surface separation (m)
        R1, R2: Radii of the two spheres (m)
        A_H: Hamaker constant (J)

    Returns:
        Interaction energy (J)
    """
    # Avoid division by zero
    h = np.maximum(h, 1e-12)

    term1 = (2 * R1 * R2) / (h * (h + 2*R1 + 2*R2))
    term2 = (2 * R1 * R2) / ((h + 2*R1) * (h + 2*R2))
    term3 = np.log((h * (h + 2*R1 + 2*R2)) / ((h + 2*R1) * (h + 2*R2)))

    V = -(A_H / 6) * (term1 + term2 + term3)
    return V

def vdw_force_spheres(h, R1, R2, A_H):
    """
    Calculate van der Waals force between two equal spheres (simplified).
    F = -dV/dh

    For equal spheres at small separation: F ≈ A_H * R / (12 * h²)
    """
    h = np.maximum(h, 1e-12)
    R = (R1 + R2) / 2  # Average radius
    F = A_H * R / (12 * h**2)
    return F

# Hamaker constants for different materials (in J)
hamaker_constants = {
    'Au-Au (in vacuum)': 45e-20,
    'Au-Au (in water)': 30e-20,
    'SiO2-SiO2 (in vacuum)': 6.5e-20,
    'SiO2-SiO2 (in water)': 0.83e-20,
    'TiO2-TiO2 (in water)': 5.3e-20,
    'Polystyrene (in water)': 1.3e-20
}

# Parameters
R = 10e-9  # 10 nm radius (20 nm diameter)
h_range = np.logspace(-10, -7, 200)  # 0.1 nm to 100 nm separation

# Calculate interaction energies
fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Plot 1: Energy vs separation for different materials
ax1 = axes[0]
colors = plt.cm.tab10(np.linspace(0, 1, len(hamaker_constants)))

for (material, A_H), color in zip(hamaker_constants.items(), colors):
    V = vdw_energy_spheres(h_range, R, R, A_H)
    V_kT = V / (1.38e-23 * 300)  # Convert to kT units
    ax1.semilogx(h_range * 1e9, V_kT, linewidth=2, label=material, color=color)

ax1.axhline(y=-1, color='gray', linestyle='--', alpha=0.7)
ax1.text(0.15, -0.5, '-1 kT', fontsize=10, color='gray')
ax1.set_xlabel('Separation Distance (nm)', fontsize=12)
ax1.set_ylabel('Interaction Energy (kT at 300K)', fontsize=12)
ax1.set_title('Van der Waals Energy: 20 nm Particles', fontsize=13, fontweight='bold')
ax1.set_xlim(0.1, 100)
ax1.set_ylim(-100, 5)
ax1.legend(loc='lower right', fontsize=8)
ax1.grid(True, alpha=0.3)

# Plot 2: Effect of particle size
ax2 = axes[1]
A_H = 30e-20  # Au in water
radii_nm = [5, 10, 25, 50, 100]
colors2 = plt.cm.plasma(np.linspace(0.1, 0.9, len(radii_nm)))

for R_nm, color in zip(radii_nm, colors2):
    R_m = R_nm * 1e-9
    V = vdw_energy_spheres(h_range, R_m, R_m, A_H)
    V_kT = V / (1.38e-23 * 300)
    ax2.semilogx(h_range * 1e9, V_kT, linewidth=2,
                 label=f'd = {2*R_nm} nm', color=color)

ax2.axhline(y=-10, color='red', linestyle=':', alpha=0.7)
ax2.text(0.15, -8, 'Strong attraction (-10 kT)', fontsize=9, color='red')
ax2.set_xlabel('Separation Distance (nm)', fontsize=12)
ax2.set_ylabel('Interaction Energy (kT at 300K)', fontsize=12)
ax2.set_title('Size Effect on vdW Attraction (Au in water)', fontsize=13, fontweight='bold')
ax2.set_xlim(0.1, 100)
ax2.set_ylim(-200, 10)
ax2.legend(loc='lower right')
ax2.grid(True, alpha=0.3)

plt.tight_layout()
plt.savefig('vdw_attraction.png', dpi=150, bbox_inches='tight')
plt.show()

# Print key values at 1 nm separation
print("\n=== Van der Waals Energy at 1 nm Separation ===")
h_1nm = 1e-9
for material, A_H in hamaker_constants.items():
    V = vdw_energy_spheres(h_1nm, 10e-9, 10e-9, A_H)
    V_kT = V / (1.38e-23 * 300)
    print(f"{material:30s}: V = {V_kT:8.1f} kT")
Example Output:
=== Van der Waals Energy at 1 nm Separation ===
Au-Au (in vacuum) : V = -76.8 kT
Au-Au (in water) : V = -51.2 kT
SiO2-SiO2 (in vacuum) : V = -11.1 kT
SiO2-SiO2 (in water) : V = -1.4 kT
TiO2-TiO2 (in water) : V = -9.0 kT
Polystyrene (in water) : V = -2.2 kT

Critical Insight

When interaction energy exceeds ~10 kT, thermal motion cannot overcome attraction and particles will irreversibly agglomerate. Gold nanoparticles in water at 1 nm separation experience ~50 kT attraction - explaining why gold nanoparticles readily aggregate without stabilization!

1.3 Electrostatic Interactions

Charged particles in electrolyte solutions develop an electric double layer (EDL) consisting of the charged surface and a diffuse layer of counterions. This creates electrostatic repulsion that can stabilize dispersions.

graph LR A[Particle Surface] --> B[Stern Layer] B --> C[Diffuse Layer] C --> D[Bulk Solution] style A fill:#f9f,stroke:#333 style B fill:#ff9,stroke:#333 style C fill:#9ff,stroke:#333 style D fill:#fff,stroke:#333

Electric Double Layer Model

Surface potential decay (Gouy-Chapman):

$$\psi(x) = \psi_0 \exp(-\kappa x)$$

Debye length (characteristic decay distance):

$$\kappa^{-1} = \lambda_D = \sqrt{\frac{\varepsilon_r \varepsilon_0 k_B T}{2 N_A e^2 I}}$$

where \(I\) is ionic strength: \(I = \frac{1}{2}\sum_i c_i z_i^2\)

Example 3: Electric Double Layer and Debye Length

import numpy as np
import matplotlib.pyplot as plt

# ===================================
# Example 3: Electric Double Layer and Debye Length
# ===================================

# Physical constants
epsilon_0 = 8.854e-12  # Vacuum permittivity (F/m)
epsilon_r = 78.5       # Water relative permittivity at 25°C
kB = 1.38e-23          # Boltzmann constant (J/K)
T = 298                # Temperature (K)
e = 1.602e-19          # Elementary charge (C)
NA = 6.022e23          # Avogadro's number

def debye_length(ionic_strength_M):
    """
    Calculate Debye length for a 1:1 electrolyte in water at 25°C.

    Parameters:
        ionic_strength_M: Ionic strength in mol/L (M)

    Returns:
        Debye length in meters
    """
    I = ionic_strength_M * 1000  # Convert to mol/m³
    kappa_inv = np.sqrt((epsilon_r * epsilon_0 * kB * T) / (2 * NA * e**2 * I))
    return kappa_inv

def potential_decay(x, psi_0, kappa):
    """
    Calculate potential as function of distance from surface.

    Parameters:
        x: Distance from surface (m)
        psi_0: Surface potential (V)
        kappa: Inverse Debye length (1/m)

    Returns:
        Potential at distance x (V)
    """
    return psi_0 * np.exp(-kappa * x)

def electrostatic_repulsion(h, R, psi_0, ionic_strength_M):
    """
    Calculate electrostatic repulsion between two equal spheres.
    Linear superposition approximation for κR >> 1.

    Parameters:
        h: Surface separation (m)
        R: Particle radius (m)
        psi_0: Surface potential (V)
        ionic_strength_M: Ionic strength (M)

    Returns:
        Interaction energy (J)
    """
    kappa = 1 / debye_length(ionic_strength_M)

    # For constant potential surfaces
    V_elec = 2 * np.pi * epsilon_r * epsilon_0 * R * psi_0**2 * np.log(1 + np.exp(-kappa * h))
    return V_elec

# Calculate Debye length for various ionic strengths
ionic_strengths = np.logspace(-5, 0, 100)  # 10 μM to 1 M
debye_lengths = [debye_length(I) * 1e9 for I in ionic_strengths]  # in nm

# Common electrolyte concentrations
common_solutions = {
    'Ultrapure water': 1e-7,      # ~1 μM from autoionization
    'DI water (CO2)': 1e-5,        # ~10 μM with dissolved CO2
    '1 mM NaCl': 1e-3,
    '10 mM NaCl': 1e-2,
    '100 mM NaCl': 0.1,
    'Seawater (~0.6M)': 0.6
}

# Create plots
fig, axes = plt.subplots(1, 3, figsize=(15, 4.5))

# Plot 1: Debye length vs ionic strength
ax1 = axes[0]
ax1.loglog(ionic_strengths * 1000, debye_lengths, 'b-', linewidth=2.5)

for name, I in common_solutions.items():
    lambda_D = debye_length(I) * 1e9
    ax1.scatter([I * 1000], [lambda_D], s=80, zorder=5)
    ax1.annotate(f'{name}\n({lambda_D:.1f} nm)',
                 xy=(I * 1000, lambda_D), xytext=(5, 5),
                 textcoords='offset points', fontsize=8)

ax1.set_xlabel('Ionic Strength (mM)', fontsize=11)
ax1.set_ylabel('Debye Length (nm)', fontsize=11)
ax1.set_title('Debye Length vs Ionic Strength', fontsize=12, fontweight='bold')
ax1.grid(True, alpha=0.3, which='both')
ax1.set_xlim(1e-2, 1e3)
ax1.set_ylim(0.1, 1000)

# Plot 2: Potential decay profiles
ax2 = axes[1]
x = np.linspace(0, 50e-9, 200)
psi_0 = 0.050  # 50 mV surface potential

for I_mM, color, ls in [(1, 'blue', '-'), (10, 'green', '--'), (100, 'red', ':')]:
    I = I_mM / 1000
    kappa = 1 / debye_length(I)
    psi = potential_decay(x, psi_0, kappa)
    lambda_D = debye_length(I) * 1e9
    ax2.plot(x * 1e9, psi * 1000, color=color, linestyle=ls, linewidth=2,
             label=f'{I_mM} mM (λD = {lambda_D:.1f} nm)')

ax2.axhline(y=psi_0 * 1000 / np.e, color='gray', linestyle=':', alpha=0.7)
ax2.text(45, psi_0 * 1000 / np.e + 2, 'ψ₀/e', fontsize=10, color='gray')
ax2.set_xlabel('Distance from Surface (nm)', fontsize=11)
ax2.set_ylabel('Potential (mV)', fontsize=11)
ax2.set_title('Potential Decay (ψ₀ = 50 mV)', fontsize=12, fontweight='bold')
ax2.legend(loc='upper right')
ax2.grid(True, alpha=0.3)
ax2.set_xlim(0, 50)

# Plot 3: Electrostatic repulsion energy
ax3 = axes[2]
h_range = np.linspace(0.5e-9, 30e-9, 200)
R = 10e-9  # 10 nm radius
psi_0 = 0.030  # 30 mV

for I_mM, color in [(1, 'blue'), (10, 'green'), (100, 'red')]:
    I = I_mM / 1000
    V_elec = [electrostatic_repulsion(h, R, psi_0, I) for h in h_range]
    V_kT = np.array(V_elec) / (kB * T)
    ax3.plot(h_range * 1e9, V_kT, color=color, linewidth=2,
             label=f'{I_mM} mM')

ax3.axhline(y=10, color='gray', linestyle='--', alpha=0.7)
ax3.text(25, 12, 'Barrier > 10 kT', fontsize=9, color='gray')
ax3.set_xlabel('Separation Distance (nm)', fontsize=11)
ax3.set_ylabel('Repulsion Energy (kT)', fontsize=11)
ax3.set_title('Electrostatic Repulsion (R=10nm, ψ₀=30mV)', fontsize=12, fontweight='bold')
ax3.legend(loc='upper right')
ax3.grid(True, alpha=0.3)
ax3.set_xlim(0, 30)
ax3.set_ylim(0, 50)

plt.tight_layout()
plt.savefig('electric_double_layer.png', dpi=150, bbox_inches='tight')
plt.show()

# Print Debye lengths for common solutions
print("\n=== Debye Lengths for Common Solutions (25°C) ===")
for name, I in sorted(common_solutions.items(), key=lambda x: x[1]):
    lambda_D = debye_length(I)
    print(f"{name:25s}: I = {I*1000:8.3f} mM, λD = {lambda_D*1e9:8.2f} nm")
Example Output:
=== Debye Lengths for Common Solutions (25°C) ===
Ultrapure water : I = 0.000 mM, λD = 961.47 nm
DI water (CO2) : I = 0.010 mM, λD = 96.15 nm
1 mM NaCl : I = 1.000 mM, λD = 9.61 nm
10 mM NaCl : I = 10.000 mM, λD = 3.04 nm
100 mM NaCl : I = 100.000 mM, λD = 0.96 nm
Seawater (~0.6M) : I = 600.000 mM, λD = 0.39 nm

Practical Implication

The Debye length determines the range of electrostatic repulsion. In 10 mM NaCl (typical buffer), repulsion extends only ~3 nm from the surface. In seawater, the double layer is compressed to <1 nm, eliminating electrostatic stabilization!

1.4 Capillary Forces (Liquid Bridges)

When particles are exposed to moisture or during drying processes, liquid bridges form between particles creating strong capillary forces. These forces can be the dominant cause of agglomeration in humid environments or spray-dried powders.

Capillary force between two spheres (equal radii R):

$$F_{cap} = 2\pi R \gamma \cos\theta$$

where \(\gamma\) is surface tension and \(\theta\) is contact angle.

For water at 25°C: \(\gamma = 0.072\) N/m

Example 4: Capillary Force Calculation

import numpy as np
import matplotlib.pyplot as plt

# ===================================
# Example 4: Capillary Force Between Nanoparticles
# ===================================

def capillary_force(R, gamma, theta_deg):
    """
    Calculate capillary force between two equal spheres.

    Parameters:
        R: Particle radius (m)
        gamma: Surface tension (N/m)
        theta_deg: Contact angle (degrees)

    Returns:
        Capillary force (N)
    """
    theta_rad = np.radians(theta_deg)
    F = 2 * np.pi * R * gamma * np.cos(theta_rad)
    return F

def capillary_energy(R, gamma, theta_deg):
    """
    Estimate capillary interaction energy.
    E ~ F * h where h ~ R (rough approximation)
    """
    F = capillary_force(R, gamma, theta_deg)
    return F * R

# Parameters
gamma_water = 0.072      # N/m (water at 25°C)
gamma_ethanol = 0.022    # N/m (ethanol)
gamma_hexane = 0.018     # N/m (hexane)

# Particle size range
R_range = np.logspace(-9, -6, 100)  # 1 nm to 1 μm

# Calculate for different liquids (hydrophilic particles, θ ≈ 30°)
theta = 30  # degrees

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# Plot 1: Capillary force vs particle size
ax1 = axes[0]
for liquid, gamma, color in [('Water', gamma_water, 'blue'),
                              ('Ethanol', gamma_ethanol, 'green'),
                              ('Hexane', gamma_hexane, 'orange')]:
    F = [capillary_force(R, gamma, theta) for R in R_range]
    ax1.loglog(R_range * 1e9, np.array(F) * 1e9, linewidth=2,
               label=f'{liquid} (γ = {gamma*1000:.0f} mN/m)', color=color)

# Add particle weight for comparison (silica)
rho = 2200  # kg/m³ (silica)
g = 9.8     # m/s²
weights = [(4/3) * np.pi * R**3 * rho * g for R in R_range]
ax1.loglog(R_range * 1e9, np.array(weights) * 1e9, 'k--',
           linewidth=1.5, label='Particle weight (SiO₂)')

ax1.set_xlabel('Particle Radius (nm)', fontsize=11)
ax1.set_ylabel('Force (nN)', fontsize=11)
ax1.set_title('Capillary Force vs Particle Size (θ = 30°)', fontsize=12, fontweight='bold')
ax1.legend()
ax1.grid(True, alpha=0.3, which='both')
ax1.set_xlim(1, 1000)

# Plot 2: Effect of contact angle
ax2 = axes[1]
theta_range = np.linspace(0, 90, 100)
R_fixed = 50e-9  # 50 nm

for liquid, gamma, color in [('Water', gamma_water, 'blue'),
                              ('Ethanol', gamma_ethanol, 'green')]:
    F = [capillary_force(R_fixed, gamma, theta) for theta in theta_range]
    ax2.plot(theta_range, np.array(F) * 1e9, linewidth=2,
             label=f'{liquid}', color=color)

ax2.axvline(x=90, color='gray', linestyle=':', alpha=0.7)
ax2.text(85, 0.5, 'No bridge\n(θ > 90°)', fontsize=9, ha='right')
ax2.set_xlabel('Contact Angle (degrees)', fontsize=11)
ax2.set_ylabel('Capillary Force (nN)', fontsize=11)
ax2.set_title('Effect of Contact Angle (R = 50 nm)', fontsize=12, fontweight='bold')
ax2.legend()
ax2.grid(True, alpha=0.3)
ax2.set_xlim(0, 90)

plt.tight_layout()
plt.savefig('capillary_forces.png', dpi=150, bbox_inches='tight')
plt.show()

# Compare forces at 50 nm
print("\n=== Force Comparison at R = 50 nm ===")
R = 50e-9
F_cap = capillary_force(R, gamma_water, 30)
F_vdw = 30e-20 * R / (12 * (1e-9)**2)  # vdW at 1 nm separation
F_weight = (4/3) * np.pi * R**3 * 2200 * 9.8
print(f"Capillary (water, θ=30°): {F_cap*1e9:.3f} nN")
print(f"Van der Waals (at 1 nm):  {F_vdw*1e9:.3f} nN")
print(f"Particle weight (SiO₂):  {F_weight*1e15:.3f} fN")
print(f"\nRatio F_cap/F_weight: {F_cap/F_weight:.1e}")
Example Output:
=== Force Comparison at R = 50 nm ===
Capillary (water, θ=30°): 19.642 nN
Van der Waals (at 1 nm): 1.250 nN
Particle weight (SiO₂): 11.258 fN

Ratio F_cap/F_weight: 1.7e+06

Key Insight

Capillary forces can be 10-100× stronger than van der Waals forces and millions of times stronger than gravity! This is why nanoparticle powders become "sticky" in humid environments and why careful drying protocols are essential.

1.5 Sintering and Necking

At elevated temperatures, nanoparticles can form permanent bonds through sintering. Atoms diffuse from particle interiors to contact points, forming "necks" that grow over time. This is particularly problematic for metallic nanoparticles.

Sintering Mechanisms

  • Surface diffusion: Atoms migrate along particle surfaces (dominant at lower T)
  • Volume diffusion: Atoms move through particle interior (higher T)
  • Grain boundary diffusion: Transport along grain boundaries
  • Viscous flow: For amorphous materials (e.g., silica)

Temperature Dependence

Sintering rate follows an Arrhenius relationship:

$$\frac{dx}{dt} \propto D_s \propto \exp\left(-\frac{E_a}{RT}\right)$$

where \(x\) is neck radius, \(D_s\) is surface diffusion coefficient, \(E_a\) is activation energy.

Example 5: Sintering Temperature Estimation

import numpy as np
import matplotlib.pyplot as plt

# ===================================
# Example 5: Sintering Temperature for Nanoparticles
# ===================================

# Sintering typically begins at T/T_m ≈ 0.3-0.5 for nanoparticles
# (lower than bulk due to high surface energy)

def tammann_temperature(T_m, factor=0.5):
    """
    Estimate Tammann temperature (onset of significant diffusion).
    For nanoparticles, this can be 0.3-0.5 × T_m.
    """
    return factor * T_m

def sintering_rate(T, T_m, E_a, R_gas=8.314):
    """
    Relative sintering rate (Arrhenius-type).

    Parameters:
        T: Temperature (K)
        T_m: Melting point (K)
        E_a: Activation energy (J/mol)
        R_gas: Gas constant

    Returns:
        Relative rate (normalized to T_m)
    """
    rate = np.exp(-E_a / (R_gas * T))
    rate_ref = np.exp(-E_a / (R_gas * T_m))
    return rate / rate_ref

# Material properties (melting points and typical activation energies)
materials = {
    'Gold (Au)': {'T_m': 1337, 'E_a': 170e3, 'color': 'gold'},
    'Silver (Ag)': {'T_m': 1235, 'E_a': 180e3, 'color': 'silver'},
    'Copper (Cu)': {'T_m': 1358, 'E_a': 200e3, 'color': 'orange'},
    'Silica (SiO2)': {'T_m': 1986, 'E_a': 300e3, 'color': 'blue'},
    'Titania (TiO2)': {'T_m': 2116, 'E_a': 250e3, 'color': 'green'}
}

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# Plot 1: Sintering onset temperatures
ax1 = axes[0]
x_pos = np.arange(len(materials))
bar_colors = [m['color'] for m in materials.values()]

T_m_values = [m['T_m'] for m in materials.values()]
T_sinter_nano = [tammann_temperature(m['T_m'], 0.3) for m in materials.values()]
T_sinter_bulk = [tammann_temperature(m['T_m'], 0.5) for m in materials.values()]

width = 0.25
ax1.bar(x_pos - width, T_m_values, width, label='Melting point', color='red', alpha=0.7)
ax1.bar(x_pos, T_sinter_bulk, width, label='Sintering (bulk, 0.5×Tm)', color='orange', alpha=0.7)
ax1.bar(x_pos + width, T_sinter_nano, width, label='Sintering (nano, 0.3×Tm)', color='green', alpha=0.7)

ax1.axhline(y=373, color='blue', linestyle='--', alpha=0.7)
ax1.text(4.5, 400, '100°C (Drying)', fontsize=9, color='blue')

ax1.set_ylabel('Temperature (K)', fontsize=11)
ax1.set_title('Sintering Onset Temperatures', fontsize=12, fontweight='bold')
ax1.set_xticks(x_pos)
ax1.set_xticklabels(materials.keys(), rotation=15, ha='right')
ax1.legend(loc='upper left')
ax1.grid(True, alpha=0.3, axis='y')

# Plot 2: Sintering rate vs temperature (Au example)
ax2 = axes[1]
T_range = np.linspace(300, 1337, 200)

for name, props in materials.items():
    T_m = props['T_m']
    E_a = props['E_a']
    T_scaled = T_range[T_range <= T_m]
    rates = sintering_rate(T_scaled, T_m, E_a)
    T_ratio = T_scaled / T_m
    ax2.semilogy(T_ratio, rates, linewidth=2, label=name, color=props['color'])

ax2.axvline(x=0.3, color='green', linestyle='--', alpha=0.7)
ax2.axvline(x=0.5, color='orange', linestyle='--', alpha=0.7)
ax2.text(0.31, 1e-6, 'Nano onset', fontsize=9, color='green', rotation=90)
ax2.text(0.51, 1e-6, 'Bulk onset', fontsize=9, color='orange', rotation=90)

ax2.set_xlabel('T / T_m (Homologous Temperature)', fontsize=11)
ax2.set_ylabel('Relative Sintering Rate', fontsize=11)
ax2.set_title('Sintering Rate vs Temperature', fontsize=12, fontweight='bold')
ax2.legend(loc='lower right')
ax2.grid(True, alpha=0.3, which='both')
ax2.set_xlim(0.2, 1.0)
ax2.set_ylim(1e-10, 1)

plt.tight_layout()
plt.savefig('sintering.png', dpi=150, bbox_inches='tight')
plt.show()

# Print critical temperatures
print("\n=== Critical Temperatures for Nanoparticle Processing ===")
print(f"{'Material':20s} {'T_m (K)':>10s} {'T_m (°C)':>10s} {'Nano onset (°C)':>15s}")
print("-" * 60)
for name, props in materials.items():
    T_m = props['T_m']
    T_nano = tammann_temperature(T_m, 0.3)
    print(f"{name:20s} {T_m:10.0f} {T_m-273:10.0f} {T_nano-273:15.0f}")
Example Output:
=== Critical Temperatures for Nanoparticle Processing ===
Material T_m (K) T_m (°C) Nano onset (°C)
------------------------------------------------------------
Gold (Au) 1337 1064 128
Silver (Ag) 1235 962 98
Copper (Cu) 1358 1085 134
Silica (SiO2) 1986 1713 323
Titania (TiO2) 2116 1843 362

Processing Implications

Silver nanoparticles can begin sintering below 100°C - even during drying! This is why low-temperature processing and surface passivation are critical for preserving nanoparticle dispersibility in metallic systems.

1.6 Aggregation vs Agglomeration

Understanding the distinction between aggregation and agglomeration is crucial for selecting appropriate dispersion strategies. IUPAC provides clear definitions:

Property Aggregation Agglomeration
Bonding Type Strong (covalent, metallic, ionic) Weak (van der Waals, electrostatic, capillary)
Reversibility Irreversible Reversible (with appropriate energy input)
Surface Area Significantly reduced Approximately preserved
Causes Sintering, chemical bonding Physical forces, drying, settling
Dispersion Method Cannot be dispersed (must prevent) Mechanical, chemical methods effective

Example 6: Identifying Agglomeration vs Aggregation

import numpy as np
import matplotlib.pyplot as plt

# ===================================
# Example 6: Distinguishing Aggregation from Agglomeration
# ===================================

class ParticleCluster:
    """Model for analyzing particle clustering behavior."""

    def __init__(self, n_primary, d_primary_nm, cluster_type='agglomerate'):
        """
        Parameters:
            n_primary: Number of primary particles
            d_primary_nm: Primary particle diameter (nm)
            cluster_type: 'agglomerate' or 'aggregate'
        """
        self.n = n_primary
        self.d = d_primary_nm * 1e-9  # Convert to m
        self.type = cluster_type

    def theoretical_surface_area(self):
        """Surface area if particles were separate."""
        return self.n * np.pi * self.d**2

    def actual_surface_area(self):
        """
        Actual surface area of cluster.
        - Agglomerates: ~80-100% of theoretical (loose packing)
        - Aggregates: ~30-60% of theoretical (sintered contacts)
        """
        if self.type == 'agglomerate':
            # Loose packing, small contact areas
            return 0.9 * self.theoretical_surface_area()
        else:  # aggregate
            # Significant neck formation, reduced surface
            return 0.4 * self.theoretical_surface_area()

    def dispersion_energy_required(self):
        """
        Estimate energy required to disperse (per particle pair).
        Returns energy in kT units at 300 K.
        """
        kT = 1.38e-23 * 300

        if self.type == 'agglomerate':
            # van der Waals at contact (~10-100 kT)
            A_H = 20e-20  # Typical Hamaker constant
            E_vdw = A_H * self.d / (24 * 0.3e-9)  # ~0.3 nm contact
            return E_vdw / kT
        else:  # aggregate
            # Chemical bond energy (~eV range)
            E_bond = 1e-19  # ~1 eV in Joules
            return E_bond / kT

    def is_redispersible(self, available_energy_kT):
        """Check if cluster can be dispersed with given energy."""
        required = self.dispersion_energy_required()
        return available_energy_kT > required

# Comparison study
d_primary = 20  # nm
n_particles = 100

agglomerate = ParticleCluster(n_particles, d_primary, 'agglomerate')
aggregate = ParticleCluster(n_particles, d_primary, 'aggregate')

# Create visualization
fig, axes = plt.subplots(1, 3, figsize=(15, 4.5))

# Plot 1: Surface area comparison
ax1 = axes[0]
categories = ['Theoretical\n(dispersed)', 'Agglomerate', 'Aggregate']
sa_values = [
    agglomerate.theoretical_surface_area() * 1e12,  # μm²
    agglomerate.actual_surface_area() * 1e12,
    aggregate.actual_surface_area() * 1e12
]
colors = ['green', 'blue', 'red']
bars = ax1.bar(categories, sa_values, color=colors, alpha=0.7, edgecolor='black')
ax1.set_ylabel('Surface Area (μm²)', fontsize=11)
ax1.set_title(f'Surface Area: {n_particles}× {d_primary}nm Particles',
              fontsize=12, fontweight='bold')
for bar, val in zip(bars, sa_values):
    ax1.text(bar.get_x() + bar.get_width()/2, bar.get_height() + 0.02*max(sa_values),
             f'{val:.2f}', ha='center', fontsize=10)
ax1.grid(True, alpha=0.3, axis='y')

# Plot 2: Dispersion energy comparison
ax2 = axes[1]
cluster_types = ['Agglomerate\n(vdW)', 'Aggregate\n(sintered)']
energies = [agglomerate.dispersion_energy_required(),
            aggregate.dispersion_energy_required()]
colors = ['blue', 'red']
bars = ax2.bar(cluster_types, energies, color=colors, alpha=0.7, edgecolor='black')
ax2.axhline(y=10, color='green', linestyle='--', label='Ultrasonic (~10 kT)')
ax2.axhline(y=100, color='orange', linestyle='--', label='Ball mill (~100 kT)')
ax2.set_ylabel('Dispersion Energy Required (kT)', fontsize=11)
ax2.set_title('Energy to Break Particle Contacts', fontsize=12, fontweight='bold')
ax2.set_yscale('log')
ax2.legend()
ax2.grid(True, alpha=0.3, axis='y')

# Plot 3: Dispersion success by method
ax3 = axes[2]
methods = ['Stirring\n(1 kT)', 'Ultrasonic\n(30 kT)', 'Ball mill\n(200 kT)',
           'High-shear\n(500 kT)']
energies_available = [1, 30, 200, 500]

# Calculate success for each type
agglom_success = [agglomerate.is_redispersible(E) for E in energies_available]
aggreg_success = [aggregate.is_redispersible(E) for E in energies_available]

x = np.arange(len(methods))
width = 0.35

bars1 = ax3.bar(x - width/2, [int(s) for s in agglom_success], width,
                label='Agglomerate', color='blue', alpha=0.7)
bars2 = ax3.bar(x + width/2, [int(s) for s in aggreg_success], width,
                label='Aggregate', color='red', alpha=0.7)

ax3.set_xticks(x)
ax3.set_xticklabels(methods)
ax3.set_ylabel('Dispersible (1=Yes, 0=No)', fontsize=11)
ax3.set_title('Dispersion Success by Method', fontsize=12, fontweight='bold')
ax3.legend()
ax3.set_ylim(0, 1.5)
ax3.grid(True, alpha=0.3, axis='y')

plt.tight_layout()
plt.savefig('aggregation_vs_agglomeration.png', dpi=150, bbox_inches='tight')
plt.show()

# Summary
print("\n=== Aggregation vs Agglomeration Summary ===")
print(f"{'Property':30s} {'Agglomerate':>15s} {'Aggregate':>15s}")
print("-" * 60)
print(f"{'Surface area retention':30s} {'90%':>15s} {'40%':>15s}")
print(f"{'Dispersion energy (kT)':30s} {agglomerate.dispersion_energy_required():>15.1f} {aggregate.dispersion_energy_required():>15.0f}")
print(f"{'Ultrasonic dispersible':30s} {'Yes':>15s} {'No':>15s}")
print(f"{'Ball mill dispersible':30s} {'Yes':>15s} {'Unlikely':>15s}")
Example Output:
=== Aggregation vs Agglomeration Summary ===
Property Agglomerate Aggregate
------------------------------------------------------------
Surface area retention 90% 40%
Dispersion energy (kT) 55.6 24155
Ultrasonic dispersible Yes No
Ball mill dispersible Yes Unlikely

Chapter Summary

Key Takeaways

  1. Surface effects dominate: At 10 nm, ~50% of atoms are at the surface, driving high reactivity and agglomeration tendency.
  2. Van der Waals forces are always attractive; Hamaker constant determines strength (metals > oxides > polymers).
  3. Electrostatic repulsion can stabilize dispersions, but Debye length decreases with ionic strength.
  4. Capillary forces during drying can be 10-100× stronger than vdW forces.
  5. Sintering begins at 0.3-0.5 × T_m for nanoparticles - much lower than bulk materials.
  6. Agglomeration is reversible; aggregation (sintering) is permanent and must be prevented.

Exercises

Exercise 1: Calculate specific surface area

Calculate the specific surface area (m²/g) for:

  1. 20 nm gold nanoparticles (ρ = 19.3 g/cm³)
  2. 50 nm silica nanoparticles (ρ = 2.2 g/cm³)
  3. 100 nm polystyrene particles (ρ = 1.05 g/cm³)

Hint: Use SSA = 6/(d×ρ) for spheres.

Exercise 2: Compare interaction energies

For two 30 nm TiO₂ particles in water (A_H = 5.3×10⁻²⁰ J) at 2 nm separation:

  1. Calculate the van der Waals attraction energy in kT.
  2. What surface potential (in mV) is needed for electrostatic repulsion to overcome vdW attraction at this separation?
Exercise 3: Debye length effects

A nanoparticle dispersion is stable in 1 mM NaCl but aggregates in 100 mM NaCl.

  1. Calculate the Debye lengths for both conditions.
  2. Explain why stability changes in terms of the DLVO energy barrier.
Exercise 4: Processing temperature limits

You need to dry silver nanoparticles without sintering. Based on the Tammann temperature:

  1. What is the maximum safe drying temperature?
  2. Suggest an alternative drying method that avoids thermal sintering.
Exercise 5: Dispersion method selection

A powder sample shows 200 nm clusters of 20 nm primary particles. DLS after ultrasonication shows 50 nm hydrodynamic diameter.

  1. Is this likely aggregation or agglomeration? Explain.
  2. What additional characterization would confirm your answer?

Next Steps

In Chapter 1, we learned the fundamental mechanisms driving nanoparticle agglomeration. In Chapter 2, we will explore the factors that influence these interactions and how they can be controlled.

Next Chapter Preview (Chapter 2)

  • Particle size effects and critical diameters
  • Surface energy and modification strategies
  • Environmental effects (humidity, temperature, pH)
  • Particle shape and surface state influences

References

  1. Israelachvili, J. N. (2011). Intermolecular and Surface Forces (3rd ed.). Academic Press.
  2. Hunter, R. J. (2001). Foundations of Colloid Science (2nd ed.). Oxford University Press.
  3. Hamaker, H. C. (1937). "The London—van der Waals attraction between spherical particles." Physica, 4(10), 1058-1072.
  4. Lyklema, J. (1995). Fundamentals of Interface and Colloid Science. Academic Press.

Disclaimer