diff --git a/particula/logger_setup.py b/particula/logger_setup.py index 8768d97be..40414e947 100644 --- a/particula/logger_setup.py +++ b/particula/logger_setup.py @@ -46,8 +46,13 @@ } }, "loggers": { - "root": { + "particula": { "level": "DEBUG", + "handlers": ["file", "stderr"], + "propagate": False + }, + "root": { + "level": "ERROR", "handlers": [ "stderr", "file" @@ -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 diff --git a/particula/next/gas/properties/__init__.py b/particula/next/gas/properties/__init__.py new file mode 100644 index 000000000..3578a27cf --- /dev/null +++ b/particula/next/gas/properties/__init__.py @@ -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 diff --git a/particula/next/gas/properties/dynamic_viscosity.py b/particula/next/gas/properties/dynamic_viscosity.py new file mode 100644 index 000000000..3ae9af7fc --- /dev/null +++ b/particula/next/gas/properties/dynamic_viscosity.py @@ -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.") + return ( + reference_viscosity * (temperature / reference_temperature)**1.5 * + (reference_temperature + SUTHERLAND_CONSTANT.m) / + (temperature + SUTHERLAND_CONSTANT.m) + ) diff --git a/particula/next/gas/properties/mean_free_path.py b/particula/next/gas/properties/mean_free_path.py new file mode 100644 index 000000000..0e4350914 --- /dev/null +++ b/particula/next/gas/properties/mean_free_path.py @@ -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_) diff --git a/particula/next/gas/properties/tests/prop_dynamic_viscosity_test.py b/particula/next/gas/properties/tests/prop_dynamic_viscosity_test.py new file mode 100644 index 000000000..c0be2bed7 --- /dev/null +++ b/particula/next/gas/properties/tests/prop_dynamic_viscosity_test.py @@ -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) diff --git a/particula/next/gas/properties/tests/prop_mean_free_path_test.py b/particula/next/gas/properties/tests/prop_mean_free_path_test.py new file mode 100644 index 000000000..7115bcad5 --- /dev/null +++ b/particula/next/gas/properties/tests/prop_mean_free_path_test.py @@ -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) diff --git a/particula/next/gas/properties/tests/prop_thermal_conductivity_test.py b/particula/next/gas/properties/tests/prop_thermal_conductivity_test.py new file mode 100644 index 000000000..020387a6f --- /dev/null +++ b/particula/next/gas/properties/tests/prop_thermal_conductivity_test.py @@ -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" diff --git a/particula/next/gas/properties/thermal_conductivity.py b/particula/next/gas/properties/thermal_conductivity.py new file mode 100644 index 000000000..93718405f --- /dev/null +++ b/particula/next/gas/properties/thermal_conductivity.py @@ -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) diff --git a/particula/util/mean_free_path.py b/particula/util/mean_free_path.py index 8bc514ca2..27b2d36be 100644 --- a/particula/util/mean_free_path.py +++ b/particula/util/mean_free_path.py @@ -9,8 +9,6 @@ """ -from typing import Union, Optional -from numpy.typing import NDArray import numpy as np from particula import u from particula.constants import ( @@ -122,51 +120,3 @@ def mfp( (2 * dyn_vis_val / pres) / (8 * molec_wt / (np.pi * gas_con * temp))**0.5 ).to_base_units() - - -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: - raise ValueError("Temperature must be positive [Kelvin]") - if pressure <= 0: - raise ValueError("Pressure must be positive [Pascal]") - if np.any(molar_mass <= 0): - raise ValueError("Molar mass must be positive [kg/mol]") - if dynamic_viscosity is None: - dynamic_viscosity = dyn_vis(temperature) # type: ignore - dynamic_viscosity = float(dynamic_viscosity.m) # type: ignore - - return np.array( - (2 * dynamic_viscosity / pressure) - / (8 * molar_mass / (np.pi * GAS_CONSTANT.m * temperature))**0.5, - dtype=np.float_) diff --git a/particula/util/reduced_quantity.py b/particula/util/reduced_quantity.py index c418e03a6..2e05f0324 100644 --- a/particula/util/reduced_quantity.py +++ b/particula/util/reduced_quantity.py @@ -4,8 +4,15 @@ quantity_1 * quantity_2 / (quantity_1 + quantity_2) """ +import logging +from typing import Union +from numpy.typing import NDArray +import numpy as np + from particula import u +logger = logging.getLogger("particula") # get instance of logger + def reduced_quantity(a_quantity, b_quantity): """ Returns the reduced mass of two particles. @@ -78,3 +85,71 @@ def reduced_quantity(a_quantity, b_quantity): b_q = u.Quantity(b_q, " ") return (a_q * b_q / (a_q + b_q)).to_base_units() + + +def reduced_value( + alpha: Union[float, NDArray[np.float_]], + beta: Union[float, NDArray[np.float_]], +) -> Union[float, NDArray[np.float_]]: + """ + Returns the reduced value of two parameters, calculated as: + reduced_value = alpha * beta / (alpha + beta) + + This formula calculates an "effective inertial" quantity, + allowing two-body problems to be solved as if they were one-body problems. + + Args: + - alpha: The first parameter (scalar or array). + - beta: The second parameter (scalar or array). + + Returns: + ------- + - A value or array of the same dimension as the input parameters. Returns + zero where alpha + beta equals zero to handle division by zero + gracefully. + + Raises: + - ValueError: If alpha and beta are arrays and their shapes do not match. + """ + # Ensure input compatibility, especially when both are arrays + if isinstance(alpha, np.ndarray) and isinstance(beta, np.ndarray) and ( + alpha.shape != beta.shape + ): + logger.error("The shapes of alpha and beta must be identical.") + raise ValueError("The shapes of alpha and beta must be identical.") + + # Calculation of the reduced value, with safety against division by zero + denominator = alpha + beta + # Using np.where to avoid division by zero + return np.where( + denominator != 0, + alpha * beta / denominator, + 0 + ) + + +def reduced_self_broadcast( + alpha_array: NDArray[np.float_]) -> NDArray[np.float_]: + """ + Returns the reduced value of an array with itself, broadcasting the + array into a matrix and calculating the reduced value of each element pair. + reduced_value = alpha_matrix * alpha_matrix_Transpose + / (alpha_matrix + alpha_matrix_Transpose) + + Args: + - alpha_array: The array to be broadcast and reduced. + + Returns: + ------- + - A square matrix of the reduced values. + """ + # Use broadcasting to create matrix and its transpose + alpha_matrix = alpha_array[:, np.newaxis] + alpha_matrix_transpose = alpha_array[np.newaxis, :] + denominator = alpha_matrix + alpha_matrix_transpose + # Perform element-wise multiplication and division + return np.where( + denominator != 0, + alpha_matrix * alpha_matrix_transpose / denominator, + 0 + ) diff --git a/particula/util/tests/mean_free_path_test.py b/particula/util/tests/mean_free_path_test.py index bb2fc3982..e2df5ff8f 100644 --- a/particula/util/tests/mean_free_path_test.py +++ b/particula/util/tests/mean_free_path_test.py @@ -2,9 +2,8 @@ """ import pytest -import numpy as np from particula import u -from particula.util.mean_free_path import mfp, molecule_mean_free_path +from particula.util.mean_free_path import mfp def test_mfp(): @@ -41,51 +40,3 @@ def test_mfp(): pressure=101325*u.Pa, molecular_weight=0.03*u.m/u.mol, ) - - -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) diff --git a/particula/util/tests/reduced_quantity_test.py b/particula/util/tests/reduced_quantity_test.py index 8bd861898..3052318b5 100644 --- a/particula/util/tests/reduced_quantity_test.py +++ b/particula/util/tests/reduced_quantity_test.py @@ -4,7 +4,8 @@ import numpy as np import pytest from particula import u -from particula.util.reduced_quantity import reduced_quantity +from particula.util.reduced_quantity import ( + reduced_quantity, reduced_value, reduced_self_broadcast) def test_reduced_quantity(): @@ -30,3 +31,110 @@ def test_reduced_quantity(): reduced_quantity(1, 2 * u.kg) with pytest.raises(TypeError): reduced_quantity(1 * u.kg, 2 * u.m) + + +def test_reduced_value_scalar(): + """ Test that the reduced value is calculated correctly.""" + assert reduced_value(4, 2) == 4 * 2 / (4 + 2), "Failed for scalar inputs" + + +def test_reduced_value_array(): + """ Test that the reduced value is calculated correctly.""" + alpha = np.array([2, 4, 6]) + beta = np.array([3, 6, 9]) + expected = alpha * beta / (alpha + beta) + result = reduced_value(alpha, beta) + assert np.allclose(result, expected), "Failed for array inputs" + + +def test_reduced_value_zero_division(): + """ Test division by zero handling.""" + alpha = np.array([0, 2, 0]) + beta = np.array([0, 0, 2]) + # Expect zeros where division by zero occurs + expected = np.array([0, 0, 0]) + result = reduced_value(alpha, beta) + assert np.array_equal(result, expected), "Failed handling division by zero" + + +def test_reduced_value_shape_mismatch(): + """ Test error handling for shape mismatch.""" + alpha = np.array([1, 2]) + beta = np.array([1, 2, 3]) + with pytest.raises(ValueError): + reduced_value(alpha, beta) + + +def test_reduced_value_negative_values(): + """ Test that the reduced value is calculated correctly.""" + alpha = np.array([-1, -2]) + beta = np.array([-3, -4]) + expected = alpha * beta / (alpha + beta) + result = reduced_value(alpha, beta) + assert np.allclose(result, expected), "Failed for negative values" + + +def test_reduced_value_one_element(): + """ Test with one element in array""" + alpha = np.array([5]) + beta = np.array([10]) + expected = alpha * beta / (alpha + beta) + result = reduced_value(alpha, beta) + assert np.allclose(result, expected), "Failed for single element arrays" + + +def test_reduced_self_broadcast_typical(): + """ Test that the reduced self broadcast is calculated correctly.""" + alpha_array = np.array([1, 2, 3]) + expected_result = np.array([ + [1 * 1 / (1 + 1), 1 * 2 / (1 + 2), 1 * 3 / (1 + 3)], + [2 * 1 / (2 + 1), 2 * 2 / (2 + 2), 2 * 3 / (2 + 3)], + [3 * 1 / (3 + 1), 3 * 2 / (3 + 2), 3 * 3 / (3 + 3)] + ]) + result = reduced_self_broadcast(alpha_array) + assert np.allclose( + result, expected_result), "Test failed for typical input" + + +def test_reduced_self_broadcast_empty(): + """ Test with empty input""" + alpha_array = np.array([]) + result = reduced_self_broadcast(alpha_array) + assert result.shape == (0, 0), "Test failed for empty input" + + +def test_reduced_self_broadcast_zero_elements(): + """ Test with zero elements""" + alpha_array = np.array([0, 0, 0]) + expected_result = np.array([ + [0, 0, 0], + [0, 0, 0], + [0, 0, 0] + ]) + result = reduced_self_broadcast(alpha_array) + assert np.allclose( + result, expected_result), "Test failed for zero elements input" + + +def test_reduced_self_broadcast_one_element(): + """ Test with a single element array""" + alpha_array = np.array([4]) + expected_result = np.array([ + [4 * 4 / (4 + 4)] + ]) + result = reduced_self_broadcast(alpha_array) + assert np.allclose( + result, expected_result), "Test failed for one element input" + + +def test_reduced_self_broadcast_negative_elements(): + """ Test with negative elements""" + alpha_array = np.array([-1, -2, -3]) + expected_result = np.array([ + [-1 * -1 / (-1 - 1), -1 * -2 / (-1 - 2), -1 * -3 / (-1 - 3)], + [-2 * -1 / (-2 - 1), -2 * -2 / (-2 - 2), -2 * -3 / (-2 - 3)], + [-3 * -1 / (-3 - 1), -3 * -2 / (-3 - 2), -3 * -3 / (-3 - 3)] + ]) + result = reduced_self_broadcast(alpha_array) + assert np.allclose( + result, expected_result), "Test failed for negative elements input"