Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Pre-Condensation PR gas properties #453

Merged
merged 9 commits into from
May 24, 2024
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
8 changes: 6 additions & 2 deletions particula/logger_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,13 @@
}
},
"loggers": {
"root": {
"particula": {
"level": "DEBUG",
"handlers": ["file", "stderr"],
"propagate": False
},
"root": {
"level": "ERROR",
"handlers": [
"stderr",
"file"
Expand All @@ -61,6 +66,5 @@ def setup():
"""Setup for logging in the particula package."""
# check for logging directory
os.makedirs(log_dir, exist_ok=True)
# configure the logger
logging.config.dictConfig(config)
return logger
11 changes: 11 additions & 0 deletions particula/next/gas/properties/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""Import all the property functions, so they can be accessed from
particula.next.gas.properties.
"""

# pylint: disable=unused-import
# flake8: noqa
# pyright: basic

from particula.next.gas.properties.mean_free_path import molecule_mean_free_path
from particula.next.gas.properties.dynamic_viscosity import get_dynamic_viscosity
from particula.next.gas.properties.thermal_conductivity import get_thermal_conductivity
59 changes: 59 additions & 0 deletions particula/next/gas/properties/dynamic_viscosity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
""" Module for calculating the dynamic viscosity

The dynamic viscosity is calculated using the Sutherland formula,
assuming ideal gas behavior, as a function of temperature.

"The dynamic viscosity equals the product of the sum of
Sutherland's constant and the reference temperature divided by
the sum of Sutherland's constant and the temperature,
the reference viscosity and the ratio to the 3/2 power
of the temperature to reference temperature."

https://resources.wolframcloud.com/FormulaRepository/resources/Sutherlands-Formula
"""

import logging
from particula.constants import (REF_TEMPERATURE_STP, REF_VISCOSITY_AIR_STP,
SUTHERLAND_CONSTANT)

logger = logging.getLogger("particula") # get instance of logger


def get_dynamic_viscosity(
temperature: float,
reference_viscosity: float = REF_VISCOSITY_AIR_STP.m,
reference_temperature: float = REF_TEMPERATURE_STP.m
) -> float:
"""
Calculates the dynamic viscosity of air via Sutherland's formula, which is
a common method in fluid dynamics for gases that involves temperature
adjustments.

Args:
-----
- temperature: Desired air temperature [K]. Must be greater than 0.
- reference_viscosity: Gas viscosity [Pa*s] at the reference temperature
(default is STP).
- reference_temperature: Gas temperature [K] for the reference viscosity
(default is STP).

Returns:
--------
- float: The dynamic viscosity of air at the given temperature [Pa*s].

Raises:
------
- ValueError: If the temperature is less than or equal to 0.

References:
----------
https://resources.wolframcloud.com/FormulaRepository/resources/Sutherlands-Formula
"""
if temperature <= 0:
logger.error("Temperature must be greater than 0 Kelvin.")
raise ValueError("Temperature must be greater than 0 Kelvin.")
ngmahfouz marked this conversation as resolved.
Show resolved Hide resolved
return (
reference_viscosity * (temperature / reference_temperature)**1.5 *
(reference_temperature + SUTHERLAND_CONSTANT.m) /
(temperature + SUTHERLAND_CONSTANT.m)
)
71 changes: 71 additions & 0 deletions particula/next/gas/properties/mean_free_path.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
""" calculating the mean free path of air

The mean free path is the average distance
traveled by a molecule between collisions
with other molecules present in a medium (air).

The expeected mean free path of air is approx.
65 nm at 298 K and 101325 Pa.

"""

import logging
from typing import Union, Optional
from numpy.typing import NDArray
import numpy as np
from particula.constants import (
GAS_CONSTANT, MOLECULAR_WEIGHT_AIR) # type: ignore
from particula.next.gas.properties.dynamic_viscosity import (
get_dynamic_viscosity)

logger = logging.getLogger("particula") # get instance of logger


def molecule_mean_free_path(
molar_mass: Union[
float, NDArray[np.float_]] = MOLECULAR_WEIGHT_AIR.m, # type: ignore
temperature: float = 298.15,
pressure: float = 101325,
dynamic_viscosity: Optional[float] = None,
) -> Union[float, NDArray[np.float_]]:
"""
Calculate the mean free path of a gas molecule in air based on the
temperature, pressure, and molar mass of the gas. The mean free path
is the average distance traveled by a molecule between collisions with
other molecules present in a medium (air).

Args:
-----
- molar_mass (Union[float, NDArray[np.float_]]): The molar mass
of the gas molecule [kg/mol]. Default is the molecular weight of air.
- temperature (float): The temperature of the gas [K]. Default is 298.15 K.
- pressure (float): The pressure of the gas [Pa]. Default is 101325 Pa.
- dynamic_viscosity (Optional[float]): The dynamic viscosity of the gas
[Pa*s]. If not provided, it will be calculated based on the temperature.

Returns:
--------
- Union[float, NDArray[np.float_]]: The mean free path of the gas molecule
in meters (m).

References:
----------
- https://en.wikipedia.org/wiki/Mean_free_path
"""
# check inputs are positive
if temperature <= 0:
logger.error("Temperature must be positive [Kelvin]")
raise ValueError("Temperature must be positive [Kelvin]")
if pressure <= 0:
logger.error("Pressure must be positive [Pascal]")
raise ValueError("Pressure must be positive [Pascal]")
if np.any(molar_mass <= 0):
logger.error("Molar mass must be positive [kg/mol]")
raise ValueError("Molar mass must be positive [kg/mol]")
if dynamic_viscosity is None:
dynamic_viscosity = get_dynamic_viscosity(temperature)

return np.array(
(2 * dynamic_viscosity / pressure)
/ (8 * molar_mass / (np.pi * GAS_CONSTANT.m * temperature))**0.5,
dtype=np.float_)
40 changes: 40 additions & 0 deletions particula/next/gas/properties/tests/prop_dynamic_viscosity_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""Test dynamic viscosity property functions."""

import pytest
from particula.next.gas.properties import get_dynamic_viscosity


def test_dynamic_viscosity_normal_conditions():
"""Test dynamic viscosity under normal conditions."""
assert pytest.approx(get_dynamic_viscosity(
300), 1e-5) == 1.8459162511975804e-5, "Failed under normal conditions"


def test_dynamic_viscosity_high_temperature():
"""Test dynamic viscosity at high temperature."""
assert pytest.approx(get_dynamic_viscosity(
1000), 1e-5) == 4.1520063611410934e-05, "Failed at high temperature"


def test_dynamic_viscosity_low_temperature():
"""Test dynamic viscosity at low temperature."""
assert pytest.approx(get_dynamic_viscosity(
250), 1e-5) == 1.599052394e-5, "Failed at low temperature"


def test_dynamic_viscosity_reference_values():
"""Test dynamic viscosity with reference values."""
assert pytest.approx(get_dynamic_viscosity(300, 1.85e-5, 300),
1e-5) == 1.85e-5, "Failed with reference values"


def test_dynamic_viscosity_zero_temperature():
"""Test for error handling with zero temperature."""
with pytest.raises(ValueError):
get_dynamic_viscosity(0)


def test_dynamic_viscosity_negative_temperature():
"""Test for error handling with negative temperature."""
with pytest.raises(ValueError):
get_dynamic_viscosity(-10)
54 changes: 54 additions & 0 deletions particula/next/gas/properties/tests/prop_mean_free_path_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
""" testing the mean free path calculation
"""

import pytest
import numpy as np
from particula.next.gas.properties import molecule_mean_free_path


def test_molecule_mean_free_path():
""" Testing the mean free path of a molecule compare with mfp"""

a_mfp = 6.52805868e-08 # at stp
b_molecule_mfp = molecule_mean_free_path(
temperature=298, pressure=101325, molar_mass=0.03
)
assert pytest.approx(a_mfp, rel=1e-6) == b_molecule_mfp


def test_dynamic_viscosity_provided():
""" Test when dynamic viscosity is explicitly provided"""
dynamic_viscosity = 5*1.78e-5 # 5x Value for air at stp
result = molecule_mean_free_path(dynamic_viscosity=dynamic_viscosity)
assert result > 0


def test_array_input():
"""Test when array inputs are provided for temperature, pressure,
and molar mass"""
molar_masses = np.array([0.028, 0.044])
results = molecule_mean_free_path(molar_mass=molar_masses)
# All calculated mean free paths should be positive
assert all(results > 0)


@pytest.mark.parametrize("temperature", [None, -1, 'a'])
def test_invalid_temperature(temperature):
"""Test when invalid temperature values are provided to the function"""
with pytest.raises((TypeError, ValueError)):
molecule_mean_free_path(temperature=temperature)


@pytest.mark.parametrize("pressure", [None, -1, 'a'])
def test_invalid_pressure(pressure):
"""Test when invalid pressure values are provided to the function"""
with pytest.raises((TypeError, ValueError)):
molecule_mean_free_path(pressure=pressure)


@pytest.mark.parametrize("molar_mass",
[None, -1, 'a', np.array([0.028, -0.044])])
def test_invalid_molar_mass(molar_mass):
"""Test when invalid molar mass values are provided to the function"""
with pytest.raises((TypeError, ValueError)):
molecule_mean_free_path(molar_mass=molar_mass)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""Test thermal conductivity property functions."""

import pytest
import numpy as np
from particula.next.gas.properties import get_thermal_conductivity


def test_thermal_conductivity_normal():
"""Test the thermal_conductivity function with a
normal temperature value."""
temperature = 300 # in Kelvin
expected = 1e-3 * (4.39 + 0.071 * 300)
assert pytest.approx(get_thermal_conductivity(temperature)
) == expected, "Failed at normal temperature"


def test_thermal_conductivity_array():
"""Test the thermal_conductivity function with an array of temperatures."""
temperatures = np.array([250, 300, 350])
expected = 1e-3 * (4.39 + 0.071 * temperatures)
result = get_thermal_conductivity(temperatures)
assert np.allclose(result, expected), "Failed with temperature array"


def test_thermal_conductivity_below_absolute_zero():
"""Test for error handling with temperature below absolute zero."""
with pytest.raises(ValueError):
get_thermal_conductivity(-1)


def test_thermal_conductivity_edge_case_zero():
"""Test the thermal_conductivity function at absolute zero."""
temperature = 0
expected = 1e-3 * (4.39 + 0.071 * 0)
assert pytest.approx(get_thermal_conductivity(temperature)
) == expected, "Failed at absolute zero"
42 changes: 42 additions & 0 deletions particula/next/gas/properties/thermal_conductivity.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
"""Thermal Conductivity of air."""

import logging
from typing import Union
from numpy.typing import NDArray
import numpy as np

logger = logging.getLogger("particula") # get instance of logger


def get_thermal_conductivity(
temperature: Union[float, NDArray[np.float_]]
) -> Union[float, NDArray[np.float_]]:
"""
Calculate the thermal conductivity of air as a function of temperature.
Based on a simplified linear relation from atmospheric science literature.
Only valid for temperatures within the range typically found on
Earth's surface.

Args:
-----
- temperature (Union[float, NDArray[np.float_]]): The temperature at which
the thermal conductivity of air is to be calculated, in Kelvin (K).

Returns:
--------
- Union[float, NDArray[np.float_]]: The thermal conductivity of air at the
specified temperature in Watts per meter-Kelvin (W/m·K) or J/(m s K).

Raises:
------
- ValueError: If the temperature is below absolute zero (0 K).

References:
----------
- Seinfeld and Pandis, "Atmospheric Chemistry and Physics", Equation 17.54.
"""
if np.any(temperature < 0):
logger.error("Temperature must be greater than or equal to 0 Kelvin.")
raise ValueError(
"Temperature must be greater than or equal to 0 Kelvin.")
return 1e-3 * (4.39 + 0.071 * temperature)
Loading