Skip to content

[Phase E3-F4-P4] Update Coagulation strategies to accept both old and new data types with tests #1071

@Gorkowski

Description

@Gorkowski

Dependencies:

#1068 [E3-F4-P1] ──┐
                    ├──► #THIS [E3-F4-P4]
#1069 [E3-F4-P2] ──┘

Parent Issue: #1020

Dependencies:


Description

Update CoagulationStrategyABC and its concrete implementations (BrownianCoagulationStrategy, ChargedCoagulationStrategy, SedimentationCoagulationStrategy, TurbulentShearCoagulationStrategy, TurbulentDnsCoagulationStrategy, CombineCoagulationStrategy) to accept both legacy ParticleRepresentation and new ParticleData types. When ParticleData is passed, the strategy should work with it directly; when ParticleRepresentation is passed, behavior remains unchanged.

Context

This is the fourth phase of E3-F4. With the facades in place (P1 #1068, P2 #1069), the coagulation strategies can now accept both old and new types. The coagulation ABC (coagulation_strategy_abc.py, 477 lines) defines kernel(), loss_rate(), gain_rate(), net_rate(), step(), diffusive_knudsen(), coulomb_potential_ratio(), and friction_factor() - all taking ParticleRepresentation. The concrete strategies override kernel() and dimensionless_kernel().

Parent Issue: #1020 (E3-F4: Facade and Migration)
Depends on: #1068 (P1: ParticleRepresentation facade), #1069 (P2: GasSpecies facade)

Value:

  • Enables users to pass ParticleData directly to coagulation without wrapping in facades
  • Reduces overhead for new code paths that don't need strategy coupling
  • Consistent dual-type support across all dynamics modules

Scope

Estimated Lines of Code: ~150 lines (excluding tests)
Complexity: Medium

Files to Modify:

  • particula/dynamics/coagulation/coagulation_strategy/coagulation_strategy_abc.py (~100 LOC changes) - ABC with shared methods
  • particula/dynamics/coagulation/coagulation_strategy/brownian_coagulation_strategy.py (~10 LOC) - Update kernel() signature
  • particula/dynamics/coagulation/coagulation_strategy/charged_coagulation_strategy.py (~10 LOC)
  • particula/dynamics/coagulation/coagulation_strategy/sedimentation_coagulation_strategy.py (~10 LOC)
  • particula/dynamics/coagulation/coagulation_strategy/turbulent_shear_coagulation_strategy.py (~10 LOC)
  • particula/dynamics/coagulation/coagulation_strategy/turbulent_dns_coagulation_strategy.py (~10 LOC)

Files to Modify (tests):

  • particula/dynamics/coagulation/coagulation_strategy/tests/brownian_coagulation_strategy_test.py (~50 LOC additions)

Acceptance Criteria

Core Implementation

  • Add _unwrap_particle() helper to coagulation_strategy_abc.py (or reuse from a shared utility if condensation P3 extracts it)
  • Update CoagulationStrategyABC.kernel() abstract signature to Union[ParticleRepresentation, ParticleData]
  • Update CoagulationStrategyABC.loss_rate() to accept both types - extract concentration and get_radius() equivalently from both
  • Update CoagulationStrategyABC.gain_rate() to accept both types
  • Update CoagulationStrategyABC.net_rate() to accept both types
  • Update CoagulationStrategyABC.step() to detect input type, process, and return matching type
  • Update CoagulationStrategyABC.diffusive_knudsen() to accept both types
  • Update CoagulationStrategyABC.coulomb_potential_ratio() to accept both types
  • Update CoagulationStrategyABC.friction_factor() to accept both types
  • Update BrownianCoagulationStrategy.kernel() to accept Union[ParticleRepresentation, ParticleData]
  • Update ChargedCoagulationStrategy.kernel() similarly
  • Update SedimentationCoagulationStrategy.kernel() similarly
  • Update TurbulentShearCoagulationStrategy.kernel() similarly
  • Update TurbulentDnsCoagulationStrategy.kernel() similarly
  • For ParticleData path, use .radii property for radius, .total_mass for mass, .concentration for concentration, .charge for charge (with appropriate box_index=0 slicing)
  • step() for ParticleData returns updated ParticleData (new instance or mutated copy)
  • step() for ParticleRepresentation returns ParticleRepresentation (existing behavior)

Testing (REQUIRED - Co-located with implementation)

  • All existing tests in particula/dynamics/coagulation/coagulation_strategy/tests/ pass WITHOUT modification
  • New test: test_brownian_kernel_with_particle_data - verify kernel() works with ParticleData
  • New test: test_brownian_step_with_particle_data - verify step() works with ParticleData and returns ParticleData
  • New test: test_brownian_step_returns_matching_types - verify legacy returns legacy, new returns new
  • New test: test_loss_rate_with_particle_data - verify loss_rate() works
  • New test: test_gain_rate_with_particle_data - verify gain_rate() works
  • New test: test_net_rate_with_particle_data - verify net_rate() works
  • New test: test_diffusive_knudsen_with_particle_data - verify helper works
  • All tests pass before merge
  • Achieve 95%+ coverage on modified code

Technical Notes

Implementation Approach

The coagulation ABC methods access particle data via method calls like particle.get_radius(), particle.get_mass(), particle.concentration, particle.get_charge(). For ParticleData, the equivalents are:

ParticleRepresentation ParticleData (box_index=0)
particle.get_radius() data.radii[0]
particle.get_mass() data.total_mass[0]
particle.get_species_mass() data.masses[0]
particle.concentration data.concentration[0]
particle.get_charge() data.charge[0]
particle.get_volume() float(data.volume[0])
particle.get_density() data.density
particle.get_effective_density() data.effective_density[0]

The cleanest approach is an adapter pattern:

from typing import Union
from particula.particles.particle_data import ParticleData
from particula.particles.representation import ParticleRepresentation

def _get_radius(
    particle: Union[ParticleRepresentation, ParticleData],
) -> NDArray[np.float64]:
    """Get radius array from either type."""
    if isinstance(particle, ParticleData):
        return particle.radii[0]
    return particle.get_radius()

def _get_mass(
    particle: Union[ParticleRepresentation, ParticleData],
) -> NDArray[np.float64]:
    """Get total mass array from either type."""
    if isinstance(particle, ParticleData):
        return particle.total_mass[0]
    return particle.get_mass()

def _get_concentration(
    particle: Union[ParticleRepresentation, ParticleData],
) -> NDArray[np.float64]:
    """Get concentration array from either type."""
    if isinstance(particle, ParticleData):
        return particle.concentration[0]
    return particle.concentration

def _get_charge(
    particle: Union[ParticleRepresentation, ParticleData],
) -> NDArray[np.float64]:
    """Get charge array from either type."""
    if isinstance(particle, ParticleData):
        return particle.charge[0]
    return particle.get_charge()

Key Design Decisions

  1. Adapter functions: Instead of modifying every method body, use adapter functions that normalize access patterns. This keeps the actual physics code unchanged.
  2. ParticleData single-box path: All coagulation methods assume single-box simulation when ParticleData is passed (use [0] indexing).
  3. Particle-resolved coagulation: step() for distribution_type="particle_resolved" calls particle.collide_pairs() and accesses particle.get_volume(). The ParticleData path must implement equivalent mutations.
  4. CombineCoagulationStrategy: This composes multiple strategies. It should automatically support both types since it delegates to the individual strategies.

Critical Compatibility Notes

  • loss_rate() and gain_rate() directly access particle.concentration (not via a method) in some code paths - this is an attribute on ParticleRepresentation but a property on ParticleData
  • step() calls particle.add_concentration() for discrete/continuous distributions - ParticleData doesn't have this method, so the P4 code must handle concentration updates directly
  • step() for particle-resolved calls particle.collide_pairs() - for ParticleData, equivalent mass/concentration merging must be performed

Integration Points

  • particula/dynamics/coagulation/coagulation_strategy/coagulation_strategy_abc.py - Main ABC
  • particula/dynamics/coagulation/coagulation_strategy/brownian_coagulation_strategy.py - Primary concrete strategy
  • particula/dynamics/coagulation/coagulation_rate.py - Rate functions take raw arrays (radius, concentration, kernel)
  • particula/particles/particle_data.py - ParticleData container

Edge Cases and Considerations

  • Particle-resolved coagulation: collide_pairs() merges particles by modifying distribution, concentration, and charge. For ParticleData, this means directly updating masses, concentration, charge arrays at [0, idx, :].
  • Zero-particle case: If ParticleData has n_particles=0, coagulation should no-op gracefully.
  • CombineCoagulationStrategy: Multiple strategies composed - verify that dual-type support propagates through the combination.
  • Kernel computation: Concrete strategies compute kernel from radius and mass. Adapter functions must provide these in the correct shape.

Example Usage

import numpy as np
from particula.particles.particle_data import ParticleData
from particula.dynamics.coagulation import BrownianCoagulationStrategy

particle_data = ParticleData(
    masses=np.random.rand(1, 50, 2) * 1e-18,
    concentration=np.ones((1, 50)) * 1e6,
    charge=np.zeros((1, 50)),
    density=np.array([1000.0, 1200.0]),
    volume=np.array([1.0]),
)

strategy = BrownianCoagulationStrategy(distribution_type="discrete")
# Returns updated ParticleData
updated = strategy.step(particle_data, 298.15, 101325.0, 1.0)
assert isinstance(updated, ParticleData)

References

Feature Plan:

  • adw-docs/dev-plans/features/E3-F4-facade-migration.md

Related Issues:

Related Code:

  • particula/dynamics/coagulation/coagulation_strategy/coagulation_strategy_abc.py (477 lines) - Main ABC
  • particula/dynamics/coagulation/coagulation_strategy/brownian_coagulation_strategy.py - Brownian kernel
  • particula/dynamics/coagulation/coagulation_strategy/charged_coagulation_strategy.py - Charged kernel
  • particula/dynamics/coagulation/coagulation_strategy/sedimentation_coagulation_strategy.py - Sedimentation
  • particula/dynamics/coagulation/coagulation_strategy/turbulent_shear_coagulation_strategy.py - Turbulent shear
  • particula/dynamics/coagulation/coagulation_strategy/turbulent_dns_coagulation_strategy.py - Turbulent DNS
  • particula/dynamics/coagulation/coagulation_strategy/combine_coagulation_strategy.py - Combined
  • particula/dynamics/coagulation/coagulation_strategy/tests/ - Existing tests

Coding Standards:

  • adw-docs/code_style.md - Python standards (snake_case, 80-char lines)
  • adw-docs/testing_guide.md - Testing patterns (*_test.py suffix)

Metadata

Metadata

Assignees

No one assigned

    Labels

    agentCreated or managed by ADW automationblockedBlocked - review required before ADW can processfeatureNew feature or significant enhancementmodel:defaultUse base/sonnet tier (workflow default)type:patchQuick patch workflow (plan → build → ship)

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions