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
5 changes: 3 additions & 2 deletions docs/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,9 @@ Whether you’re a researcher, educator, or industry expert, Particula is design
- **Providing a Python-based API** for reproducible and modular simulations.
- **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 and
latent-heat mass transfer rate utilities.
- **Supporting non-isothermal condensation** with thermal resistance,
latent-heat mass transfer rate utilities, and latent-heat energy release
bookkeeping.
- **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 @@ -65,6 +65,7 @@
from particula.dynamics.condensation.mass_transfer import (
get_mass_transfer_rate,
get_mass_transfer_rate_latent_heat,
get_latent_heat_energy_released,
get_first_order_mass_transport_k,
get_thermal_resistance_factor,
get_radius_transfer_rate,
Expand Down
66 changes: 66 additions & 0 deletions particula/dynamics/condensation/mass_transfer.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,72 @@ def get_mass_transfer_rate_latent_heat(
return np.asarray(isothermal_rate / correction, dtype=np.float64)


@validate_inputs(
{
"mass_transfer": "finite",
"latent_heat": "nonnegative",
}
)
def get_latent_heat_energy_released(
mass_transfer: Union[float, NDArray[np.float64]],
latent_heat: Union[float, NDArray[np.float64]],
) -> Union[float, NDArray[np.float64]]:
"""Calculate latent heat energy released during phase change.

This diagnostic function converts the mass transferred in a time step into
energy released to (or absorbed from) the gas phase:

- Q = dm × L
- Q : Latent heat energy [J].
- dm : Mass transferred per step [kg].
- L : Latent heat of vaporization [J/kg].

Positive mass transfer (condensation) yields Q > 0, while negative mass
transfer (evaporation) yields Q < 0.

Args:
mass_transfer: Mass transferred per step [kg].
latent_heat: Latent heat of vaporization [J/kg].

Returns:
Latent heat energy released or absorbed [J].

Examples:
```py title="Condensation releases heat"
import particula as par

par.dynamics.get_latent_heat_energy_released(
mass_transfer=1.0e-15,
latent_heat=2.454e6,
)
# Output: 2.454e-09
```

```py title="Evaporation absorbs heat"
import particula as par

par.dynamics.get_latent_heat_energy_released(
mass_transfer=-1.0e-15,
latent_heat=2.454e6,
)
# Output: -2.454e-09
```

Raises:
ValueError: If mass_transfer is non-finite or latent_heat is negative.

Notes:
Large magnitudes may overflow following NumPy's float64 behavior.
"""
mass_transfer = np.asarray(mass_transfer, dtype=np.float64)
latent_heat = np.asarray(latent_heat, dtype=np.float64)
if not np.all(np.isfinite(latent_heat)):
raise ValueError(
"Argument 'latent_heat' must be finite (no inf or NaN)."
)
return np.asarray(mass_transfer * latent_heat, dtype=np.float64)


@validate_inputs(
{
"mass_rate": "finite",
Expand Down
124 changes: 124 additions & 0 deletions particula/dynamics/condensation/tests/mass_transfer_test.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
"""Test the Condensation module."""

import numpy as np
import particula as par
import pytest
from particula.dynamics.condensation.mass_transfer import (
get_first_order_mass_transport_k,
get_latent_heat_energy_released,
get_mass_transfer,
get_mass_transfer_of_multiple_species,
get_mass_transfer_of_single_species,
Expand Down Expand Up @@ -453,6 +455,128 @@ def test_mass_transfer_rate_latent_heat_validation_additional_inputs(
get_mass_transfer_rate_latent_heat(**kwargs)


def test_latent_heat_energy_released_precision():
"""Check latent heat energy calculation at high precision."""
mass_transfer = 1.5e-15
latent_heat = 2.454e6
expected = 3.681e-9
result = get_latent_heat_energy_released(
mass_transfer=mass_transfer,
latent_heat=latent_heat,
)
np.testing.assert_allclose(result, expected, rtol=1e-14)


def test_latent_heat_energy_released_sign_convention():
"""Positive mass transfer releases heat; negative absorbs heat."""
latent_heat = 2.454e6
positive_result = get_latent_heat_energy_released(
mass_transfer=1.0e-15,
latent_heat=latent_heat,
)
negative_result = get_latent_heat_energy_released(
mass_transfer=-1.0e-15,
latent_heat=latent_heat,
)
assert positive_result > 0.0
assert negative_result < 0.0


def test_latent_heat_energy_released_zero_latent_heat():
"""Zero latent heat returns exact zero."""
result = get_latent_heat_energy_released(
mass_transfer=1.0e-15,
latent_heat=0.0,
)
assert result == 0.0


def test_latent_heat_energy_released_zero_mass_transfer():
"""Zero mass transfer returns exact zero."""
result = get_latent_heat_energy_released(
mass_transfer=0.0,
latent_heat=2.454e6,
)
assert result == 0.0


def test_latent_heat_energy_released_array_shapes():
"""Broadcast scalar and array inputs and preserve shapes."""
mass_transfer_1d = np.array([1.0e-15, -2.0e-15])
latent_heat_scalar = 2.454e6
result_1d = get_latent_heat_energy_released(
mass_transfer=mass_transfer_1d,
latent_heat=latent_heat_scalar,
)
expected_1d = mass_transfer_1d * latent_heat_scalar
assert np.asarray(result_1d).shape == mass_transfer_1d.shape
np.testing.assert_allclose(result_1d, expected_1d, rtol=1e-14)

mass_transfer_2d = np.array([[1.0e-15, -1.0e-15], [2.0e-15, 3.0e-15]])
latent_heat_1d = np.array([2.454e6, 1.8e6])
result_2d = get_latent_heat_energy_released(
mass_transfer=mass_transfer_2d,
latent_heat=latent_heat_1d,
)
expected_2d = mass_transfer_2d * latent_heat_1d
assert np.asarray(result_2d).shape == mass_transfer_2d.shape
np.testing.assert_allclose(result_2d, expected_2d, rtol=1e-14)


def test_latent_heat_energy_released_list_inputs():
"""List inputs should coerce to arrays before multiplication."""
mass_transfer = [1.0e-15, -2.0e-15]
latent_heat = [2.454e6, 1.8e6]
result = get_latent_heat_energy_released(
mass_transfer=mass_transfer,
latent_heat=latent_heat,
)
expected = np.asarray(mass_transfer) * np.asarray(latent_heat)
np.testing.assert_allclose(result, expected, rtol=1e-14)


def test_latent_heat_energy_released_validation_negative_latent_heat():
"""Negative latent heat should raise validation error."""
with pytest.raises(ValueError, match="latent_heat"):
get_latent_heat_energy_released(
mass_transfer=1.0e-15,
latent_heat=-1.0,
)


@pytest.mark.parametrize("latent_heat", [np.nan, np.inf])
def test_latent_heat_energy_released_validation_nonfinite_latent_heat(
latent_heat,
):
"""Non-finite latent heat should raise validation error."""
with pytest.raises(ValueError, match="latent_heat"):
get_latent_heat_energy_released(
mass_transfer=1.0e-15,
latent_heat=latent_heat,
)


@pytest.mark.parametrize("mass_transfer", [np.nan, np.inf])
def test_latent_heat_energy_released_validation_nonfinite_mass_transfer(
mass_transfer,
):
"""Non-finite mass transfer should raise validation error."""
with pytest.raises(ValueError, match="mass_transfer"):
get_latent_heat_energy_released(
mass_transfer=mass_transfer,
latent_heat=2.454e6,
)


def test_latent_heat_energy_released_public_reexport():
"""Public API should re-export the latent heat helper."""
result = par.dynamics.get_latent_heat_energy_released(
mass_transfer=1.0e-15,
latent_heat=2.454e6,
)
assert result == pytest.approx(2.454e-9)


def test_multi_species_mass_transfer_rate():
"""Test the mass_transfer_rate function for multiple species."""
pressure_delta = np.array([10.0, 15.0])
Expand Down
5 changes: 3 additions & 2 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,8 +94,9 @@ particula/
- **Builder Pattern** — Clean, validated object construction with unit conversion
- **Composable Processes** — Chain runnables with `|` operator
- **Condensation Utilities** — Non-isothermal helpers via
`particula.dynamics.get_thermal_resistance_factor` and
`particula.dynamics.get_mass_transfer_rate_latent_heat`
`particula.dynamics.get_thermal_resistance_factor`,
`particula.dynamics.get_mass_transfer_rate_latent_heat`, and
`particula.dynamics.get_latent_heat_energy_released`
- **Latent Heat Factories** — Build constant, linear, and power-law latent heat
strategies via `particula.gas.LatentHeatFactory` with unit-aware builders and
gas-phase exports for upcoming non-isothermal workflows
Expand Down