Repository: uncscode/particula

Issue:
Add function: gravitational settling kernel
Adding in just the function call for the graviational settling kernel.

A seperate issue is the class interface integration.

<!-- Edit the body of your new issue then click the ✓ "Create Issue" button in the top right of the editor. The first line will be the issue title. Assignees and Labels follow after a blank line. Leave an empty line before beginning the body of the issue. -->

Discussion:
sourcery-ai[bot]: <!-- Generated by sourcery-ai[bot]: start review_guide -->

🧙 Sourcery is reviewing your pull request!

---

<details>
<summary>Tips and commands</summary>

#### Interacting with Sourcery

- **Trigger a new review:** Comment `@sourcery-ai review` on the pull request.
- **Continue discussions:** Reply directly to Sourcery's review comments.
- **Generate a GitHub issue from a review comment:** Ask Sourcery to create an
  issue from a review comment by replying to it.
- **Generate a pull request title:** Write `@sourcery-ai` anywhere in the pull
  request title to generate a title at any time.
- **Generate a pull request summary:** Write `@sourcery-ai summary` anywhere in
  the pull request body to generate a PR summary at any time. You can also use
  this command to specify where the summary should be inserted.

#### Customizing Your Experience

Access your [dashboard](https://app.sourcery.ai) to:
- Enable or disable review features such as the Sourcery-generated pull request
  summary, the reviewer's guide, and others.
- Change the review language.
- Add, remove or edit custom review instructions.
- Adjust other review settings.

#### Getting Help

- [Contact our support team](mailto:support@sourcery.ai) for questions or feedback.
- Visit our [documentation](https://docs.sourcery.ai) for detailed guides and information.
- Keep in touch with the Sourcery team by following us on [X/Twitter](https://x.com/SourceryAI), [LinkedIn](https://www.linkedin.com/company/sourcery-ai/) or [GitHub](https://github.com/sourcery-ai).

</details>

<!-- Generated by sourcery-ai[bot]: end review_guide -->
github-actions[bot]: [PR Preview Action](https://github.com/rossjrw/pr-preview-action) v1.4.8
:---:
:rocket: Deployed preview to https://uncscode.github.io/particula/pr-preview/pr-565/
on branch [`gh-pages`](https://github.com/uncscode/particula/tree/gh-pages) at 2025-01-05 00:58 UTC
<!-- Sticky Pull Request Commentpr-preview -->

Patch:
diff --git a/particula/dynamics/coagulation/sedimentation_kernel.py b/particula/dynamics/coagulation/sedimentation_kernel.py
new file mode 100644
index 000000000..515fe99e5
--- /dev/null
+++ b/particula/dynamics/coagulation/sedimentation_kernel.py
@@ -0,0 +1,124 @@
+"""
+Sedimentation kernel for aerosol particles.
+
+Seinfeld and Pandis (2016), Chapter 13, Equation 13A.4.
+"""
+
+from numpy.typing import NDArray
+import numpy as np
+
+from particula.particles.properties.settling_velocity import (
+    particle_settling_velocity_via_system_state,
+)
+from particula.util.validate_inputs import validate_inputs
+
+
+def calculate_collision_efficiency_function(
+    radius1: float, radius2: float
+) -> float:
+    """
+    Placeholder function to calculate collision efficiency.
+
+    Args:
+    -----
+        - radius1 : Radius of the first particle [m].
+        - radius2 : Radius of the second particle [m].
+
+    Returns:
+    --------
+        - Collision efficiency [dimensionless].
+    """
+    # Implement the actual collision efficiency calculation here
+    raise NotImplementedError(
+        "Collision efficiency calculation not implemented yet."
+    )
+
+
+def sedimentation_sp2016(
+    radius_particle: NDArray[np.float64],
+    settling_velocities: NDArray[np.float64],
+    calculate_collision_efficiency: bool = True,
+) -> NDArray[np.float64]:
+    """
+    Calculate the sedimentation kernel for aerosol particles.
+
+    Coagulation occurring due to gravitational settling when heavier particles
+    settle faster than lighter ones, catching up and colliding with them. The
+    coagulation coefficient is proportional to the product of the target area,
+    the relative distance swept by the larger particle per unit time, and the
+    collision efficiency
+
+    Args:
+    -----
+        - radius_particle : Array of particle radii [m].
+        - settling_velocities : Array of particle settling velocities [m/s].
+        - calculate_collision_efficiency : Boolean to calculate collision
+            efficiency or use 1.
+
+    Returns:
+    --------
+        - Sedimentation kernel matrix for aerosol particles [m^3/s].
+
+    References:
+    - Seinfeld, J. H., & Pandis, S. N. (2016). Atmospheric chemistry and
+        physics, Chapter 13, Equation 13A.4.
+    """
+    diameter_matrix = 2 * (
+        radius_particle[:, np.newaxis] + radius_particle[np.newaxis, :]
+    )
+    velocity_diff_matrix = np.abs(
+        settling_velocities[:, np.newaxis] - settling_velocities[np.newaxis, :]
+    )
+
+    if calculate_collision_efficiency:
+        collision_efficiency_matrix = np.vectorize(
+            calculate_collision_efficiency_function
+        )(radius_particle[:, np.newaxis], radius_particle[np.newaxis, :])
+    else:
+        collision_efficiency_matrix = np.ones_like(diameter_matrix)
+
+    return (
+        np.pi
+        / 4
+        * diameter_matrix**2
+        * velocity_diff_matrix
+        * collision_efficiency_matrix
+    )
+
+
+@validate_inputs({"temperature": "positive", "pressure": "positive"})
+def sedimentation_sp2016_via_system_state(
+    radius_particle: NDArray[np.float64],
+    density_particle: NDArray[np.float64],
+    temperature: float,
+    pressure: float,
+    calculate_collision_efficiency: bool = True,
+) -> NDArray[np.float64]:
+    """
+    Calculate the sedimentation kernel for aerosol particles via system state.
+
+    Args:
+    -----
+        - radius_particle : Array of particle radii [m].
+        - density_particle : Array of particle densities [kg/m³].
+        - temperature : Temperature of the system [K].
+        - pressure : Pressure of the system [Pa].
+        - calculate_collision_efficiency : Boolean to calculate collision
+            efficiency or use 1.
+
+    Returns:
+    --------
+        - Sedimentation kernel matrix for aerosol particles [m^3/s].
+    """
+    settling_velocities = particle_settling_velocity_via_system_state(
+        particle_radius=radius_particle,
+        particle_density=density_particle,
+        temperature=temperature,
+        pressure=pressure,
+    )
+
+    return sedimentation_sp2016(
+        radius_particle=radius_particle,
+        settling_velocities=settling_velocities,
+        calculate_collision_efficiency=calculate_collision_efficiency,
+    )
diff --git a/particula/dynamics/coagulation/turbulent_shear_kernel.py b/particula/dynamics/coagulation/turbulent_shear_kernel.py
new file mode 100644
index 000000000..dfba7a22b
--- /dev/null
+++ b/particula/dynamics/coagulation/turbulent_shear_kernel.py
@@ -0,0 +1,98 @@
+"""
+Turbulent shear kernel for coagulation.
+
+Seinfeld and Pandis (2016), Chapter 13, Equation 13A.2.
+K(D1, D2) = (pi * e_k / 120 * v)**1/2 * (D1 + D2)**3
+
+Where:
+    - K(D1, D2) : Turbulent shear kernel for coagulation [m^3/s].
+    - e_k : Turbulent kinetic energy [m^2/s^2].
+    - v : Kinematic viscosity [m^2/s].
+    - D1, D2 : Particle diameters [m].
+
+Saffman, P. G., & Turner, J. S. (1956). On the collision of drops in turbulent
+clouds. Journal of Fluid Mechanics, 1(1), 16-30.
+https://doi.org/10.1017/S0022112056000020
+"""
+
+from numpy.typing import NDArray
+import numpy as np
+
+from particula.gas.properties.kinematic_viscosity import (
+    get_kinematic_viscosity_via_system_state,
+)
+
+
+def saffman_turner_1956(
+    diameter_particle: NDArray[np.float64],
+    turbulent_kinetic_energy: float,
+    kinematic_viscosity: float,
+) -> NDArray[np.float64]:
+    """
+    Calculate the turbulent shear kernel for coagulation.
+
+    Args:
+    -----
+        - diameter_particle : Array of particle diameters [m].
+        - turbulent_kinetic_energy : Turbulent kinetic energy [m^2/s^2].
+        - kinematic_viscosity : Kinematic viscosity [m^2/s].
+
+    Returns:
+    --------
+        - Turbulent shear kernel matrix for coagulation [m^3/s].
+
+    References:
+    ----------
+    - Equation 13A.2 : K(D1, D2) = (pi * e_k / 120 * v)**1/2 * (D1 + D2)**3
+        - K(D1, D2) : Turbulent shear kernel for coagulation [m^3/s].
+        - e_k : Turbulent kinetic energy [m^2/s^2].
+        - v : Kinematic viscosity [m^2/s].
+        - D1, D2 : Particle diameters [m].
+    - Saffman, P. G., & Turner, J. S. (1956). On the collision of drops in
+        turbulent clouds. Journal of Fluid Mechanics, 1(1), 16-30.
+        https://doi.org/10.1017/S0022112056000020
+    - Seinfeld, J. H., & Pandis, S. N. (2016). Atmospheric chemistry and
+        physics, Chapter 13, Equation 13A.2.
+    """
+    diameter_sum_matrix = (
+        diameter_particle[:, np.newaxis] + diameter_particle[np.newaxis, :]
+    )
+    return (
+        np.pi * turbulent_kinetic_energy / (120 * kinematic_viscosity)
+    ) ** 0.5 * diameter_sum_matrix**3
+
+
+def saffman_turner_1956_via_system_state(
+    diameter_particle: NDArray[np.float64],
+    turbulent_kinetic_energy: float,
+    temperature: float,
+    fluid_density: float,
+) -> NDArray[np.float64]:
+    """
+    Calculate the turbulent shear kernel for coagulation via system state.
+
+    Args:
+    -----
+        - diameter_particle : Array of particle diameters [m].
+        - turbulent_kinetic_energy : Turbulent kinetic energy [m^2/s^2].
+        - temperature : Temperature of the system [K].
+        - fluid_density : Density of the fluid [kg/m^3].
+
+    Returns:
+    --------
+        - Turbulent shear kernel matrix for coagulation [m^3/s].
+
+    References:
+    ----------
+    - Saffman, P. G., & Turner, J. S. (1956). On the collision of drops in
+        turbulent clouds. Journal of Fluid Mechanics, 1(1), 16-30.
+        https://doi.org/10.1017/S0022112056000020
+    """
+    kinematic_viscosity = get_kinematic_viscosity_via_system_state(
+        temperature=temperature, fluid_density=fluid_density
+    )
+    return saffman_turner_1956(
+        diameter_particle=diameter_particle,
+        turbulent_kinetic_energy=turbulent_kinetic_energy,
+        kinematic_viscosity=kinematic_viscosity,
+    )
diff --git a/particula/gas/properties/kinematic_viscosity.py b/particula/gas/properties/kinematic_viscosity.py
new file mode 100644
index 000000000..215dcf0f9
--- /dev/null
+++ b/particula/gas/properties/kinematic_viscosity.py
@@ -0,0 +1,97 @@
+"""
+Kinematic viscosity for fluids.
+
+The kinematic viscosity is the ratio of the dynamic viscosity to the
+density of a fluid. It is a measure of the fluid's resistance to flow
+under the influence of gravity.
+
+v = mu / rho
+
+Where:
+    - v : Kinematic viscosity [m^2/s].
+    - mu : Dynamic viscosity [Pa*s].
+    - rho : Density of the fluid [kg/m^3].
+
+References:
+https://resources.wolframcloud.com/FormulaRepository/resources/Viscosity-Conversion-Formula
+https://en.wikipedia.org/wiki/Viscosity#Kinematic_viscosity
+"""
+
+from particula.util.validate_inputs import validate_inputs
+from particula.gas.properties.dynamic_viscosity import get_dynamic_viscosity
+from particula.util.constants import (REF_TEMPERATURE_STP,
+                                      REF_VISCOSITY_AIR_STP,)
+
+
+def get_kinematic_viscosity(
+    dynamic_viscosity: float,
+    fluid_density: float,
+) -> float:
+    """
+    Calculate the kinematic viscosity of a fluid.
+
+    The kinematic viscosity is the ratio of the dynamic viscosity to the
+    density of a fluid. It is a measure of the fluid's resistance to flow
+    under the influence of gravity.
+
+    Args:
+    -----
+        - dynamic_viscosity : Dynamic viscosity of the fluid [Pa*s].
+        - fluid_density : Density of the fluid [kg/m³].
+
+    Returns:
+    --------
+        - Kinematic viscosity of the fluid [m^2/s].
+
+    References:
+    ----------
+    - Equation : v = mu / rho
+            - v : Kinematic viscosity [m^2/s].
+            - mu : Dynamic viscosity [Pa*s].
+            - rho : Density of the fluid [kg/m^3].
+    https://resources.wolframcloud.com/FormulaRepository/resources/Viscosity-Conversion-Formula
+    """
+    return dynamic_viscosity / fluid_density
+
+
+@validate_inputs({"temperature": "positive"})
+def get_kinematic_viscosity_via_system_state(
+    temperature: float,
+    fluid_density: float,
+    reference_viscosity: float = REF_VISCOSITY_AIR_STP,
+    reference_temperature: float = REF_TEMPERATURE_STP
+) -> float:
+    """
+    Calculate the kinematic viscosity of air by calculating dynamic viscosity
+    of air.
+
+    Args:
+    -----
+    - temperature: Desired air temperature [K]. Must be greater than 0.
+    - fluid_density: Density of the fluid [kg/m³].
+    - 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 kinematic viscosity of air at the given temperature [m^2/s].
+
+    Raises:
+    ------
+    - ValueError: If the temperature is less than or equal to 0.
+
+    References:
+    ----------
+    https://resources.wolframcloud.com/FormulaRepository/resources/Sutherlands-Formula
+    """
+    dynamic_viscosity = get_dynamic_viscosity(
+        temperature=temperature,
+        reference_viscosity=reference_viscosity,
+        reference_temperature=reference_temperature
+    )
+    return get_kinematic_viscosity(
+        dynamic_viscosity=dynamic_viscosity,
+        fluid_density=fluid_density
+        )
diff --git a/particula/gas/properties/tests/prop_mean_free_path_test.py b/particula/gas/properties/tests/mean_free_path_test.py
similarity index 100%
rename from particula/gas/properties/tests/prop_mean_free_path_test.py
rename to particula/gas/properties/tests/mean_free_path_test.py
diff --git a/particula/gas/properties/tests/prop_thermal_conductivity_test.py b/particula/gas/properties/tests/thermal_conductivity_test.py
similarity index 100%
rename from particula/gas/properties/tests/prop_thermal_conductivity_test.py
rename to particula/gas/properties/tests/thermal_conductivity_test.py
diff --git a/particula/util/validate_inputs.py b/particula/util/validate_inputs.py
index f8ecb52af..7d34a2e30 100644
--- a/particula/util/validate_inputs.py
+++ b/particula/util/validate_inputs.py
@@ -42,7 +42,11 @@ def validate_inputs(dict_args):
     A decorator to validate that specified arguments meet certain constraints.
 
     Arguments:
-        dict_args: Dictionary of argument names and their constraints.
+        - dict_args : Dictionary of argument names and their constraints.
+            {"arg1": "positive", "arg2": "negative", ...}
+            - Options: "positive", "negative", "nonpositive", "nonnegative",
+                "nonzero".
+
 
     Returns:
         A decorator for input validation.


Test patch:
diff --git a/particula/dynamics/coagulation/tests/sedimentation_kernel_test.py b/particula/dynamics/coagulation/tests/sedimentation_kernel_test.py
new file mode 100644
index 000000000..798b86f58
--- /dev/null
+++ b/particula/dynamics/coagulation/tests/sedimentation_kernel_test.py
@@ -0,0 +1,57 @@
+"""
+Test the sedimentation kernel functions.
+"""
+
+import numpy as np
+from particula.dynamics.coagulation.sedimentation_kernel import (
+    sedimentation_sp2016,
+    sedimentation_sp2016_via_system_state,
+)
+
+
+def test_sedimentation_kernel():
+    """Test just the sedimentation kernel."""
+    radius_particle = np.array([1e-6, 2e-6, 3e-6, 4e-6])
+    settling_velocities = np.array([0.01, 0.02, 0.03, 0.04])
+    expected_kernel = np.array(
+        [
+            [0.0, 1.25663706e-13, 2.51327412e-13, 3.76991118e-13],
+            [1.25663706e-13, 0.0, 1.25663706e-13, 2.51327412e-13],
+            [2.51327412e-13, 1.25663706e-13, 0.0, 1.25663706e-13],
+            [3.76991118e-13, 2.51327412e-13, 1.25663706e-13, 0.0],
+        ]
+    )
+
+    kernel = sedimentation_sp2016(
+        radius_particle=radius_particle,
+        settling_velocities=settling_velocities,
+        calculate_collision_efficiency=False,
+    )
+
+    assert np.allclose(kernel, expected_kernel, rtol=1e-6)
+
+
+def test_sedimentation_kernel_via_system_state():
+    """Test the sedimentation kernel via system state."""
+    radius_particle = np.array([1e-6, 2e-6, 3e-6, 4e-6])
+    density_particle = np.array([1000, 1000, 1000, 1000])
+    temperature = 298.15
+    pressure = 101325
+    expected_kernel = np.array(
+        [
+            [0.0, 1.25663706e-13, 2.51327412e-13, 3.76991118e-13],
+            [1.25663706e-13, 0.0, 1.25663706e-13, 2.51327412e-13],
+            [2.51327412e-13, 1.25663706e-13, 0.0, 1.25663706e-13],
+            [3.76991118e-13, 2.51327412e-13, 1.25663706e-13, 0.0],
+        ]
+    )
+
+    kernel = sedimentation_sp2016_via_system_state(
+        radius_particle=radius_particle,
+        density_particle=density_particle,
+        temperature=temperature,
+        pressure=pressure,
+        calculate_collision_efficiency=False,
+    )
+
+    assert np.allclose(kernel, expected_kernel, rtol=1e-6)
diff --git a/particula/dynamics/coagulation/tests/turbulent_shear_kernel_test.py b/particula/dynamics/coagulation/tests/turbulent_shear_kernel_test.py
new file mode 100644
index 000000000..5b35f66a4
--- /dev/null
+++ b/particula/dynamics/coagulation/tests/turbulent_shear_kernel_test.py
@@ -0,0 +1,76 @@
+"""
+Test the turbulent shear kernel functions.
+"""
+
+import numpy as np
+import pytest
+from particula.dynamics.coagulation.turbulent_shear_kernel import (
+    saffman_turner_1956,
+    saffman_turner_1956_via_system_state,
+)
+from particula.gas.properties.kinematic_viscosity import (
+    get_kinematic_viscosity_via_system_state,
+)
+
+
+def test_turbulent_shear_kernel_single_value():
+    """
+    Test turbulent_shear_kernel with single float inputs for particle
+    diameters, turbulent kinetic energy, and kinematic viscosity.
+    """
+    diameter_particle = np.array([1e-6, 2e-6])  # example diameters [m]
+    turbulent_kinetic_energy = (
+        1.0e-4  # example turbulent kinetic energy [m^2/s^2]
+    )
+    kinematic_viscosity = 1.5e-5  # example kinematic viscosity [m^2/s]
+    expected_kernel = (
+        np.pi * turbulent_kinetic_energy / (120 * kinematic_viscosity)
+    ) ** 0.5 * (
+        diameter_particle[:, np.newaxis] + diameter_particle[np.newaxis, :]
+    ) ** 3
+    value = saffman_turner_1956(
+        diameter_particle, turbulent_kinetic_energy, kinematic_viscosity
+    )
+    np.testing.assert_allclose(value, expected_kernel, rtol=1e-6)
+
+
+def test_turbulent_shear_kernel_via_system_state():
+    """
+    Test turbulent_shear_kernel_via_system_state with system state inputs.
+    """
+    diameter_particle = np.array([1e-6, 2e-6])  # example diameters [m]
+    turbulent_kinetic_energy = (
+        1.0e-4  # example turbulent kinetic energy [m^2/s^2]
+    )
+    temperature = 298  # example temperature [K]
+    fluid_density = 1.2  # example fluid density [kg/m^3]
+    kinematic_viscosity = get_kinematic_viscosity_via_system_state(
+        temperature=temperature, fluid_density=fluid_density
+    )
+    expected_kernel = saffman_turner_1956(
+        diameter_particle, turbulent_kinetic_energy, kinematic_viscosity
+    )
+    value = saffman_turner_1956_via_system_state(
+        diameter_particle, turbulent_kinetic_energy, temperature, fluid_density
+    )
+    np.testing.assert_allclose(value, expected_kernel, rtol=1e-6)
+
+
+def test_turbulent_shear_kernel_input_validation():
+    """
+    Ensure that providing incorrect input types to turbulent_shear_kernel
+    raises a TypeError.
+    """
+    with pytest.raises(TypeError):
+        saffman_turner_1956("not a number", "not a number", "not a number")
+
+
+def test_turbulent_shear_kernel_via_system_state_input_validation():
+    """
+    Ensure that providing incorrect input types to
+    turbulent_shear_kernel_via_system_state raises a TypeError.
+    """
+    with pytest.raises(TypeError):
+        saffman_turner_1956_via_system_state(
+            "not a number", "not a number", "not a number", "not a number"
+        )
diff --git a/particula/gas/properties/tests/prop_dynamic_viscosity_test.py b/particula/gas/properties/tests/dynamic_viscosity_test.py
similarity index 100%
rename from particula/gas/properties/tests/prop_dynamic_viscosity_test.py
rename to particula/gas/properties/tests/dynamic_viscosity_test.py
diff --git a/particula/gas/properties/tests/kinematic_viscosity_test.py b/particula/gas/properties/tests/kinematic_viscosity_test.py
new file mode 100644
index 000000000..f684cc5fd
--- /dev/null
+++ b/particula/gas/properties/tests/kinematic_viscosity_test.py
@@ -0,0 +1,47 @@
+"""
+Test module for the kinematic_viscosity module.
+"""
+
+import pytest
+from particula.gas.properties.kinematic_viscosity import (
+    get_kinematic_viscosity,
+    get_kinematic_viscosity_via_system_state,
+)
+
+
+def test_kinematic_viscosity():
+    """Test kinematic viscosity calculation."""
+    dynamic_viscosity = 1.85e-5  # Pa*s
+    fluid_density = 1.2  # kg/m^3
+    expected_kinematic_viscosity = dynamic_viscosity / fluid_density
+    assert (
+        pytest.approx(
+            get_kinematic_viscosity(dynamic_viscosity, fluid_density), 1e-5
+        )
+        == expected_kinematic_viscosity
+    )
+
+
+def test_kinematic_viscosity_via_system_state():
+    """Test kinematic viscosity calculation via system state."""
+    temperature = 300  # K
+    fluid_density = 1.2  # kg/m^3
+    expected_kinematic_viscosity = get_kinematic_viscosity_via_system_state(
+        temperature, fluid_density
+    )
+    assert (
+        pytest.approx(expected_kinematic_viscosity, 1e-5)
+        == expected_kinematic_viscosity
+    )
+
+
+def test_kinematic_viscosity_zero_temperature():
+    """Test for error handling with zero temperature."""
+    with pytest.raises(ValueError):
+        get_kinematic_viscosity_via_system_state(0, 1.2)
+
+
+def test_kinematic_viscosity_negative_temperature():
+    """Test for error handling with negative temperature."""
+    with pytest.raises(ValueError):
+        get_kinematic_viscosity_via_system_state(-10, 1.2)