Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion adw-docs/dev-plans/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,8 @@ and rollout.
- [E5-F2: Non-Isothermal Mass Transfer Functions][e5-f2] — Status: Planning
- Scope: Thermal resistance factor and non-isothermal mass transfer rate
pure functions with energy tracking.
- [E5-F3: CondensationLatentHeat Strategy Class][e5-f3] — Status: Planning
- [E5-F3: CondensationLatentHeat Strategy Class][e5-f3] — Status: In Progress
(P1, #1139)
- Scope: New condensation strategy with latent heat correction and energy
diagnostics.
- [E5-F4: Builder, Factory, and Exports][e5-f4] — Status: Planning
Expand Down
7 changes: 5 additions & 2 deletions adw-docs/dev-plans/epics/E5-non-isothermal-condensation.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
**Owners**: @Gorkowski
**Start Date**: 2026-03-02
**Target Date**: TBD
**Last Updated**: 2026-03-02
**Last Updated**: 2026-03-04
**Size**: Medium (7 features, ~22 phases)

## Vision
Expand Down Expand Up @@ -233,6 +233,7 @@ L -> 0 as T -> T_c. Used in engineering thermodynamics and EOS-based models.
### Feature E5-F3: `CondensationLatentHeat` Strategy Class

- [ ] **E5-F3-P1**: Create `CondensationLatentHeat` class skeleton with tests
- Issue: #1139 | Size: M (~80 LOC) | Status: In Progress
- File: `particula/dynamics/condensation/condensation_strategies.py` (extend
existing file, currently 1699 lines with `CondensationStrategy` ABC,
`CondensationIsothermal`, and `CondensationIsothermalStaggered`)
Expand All @@ -253,7 +254,8 @@ L -> 0 as T -> T_c. Used in engineering thermodynamics and EOS-based models.
- Tests: `particula/dynamics/condensation/tests/
condensation_strategies_test.py` (extend existing, currently 1655 lines)
- Tests: instantiation with all param combos, strategy resolution priority,
fallback to isothermal when L=0, type errors for bad inputs
fallback to isothermal when L=0, logging for array/negative latent heat,
stub methods raise `NotImplementedError`

- [ ] **E5-F3-P2**: Implement `mass_transfer_rate()` and `rate()` with tests
- `mass_transfer_rate()` follows `CondensationIsothermal.mass_transfer_rate()`
Expand Down Expand Up @@ -620,3 +622,4 @@ class CondensationLatentHeat(CondensationStrategy):
|------|--------|--------|
| 2026-03-02 | Initial epic creation | ADW |
| 2026-03-02 | Split E5-F1-P3 into P3 (builders) + P4 (factory+exports); split E5-F3-P3 into P3 (particle-resolved step) + P4 (discrete+continuous) + P5 (data-only parity); added missing details: function signatures, file references, thermal conductivity source, vapor_pressure_surface parameter, test tolerances, literature targets | ADW |
| 2026-03-04 | Noted E5-F3-P1 issue #1139 and logging expectations | ADW |
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
# Feature E5-F3: CondensationLatentHeat Strategy Class

**Parent Epic**: [E5: Non-Isothermal Condensation with Latent Heat](../epics/E5-non-isothermal-condensation.md)
**Status**: Planning
**Status**: In Progress
**Priority**: P1
**Owners**: @Gorkowski
**Start Date**: TBD
**Start Date**: 2026-03-04
**Target Date**: TBD
**Last Updated**: 2026-03-02
**Last Updated**: 2026-03-04
**Size**: Large (5 phases)

## Summary
Expand Down Expand Up @@ -105,7 +105,7 @@ additions:
## Phase Checklist

- [ ] **E5-F3-P1**: Create `CondensationLatentHeat` class skeleton with tests
- Issue: TBD | Size: M (~80 LOC) | Status: Not Started
- Issue: #1139 | Size: M (~80 LOC) | Status: In Progress
- File: `particula/dynamics/condensation/condensation_strategies.py` (extend
existing file, currently 1699 lines)
- Extends `CondensationStrategy` (inherits all base methods including
Expand All @@ -118,11 +118,14 @@ additions:
1. If `latent_heat_strategy` provided -> use it directly
2. Else if `latent_heat > 0` -> wrap in `ConstantLatentHeat(latent_heat)`
3. Else (both zero/None) -> store None, behave as isothermal
4. If `latent_heat` is array-like, log warning and fall back to None
5. If `latent_heat` is negative, log warning and fall back to None
- Instance attribute: `last_latent_heat_energy: float = 0.0` (diagnostic)
- Tests: `particula/dynamics/condensation/tests/
condensation_strategies_test.py` (extend existing, currently 1655 lines)
- Tests: instantiation with all param combos, strategy resolution priority,
fallback to isothermal when L=0, type errors for bad inputs
fallback to isothermal when L=0, logging for array/negative latent heat,
stub methods raise `NotImplementedError`

- [ ] **E5-F3-P2**: Implement `mass_transfer_rate()` and `rate()` with tests
- Issue: TBD | Size: M (~100 LOC) | Status: Not Started
Expand Down Expand Up @@ -237,3 +240,4 @@ additions:
| Date | Change | Author |
|------|--------|--------|
| 2026-03-02 | Initial feature document created from E5 epic | ADW |
| 2026-03-04 | Marked P1 in progress for issue #1139 | ADW |
2 changes: 1 addition & 1 deletion adw-docs/dev-plans/features/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ work, typically ~100 LOC per phase, that deliver user-facing functionality.
|----|------|--------|----------|--------|
| E5-F1 | [Latent Heat Strategy Pattern](E5-F1-latent-heat-strategy.md) | In Progress | P1 | 4 |
| E5-F2 | [Non-Isothermal Mass Transfer Functions](E5-F2-non-isothermal-mass-transfer.md) | Planning | P1 | 3 |
| E5-F3 | [CondensationLatentHeat Strategy Class](E5-F3-condensation-latent-heat-strategy.md) | Planning | P1 | 5 |
| E5-F3 | [CondensationLatentHeat Strategy Class](E5-F3-condensation-latent-heat-strategy.md) | In Progress | P1 | 5 |
| E5-F4 | [Builder, Factory, and Exports](E5-F4-builder-factory-exports.md) | Planning | P1 | 2 |
| E5-F5 | [Validation and Integration Tests](E5-F5-validation-integration-tests.md) | Planning | P1 | 2 |
| E5-F6 | [Documentation and Examples](E5-F6-documentation-examples.md) | Planning | P2 | 3 |
Expand Down
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,9 @@ Whether you’re a researcher, educator, or industry expert, Particula is design
- **Building gas-phase properties** with builder/factory patterns (vapor
pressure and latent heat) that support unit-aware setters and exports.
- **Supporting non-isothermal condensation** with thermal resistance,
latent-heat mass transfer rate utilities, and latent-heat energy release
bookkeeping.
latent-heat mass transfer rate utilities, latent-heat energy release
bookkeeping, and the `CondensationLatentHeat` strategy scaffold
(latent-heat strategy resolution with a constant fallback).
- **Interrogating your experimental data** to validate and expand your impact.
- **Fostering open-source collaboration** to share ideas and build on each other’s work.

Expand Down
1 change: 1 addition & 0 deletions particula/dynamics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
from particula.dynamics.condensation.condensation_strategies import (
CondensationIsothermal,
CondensationIsothermalStaggered,
CondensationLatentHeat,
)
from particula.dynamics.condensation.condensation_builder.condensation_isothermal_builder import (
CondensationIsothermalBuilder,
Expand Down
2 changes: 2 additions & 0 deletions particula/dynamics/condensation/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,14 @@
from .condensation_strategies import (
CondensationIsothermal,
CondensationIsothermalStaggered,
CondensationLatentHeat,
CondensationStrategy,
)

__all__ = [
"CondensationIsothermal",
"CondensationIsothermalStaggered",
"CondensationLatentHeat",
"CondensationStrategy",
"CondensationIsothermalBuilder",
"CondensationIsothermalStaggeredBuilder",
Expand Down
191 changes: 191 additions & 0 deletions particula/dynamics/condensation/condensation_strategies.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,10 @@
)
from particula.gas import get_molecule_mean_free_path
from particula.gas.gas_data import GasData
from particula.gas.latent_heat_strategies import (
ConstantLatentHeat,
LatentHeatStrategy,
)
from particula.gas.species import GasSpecies
from particula.gas.vapor_pressure_strategies import VaporPressureStrategy
from particula.particles import (
Expand Down Expand Up @@ -1696,3 +1700,190 @@ def step( # noqa: C901
(particle, gas_species),
)
return cast(Tuple[ParticleData, GasData], (particle_data, gas_data))


class CondensationLatentHeat(CondensationStrategy):
"""Condensation strategy with latent heat configuration.

This class mirrors the base condensation setup while deferring the
non-isothermal mass-transfer implementation to later phases. It resolves
latent heat either from a provided strategy or a scalar fallback value.

Attributes:
latent_heat_strategy_input: Strategy input provided at initialization.
latent_heat_input: Raw latent heat value provided at initialization.
last_latent_heat_energy: Diagnostic latent heat energy tracker.
"""

# pylint: disable=R0913, R0917
def __init__(
self,
molar_mass: Union[float, NDArray[np.float64]],
diffusion_coefficient: Union[float, NDArray[np.float64]] = 2e-5,
accommodation_coefficient: Union[float, NDArray[np.float64]] = 1.0,
update_gases: bool = True,
skip_partitioning_indices: Optional[Sequence[int]] = None,
activity_strategy: ActivityStrategy | None = None,
surface_strategy: SurfaceStrategy | None = None,
vapor_pressure_strategy: VaporPressureStrategy
| Sequence[VaporPressureStrategy]
| None = None,
*,
latent_heat_strategy: LatentHeatStrategy | None = None,
latent_heat: float | NDArray[np.float64] = 0.0,
):
"""Initialize the CondensationLatentHeat strategy.

Args:
molar_mass: Molar mass of the species [kg/mol].
diffusion_coefficient: Diffusion coefficient [m^2/s].
accommodation_coefficient: Mass accommodation coefficient.
update_gases: Whether to update gas concentrations on update.
skip_partitioning_indices: Species indices that should skip
partitioning.
activity_strategy: Activity strategy used for ParticleData inputs.
surface_strategy: Surface strategy used for ParticleData inputs.
vapor_pressure_strategy: Vapor pressure strategy used for GasData
inputs.
latent_heat_strategy: Optional latent heat strategy to use.
latent_heat: Scalar latent heat fallback [J/kg].
"""
super().__init__(
molar_mass=molar_mass,
diffusion_coefficient=diffusion_coefficient,
accommodation_coefficient=accommodation_coefficient,
update_gases=update_gases,
skip_partitioning_indices=skip_partitioning_indices,
activity_strategy=activity_strategy,
surface_strategy=surface_strategy,
vapor_pressure_strategy=vapor_pressure_strategy,
)
self.latent_heat_strategy_input = latent_heat_strategy
self.latent_heat_input = latent_heat
self._latent_heat_strategy = self._resolve_latent_heat_strategy(
latent_heat_strategy=latent_heat_strategy,
latent_heat=latent_heat,
)
self.last_latent_heat_energy = 0.0

def _resolve_latent_heat_strategy(
self,
latent_heat_strategy: LatentHeatStrategy | None,
latent_heat: float | NDArray[np.float64],
) -> LatentHeatStrategy | None:
"""Resolve the latent heat strategy from inputs.

Prefers an explicit strategy, otherwise converts scalar latent heat
values into a constant strategy. Array-like or negative latent heat
values log a warning and fall back to isothermal behavior.

Args:
latent_heat_strategy: Optional strategy to use directly.
latent_heat: Scalar or array-like latent heat input [J/kg].

Returns:
Resolved latent heat strategy, or None for isothermal fallback.
"""
if latent_heat_strategy is not None:
return latent_heat_strategy

latent_heat_array = np.asarray(latent_heat, dtype=np.float64)
if latent_heat_array.shape == ():
latent_heat_value = float(latent_heat_array)
if latent_heat_value > 0:
return ConstantLatentHeat(latent_heat_value)
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use a keyword argument when constructing ConstantLatentHeat (e.g., latent_heat_ref=...) to match how this codebase instantiates it elsewhere and to make the units/meaning of the positional value unambiguous.

Suggested change
return ConstantLatentHeat(latent_heat_value)
return ConstantLatentHeat(latent_heat_ref=latent_heat_value)

Copilot uses AI. Check for mistakes.
if latent_heat_value < 0:
logger.warning(
"Negative latent_heat provided; falling back to "
"isothermal behavior."
)
return None

logger.warning(
"Array-like latent_heat provided; use a LatentHeatStrategy "
"for per-species values."
)
return None

def mass_transfer_rate(
self,
particle: ParticleRepresentation | ParticleData,
gas_species: GasSpecies | GasData,
temperature: float,
pressure: float,
dynamic_viscosity: Optional[float] = None,
) -> Union[float, NDArray[np.float64]]:
"""Return the mass transfer rate (stub).

Args:
particle: Particle representation providing radius and activity
information.
gas_species: Gas species supplying vapor properties and
concentrations.
temperature: System temperature in Kelvin.
pressure: System pressure in Pascals.
dynamic_viscosity: Optional dynamic viscosity override.

Returns:
Mass transfer rate per particle and per species in kg/s.

Raises:
NotImplementedError: Implemented in E5-F3-P2/P3.
"""
raise NotImplementedError("Implemented in E5-F3-P2/P3")

def rate(
self,
particle: ParticleRepresentation | ParticleData,
gas_species: GasSpecies | GasData,
temperature: float,
pressure: float,
dynamic_viscosity: Optional[float] = None,
) -> NDArray[np.float64]:
"""Return the condensation rate (stub).

Args:
particle: Particle representation providing radius and activity
information.
gas_species: Gas species supplying vapor properties and
concentrations.
temperature: System temperature in Kelvin.
pressure: System pressure in Pascals.
dynamic_viscosity: Optional dynamic viscosity override.

Returns:
Condensation rate per particle and per species in kg/s.

Raises:
NotImplementedError: Implemented in E5-F3-P2/P3.
"""
raise NotImplementedError("Implemented in E5-F3-P2/P3")

def step(
self,
particle: ParticleRepresentation | ParticleData,
gas_species: GasSpecies | GasData,
temperature: float,
pressure: float,
time_step: float,
dynamic_viscosity: Optional[float] = None,
) -> (
Tuple[ParticleRepresentation, GasSpecies] | Tuple[ParticleData, GasData]
):
Comment on lines +1835 to +1872
Copy link

Copilot AI Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

rate() and step() add a dynamic_viscosity parameter that is not part of the CondensationStrategy interface (and is not present on CondensationIsothermal / CondensationIsothermalStaggered). For API consistency across condensation strategies, either remove dynamic_viscosity from these method signatures or update the base ABC + other implementations in the same change set.

Copilot uses AI. Check for mistakes.
"""Advance one condensation step (stub).

Args:
particle: Particle representation to update.
gas_species: Gas species object providing vapor properties.
temperature: System temperature in Kelvin.
pressure: System pressure in Pascals.
time_step: Integration timestep in seconds.
dynamic_viscosity: Optional dynamic viscosity override.

Returns:
Tuple containing updated particle and gas species objects.

Raises:
NotImplementedError: Implemented in E5-F3-P2/P3.
"""
raise NotImplementedError("Implemented in E5-F3-P2/P3")
Loading