From 974756aba65b551cfcee0f7e0310d5554ce019ae Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Sun, 17 Dec 2023 18:46:30 +0330 Subject: [PATCH 1/6] Refine Solar Energy Calculations and Documentation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Updated documentation in ‘index.rst’ to include effects of cloudy skies and polluted air on solar energy reception. - Clarified ‘zenith angle’ as ‘solar zenith angle’ in ‘atmospheric_transmission.py’ to maintain consistency across modules. - Revised the solar irradiance model in ‘irradiance.py’: corrected symbols, provided more accurate description of solar constants and the Earth’s orbit eccentricity’s influence. - Expanded ‘HottelModel’ in ‘model.py’ to include table formatting for climate constants and detailed formulae for transmittance component calculations based on observer altitude, enhancing readability and comprehension. - Harmonized term usage by consistently naming ‘solar zenith angle’ throughout ‘observer.py’, aligning with changes in ‘atmospheric_transmission.py’. - Ensured accurate scientific descriptions and fixed misleading variable names to improve code clarity and correctness across all modified files. --- docs/index.rst | 4 +- src/pysolorie/atmospheric_transmission.py | 2 +- src/pysolorie/irradiance.py | 16 +++--- src/pysolorie/model.py | 62 ++++++++++++++++++----- src/pysolorie/observer.py | 6 +-- 5 files changed, 64 insertions(+), 26 deletions(-) diff --git a/docs/index.rst b/docs/index.rst index 3aae9d2..8932cf8 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -23,7 +23,9 @@ A solar collector can be positioned at a fixed orientation to maximize energy re or it can be fixed for optimal performance throughout the year. The orientation can then be adjusted for the next time period. Solar irradiation is composed of three components: the direct beam, sky diffusion, and ground reflection. Now, we are considering flat solar panels and focusing on direct beam irradiation, which contributes the most to solar irradiation. -There are various models available for different climate types. In our case, we are using Hottel's model to estimate the transmittance of direct solar radiation through clear atmospheres [3]_. +There are various models available for different climate types. +Moreover, a cloudy sky or polluted air affects the solar energy received on Earth. +In our case, we are using Hottel's model to estimate the transmittance of direct solar radiation through clear atmospheres [3]_. ``pysolorie`` is a library designed to help you find this optimal orientation. Its features include, but are not limited to: diff --git a/src/pysolorie/atmospheric_transmission.py b/src/pysolorie/atmospheric_transmission.py index 2d4a29a..670f12b 100644 --- a/src/pysolorie/atmospheric_transmission.py +++ b/src/pysolorie/atmospheric_transmission.py @@ -61,7 +61,7 @@ def calculate_transmittance(self, day_of_year: int, solar_time: float) -> float: coefficient of the direct beam | - :math:`a_0`, :math:`a_1`, and :math:`k` are the components of clear-sky beam radiation transmittance - | - :math:`\theta_z` is the zenith angle + | - :math:`\theta_z` is the solar zenith angle :param day_of_year: The day of the year. :type day_of_year: int diff --git a/src/pysolorie/irradiance.py b/src/pysolorie/irradiance.py index f4666c1..c20d451 100644 --- a/src/pysolorie/irradiance.py +++ b/src/pysolorie/irradiance.py @@ -32,14 +32,16 @@ def calculate_extraterrestrial_irradiance(self, day_of_year: int) -> float: r""" Calculate the extraterrestrial solar irradiance for a given day of the year. - The extraterrestrial solar irradiance, :math:`E`, - is the amount of solar energy received per unit area on - a surface perpendicular to the Sun's rays outside Earth's atmosphere. + The extraterrestrial solar irradiance, :math:`I`, + is the annual average of the Sun's irradiation intensity. + This refers to the amount of solar energy received per unit + area per unit time on a surface perpendicular to the Sun's rays, + outside Earth's atmosphere. The formula used is: .. math:: - E = SC + I = SC \times (1 + 0.33 \times \cos (\frac{2\pi~n}{365})) @@ -47,9 +49,9 @@ def calculate_extraterrestrial_irradiance(self, day_of_year: int) -> float: of the Earth's atmosphere, which is approximately ``1367`` Watts per square meter. This is also known as the solar constant. - | - The factor ``0.033`` accounts - for the variation in the Earth-Sun distance - due to the Earth's elliptical orbit. + | - The factor 0.033, which is two times the eccentricity of + the Earth's orbit around the Sun, accounts for the variation + in the Earth-Sun distance due to the Earth's elliptical orbit. | - :math:`n` is the day of the year (i.e., ``day_of_year``) :param day\_of\_year: The day of the year, ranging from 1 to 365. diff --git a/src/pysolorie/model.py b/src/pysolorie/model.py index 53f066c..e0ae759 100644 --- a/src/pysolorie/model.py +++ b/src/pysolorie/model.py @@ -20,16 +20,34 @@ class HottelModel: Hottel Model for estimating clear-sky beam radiation transmittance based on climate type, and observer altitude. - ``CLIMATE_CONSTANTS``: Correction factors for different climate types + Climate Constants are Correction factors for different climate types :math:`r_0`, :math:`r_1`, and :math:`r_k`. - """ - CLIMATE_CONSTANTS: Dict[str, Tuple[float, float, float]] = { - "TROPICAL": (0.95, 0.98, 1.02), - "MIDLATITUDE SUMMER": (0.97, 0.99, 1.02), - "SUBARCTIC SUMMER": (0.99, 0.99, 1.01), - "MIDLATITUDE WINTER": (1.03, 1.01, 1.00), - } + .. list-table:: Climate Constants + :widths: 25 25 25 25 + :header-rows: 1 + + * - Climate Type + - :math:`r_0` + - :math:`r_1` + - :math:`r_k` + * - TROPICAL + - 0.95 + - 0.98 + - 1.02 + * - MIDLATITUDE SUMMER + - 0.97 + - 0.99 + - 1.02 + * - SUBARCTIC SUMMER + - 0.99 + - 0.99 + - 1.01 + * - MIDLATITUDE WINTER + - 1.03 + - 1.01 + - 1.00 + """ def _convert_to_km(self, observer_altitude: int) -> float: r""" @@ -104,10 +122,8 @@ def calculate_transmittance_components( :math:`a_0`, :math:`a_1`, and :math:`k` based on climate type and observer altitude. - Correction factors adjust the clear-sky beam - radiation transmittance components. According - to the following formulas: + radiation transmittance components according to the following formulas: .. math:: a_0 = r_0 \times a_0^* @@ -116,11 +132,29 @@ def calculate_transmittance_components( k = r_k \times k^* - :param climate_type: Climate type - (i.e., ``TROPICAL``, ``MIDLATITUDE SUMMER``, - ``SUBARCTIC SUMMER``, or ``MIDLATITUDE WINTER``). + The formula used to calculate :math:`a_0^*` is: + + .. math:: + a_0^* = 0.4237 - 0.00821 \times (6 - A)^2 + + The formula used to calculate :math:`a_1^*` is: + + .. math:: + a_1^* = 0.5055 + 0.00595 \times (6.5 - A)^2 + + The formula used to calculate :math:`k^*` is: + + .. math:: + k^* = 0.2711 + 0.01858 \times (2.5 - A)^2 + + Where `A` is the observer altitude in kilometers. + + :param climate_type: Climate type (i.e., one of the keys in + `Climate Constants`: ``TROPICAL``, ``MIDLATITUDE SUMMER``, + ``SUBARCTIC SUMMER``, or ``MIDLATITUDE WINTER``). :type climate_type: str :param observer_altitude: Altitude of the observer in meters. + It is converted to kilometers in the calculations. :type observer_altitude: float :return: Components of clear-sky beam radiation transmittance (:math:`a_0`, :math:`a_1`, :math:`k`). diff --git a/src/pysolorie/observer.py b/src/pysolorie/observer.py index a93416b..ec3b620 100644 --- a/src/pysolorie/observer.py +++ b/src/pysolorie/observer.py @@ -42,16 +42,16 @@ def __init__( def calculate_zenith_angle(self, day_of_year: int, solar_time: float) -> float: r""" - Calculate the zenith angle. + Calculate the solar zenith angle. - The zenith angle is calculated using the formula: + The solar zenith angle is calculated using the formula: .. math:: \cos(\theta_z) = \sin(\phi) \times \sin(\delta) + \cos(\phi) \times \cos(\delta) \times \cos(\omega) - | - :math:`\theta_z` is the zenith angle + | - :math:`\theta_z` is the solar zenith angle | - :math:`\phi` is the latitude of the observer | - :math:`\delta` is the solar declination | - :math:`\omega` is the hour angle. From dca0de6c319790bfcf8472302cb2a81e018080d9 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Sun, 17 Dec 2023 18:48:44 +0330 Subject: [PATCH 2/6] Enhance Optimization in Solar Models - Implemented a dictionary of climate constants within the 'HottelModel' class for easier management and access of climate correction factors. - Replaced the discretization method for finding the optimal orientation with a more precise and efficient numerical optimization using 'optimize.minimize_scalar' from SciPy in 'numerical_integration.py'. - Adjusted test values in 'test_pysolorie.py' to reflect more accurate sunrise and sunset times, aligning test data with new optimization logic and amended climate constants. --- src/pysolorie/model.py | 8 ++++++++ src/pysolorie/numerical_integration.py | 18 ++++++++++-------- tests/test_pysolorie.py | 8 ++++---- 3 files changed, 22 insertions(+), 12 deletions(-) diff --git a/src/pysolorie/model.py b/src/pysolorie/model.py index e0ae759..26d3605 100644 --- a/src/pysolorie/model.py +++ b/src/pysolorie/model.py @@ -161,6 +161,14 @@ def calculate_transmittance_components( :rtype: tuple of floats :raises ValueError: If an invalid climate type is provided. """ + + self.CLIMATE_CONSTANTS: Dict[str, Tuple[float, float, float]] = { + "TROPICAL": (0.95, 0.98, 1.02), + "MIDLATITUDE SUMMER": (0.97, 0.99, 1.02), + "SUBARCTIC SUMMER": (0.99, 0.99, 1.01), + "MIDLATITUDE WINTER": (1.03, 1.01, 1.00), + } + if climate_type.upper() not in self.CLIMATE_CONSTANTS: raise ValueError("Invalid climate type") diff --git a/src/pysolorie/numerical_integration.py b/src/pysolorie/numerical_integration.py index 6e0c81e..a979139 100644 --- a/src/pysolorie/numerical_integration.py +++ b/src/pysolorie/numerical_integration.py @@ -15,7 +15,7 @@ import math import numpy as np -from scipy import integrate # type: ignore +from scipy import integrate, optimize # type: ignore from .atmospheric_transmission import AtmosphericTransmission from .irradiance import SolarIrradiance @@ -154,11 +154,13 @@ def find_optimal_orientation(self, day_of_year: int) -> float: :return: The optimal orientation (i.e., :math:`beta`) in degrees. :rtype: float """ - betas = np.arange(-math.pi / 2, math.pi / 2, 0.005) # Discretize beta - irradiations = [ - self.calculate_direct_irradiation(beta, day_of_year) for beta in betas - ] - optimal_beta = betas[ - np.argmax(irradiations) - ] # Find beta that gives max irradiation + + def neg_irradiation(beta: float): + # We negate the irradiation because we're minimizing + return -self.calculate_direct_irradiation(beta, day_of_year) + + result = optimize.minimize_scalar( + neg_irradiation, bounds=(-math.pi / 2, math.pi / 2), method="bounded" + ) + optimal_beta = result.x return math.degrees(optimal_beta) diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index d48323a..4ba2a51 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -239,28 +239,28 @@ def test_calculate_sunrise_sunset( 1200, 35.6892, 172, - 0.241, + 0.170, ), # Tehran Summer, day_of_year=172 (June 21) ( "MIDLATITUDE WINTER", 1200, 35.6892, 355, - 63.839, + 63.791, ), # Tehran Winter, day_of_year=355 (Dec 21) ( "TROPICAL", 26, 3.5952, 100, - -6.635, + -6.610, ), # Medan, day_of_year=100 (April 10) ( "SUBARCTIC SUMMER", 132, 64.84361, 200, - 32.613, + 32.614, ), # Fairbanks Summer, day_of_year=200 (July 19) ], ) From f48c98575474790362eee8ded3ea37d90e6a2b64 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Sun, 17 Dec 2023 18:50:02 +0330 Subject: [PATCH 3/6] Refactor Direct Irradiation Formula Documentation - Updated the formula for total direct irradiation in the documentation of 'IrradiationCalculator' class to correct the symbol from `E` to `I`, reflecting the amount of solar energy received per unit area per second. - Enhanced clarity in the documentation by specifying that `I` represents solar energy per unit time, and by providing a more detailed description of the incidence angle `theta`. - Revised the constant `Omega` description for better readability and understanding in scientific contexts. - Modified the Heaviside step function notation from `H` to match the standard mathematical representation. --- src/pysolorie/numerical_integration.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pysolorie/numerical_integration.py b/src/pysolorie/numerical_integration.py index a979139..95fddc4 100644 --- a/src/pysolorie/numerical_integration.py +++ b/src/pysolorie/numerical_integration.py @@ -105,7 +105,7 @@ def calculate_direct_irradiation( | The total direct irradiation is calculated using the formula: .. math:: - E(n,\phi) = \frac{E}{\Omega} \int_{\omega_s}^{\omega_t} + E(n,\phi) = \frac{I}{\Omega} \int_{\omega_s}^{\omega_t} \cos(\theta) \times H(\cos(\theta)) \times \tau_b~d\omega @@ -113,17 +113,19 @@ def calculate_direct_irradiation( | - :math:`\phi` is the latitude of the observer - | - :math:`E` is the amount of solar energy received + | - :math:`I` is the amount of + solar energy received per unit area per second. - | - :math:`\Omega` is a constant equal to ``7.15 * 1e-5`` + | - :math:`\Omega` = ``7.15 * 1e-5`` - | - :math:`\theta` is incidence angle + | - :math:`\theta` is incidence angle, the angle between the position vector + of the sun and the normal vector to the solar panel. | - :math:`\omega_s` is the sunrise hour angle | - :math:`\omega_t` is the sunset hour angle - | - :math:`H` is the heaviside step function + | - :math:`H` is the Heaviside step function :param panel_orientation: The orientation of the solar panel in radians. From 399b5f1d8b5bd2caf3d89f9bb3579e3d5b734610 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Sun, 17 Dec 2023 20:44:47 +0330 Subject: [PATCH 4/6] Add Functionality to Direct Irradiation - Enhanced the `getting_started.rst` with a new section on calculating direct irradiation, including examples for different climate types to facilitate user understanding. - Fixed an incorrect unit reference for the solar constant in `SolarIrradiance` class, changing it from "Watts per square meter" to "Megawatts per square meter" for consistency with internal calculations. - Modified `numerical_integration.py` to clarify that the `panel_orientation` parameter is now expected to be given in degrees, improving the method's interface. - Ensured the conversion from degrees to radians is handled within the direct irradiation calculation method to maintain internal accuracy. - Expanded the `Plotter` class functionality with a `plot_total_direct_irradiation` method, allowing users to visualize irradiation data and save plots as images if desired. - Updated `ReportGenerator` to produce more informative CSV reports which now include total direct irradiation data alongside optimal panel orientations. - Adapted unit tests to reflect changes to the solar constant and to check against updated irradiance values. --- docs/getting_started.rst | 52 +++++++++++ src/pysolorie/irradiance.py | 8 +- src/pysolorie/numerical_integration.py | 5 +- src/pysolorie/plotter.py | 46 ++++++++++ src/pysolorie/report.py | 14 ++- tests/test_pysolorie.py | 121 +++++++++++++++++++++++-- 6 files changed, 226 insertions(+), 20 deletions(-) diff --git a/docs/getting_started.rst b/docs/getting_started.rst index a1a43d0..f70b972 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -42,6 +42,35 @@ The ``climate_type`` can be one of the following: print(f"Optimal orientation: {result}") +Calculating Direct Irradiation +------------------------------ +The ``calculate_direct_irradiation`` method calculates the total direct irradiation for a given solar panel orientation and day of the year. + +The ``climate_type`` can be one of the following: + +- ``"MIDLATITUDE SUMMER"`` +- ``"MIDLATITUDE WINTER"`` +- ``"TROPICAL"`` +- ``"SUBARCTIC SUMMER"`` + +.. code-block:: python + + from pysolorie import IrradiationCalculator + + # Create an irradiation calculator for Tehran in the summer + irradiation_calculator = IrradiationCalculator( + "MIDLATITUDE SUMMER", 1200, 35.6892 + ) + + # Define the panel orientation and day of the year + panel_orientation: float = 45.0 # degrees + day_of_year: int = 172 # June 21, approximately the summer solstice + + # Calculate the direct irradiation + result = irradiation_calculator.calculate_direct_irradiation(panel_orientation, day_of_year) + + print(f"Direct irradiation: {result}") + Generating a CSV Report ----------------------- @@ -85,6 +114,29 @@ The plot will be saved to the specified path. The ``plot_kwargs`` and ``savefig_ parameters can be used to customize the plot and the savefig function, respectively. If no path is provided, the plot will be displayed but not saved. If the path is provided, the plot will be saved to the specified path and not displayed. If you want to both display and save the plot, you should call ``plt.show()`` after this function. +Plotting the Total Direct Irradiation +------------------------------------- + +The ``plot_total_direct_irradiation`` method plots the total direct irradiation for a range of days. + +.. code-block:: python + + from pysolorie import Plotter, IrradiationCalculator + from pathlib import Path + + # Create a plotter and an irradiation calculator + plotter = Plotter() + irradiation_calculator = IrradiationCalculator("MIDLATITUDE SUMMER", 1200, 35.6892) + + # Plot the total direct irradiation for days 60 to 70 + plotter.plot_total_direct_irradiation(irradiation_calculator, 60, 70, Path('results.png'), plot_kwargs={'xlabel': 'Day', 'ylabel': 'Total Direct Irradiation (MW/m²)', 'title': 'Total Direct Irradiation', "figsize": (16,9)}, savefig_kwargs={'dpi': 300}) + +The plot will be saved to the specified path. The ``plot_kwargs`` and ``savefig_kwargs`` +parameters can be used to customize the plot and the savefig function, respectively. If no path is provided, the plot will be displayed but not saved. +If the path is provided, the plot will be saved to the specified path and not displayed. If you want to both display and save the plot, you should call ``plt.show()`` after this function. + + + Calculating Sunrise and Sunset ------------------------------ diff --git a/src/pysolorie/irradiance.py b/src/pysolorie/irradiance.py index c20d451..3fbccf2 100644 --- a/src/pysolorie/irradiance.py +++ b/src/pysolorie/irradiance.py @@ -47,7 +47,7 @@ def calculate_extraterrestrial_irradiance(self, day_of_year: int) -> float: | - :math:`SC` is the average solar radiation arriving outside of the Earth's atmosphere, - which is approximately ``1367`` Watts per square meter. + which is approximately ``1367`` Megawatts per square meter. This is also known as the solar constant. | - The factor 0.033, which is two times the eccentricity of the Earth's orbit around the Sun, accounts for the variation @@ -57,12 +57,12 @@ def calculate_extraterrestrial_irradiance(self, day_of_year: int) -> float: :param day\_of\_year: The day of the year, ranging from 1 to 365. :type day\_of\_year: int - :return: The extraterrestrial solar irradiance in Watts per square meter. + :return: The extraterrestrial solar irradiance in Megawatts per square meter. :rtype: float """ - # Solar constant (W/m^2) - SOLAR_CONSTANT = 1367 + # Solar constant (MW/m^2) + SOLAR_CONSTANT = 1367 * 1e-6 # Factor to account for the Earth's orbital eccentricity earth_orbital_eccentricity = 0.033 diff --git a/src/pysolorie/numerical_integration.py b/src/pysolorie/numerical_integration.py index 95fddc4..31a61a9 100644 --- a/src/pysolorie/numerical_integration.py +++ b/src/pysolorie/numerical_integration.py @@ -128,7 +128,7 @@ def calculate_direct_irradiation( | - :math:`H` is the Heaviside step function - :param panel_orientation: The orientation of the solar panel in radians. + :param panel_orientation: The orientation of the solar panel in degrees. :type panel_orientation: float :param day_of_year: The day of the year. :type day_of_year: int @@ -138,6 +138,7 @@ def calculate_direct_irradiation( sunrise_hour_angle, sunset_hour_angle = self._observer.calculate_sunrise_sunset( day_of_year ) + panel_orientation = math.radians(panel_orientation) irradiance_components = [ self._calculate_irradiance_component( hour_angle, panel_orientation, day_of_year @@ -159,7 +160,7 @@ def find_optimal_orientation(self, day_of_year: int) -> float: def neg_irradiation(beta: float): # We negate the irradiation because we're minimizing - return -self.calculate_direct_irradiation(beta, day_of_year) + return -self.calculate_direct_irradiation(math.degrees(beta), day_of_year) result = optimize.minimize_scalar( neg_irradiation, bounds=(-math.pi / 2, math.pi / 2), method="bounded" diff --git a/src/pysolorie/plotter.py b/src/pysolorie/plotter.py index 5c83528..1a91302 100644 --- a/src/pysolorie/plotter.py +++ b/src/pysolorie/plotter.py @@ -66,6 +66,52 @@ def plot_optimal_orientation( self._plot(days, betas, path, plot_kwargs, savefig_kwargs) + @logger_decorator + def plot_total_direct_irradiation( + self, + irradiation_calculator: IrradiationCalculator, + from_day: int, + to_day: int, + path: Optional[Path] = None, + plot_kwargs: Optional[Dict[str, str]] = None, + savefig_kwargs: Optional[Dict[str, str]] = None, + ) -> None: + r""" + Plots the total direct irradiation for a range of days. + + :param irradiation_calculator: An instance of the IrradiationCalculator class. + :type irradiation_calculator: pysolorie.IrradiationCalculator + :param from_day: The starting day for the range of days. + :type from_day: int + :param to_day: The ending day for the range of days. + :type to_day: int + :param path: The path where the plot will be saved (default is None, + which means the plot will be shown but not saved). + :type path: Path, optional + :param plot_kwargs: A dictionary of keyword arguments + to be passed to the plot (default is None). + :type plot_kwargs: dict, optional + :param savefig_kwargs: A dictionary of keyword arguments + to be passed to the savefig function (default is None). + :type savefig_kwargs: dict, optional + """ + + days = [] + total_direct_irradiations = [] + + for day in range(from_day, to_day): + optimal_beta = irradiation_calculator.find_optimal_orientation(day) + total_direct_irradiation = ( + irradiation_calculator.calculate_direct_irradiation(optimal_beta, day) + ) + days.append(day) + total_direct_irradiations.append(total_direct_irradiation) + + plot_kwargs = plot_kwargs if plot_kwargs else {} + savefig_kwargs = savefig_kwargs if savefig_kwargs else {} + + self._plot(days, total_direct_irradiations, path, plot_kwargs, savefig_kwargs) + def _calculate_optimal_orientations( self, irradiation_calculator: IrradiationCalculator, from_day: int, to_day: int ) -> Tuple[List[int], List[float]]: diff --git a/src/pysolorie/report.py b/src/pysolorie/report.py index c51276c..897133f 100644 --- a/src/pysolorie/report.py +++ b/src/pysolorie/report.py @@ -45,17 +45,23 @@ def generate_optimal_orientation_csv_report( """ with open(path, "w", newline="") as file: writer = csv.writer(file) - writer.writerow(["Day", "Beta (degrees)"]) + writer.writerow( + ["Day", "Beta (degrees)", "Total Direct Irradiation (MW/m²)"] + ) for day in range(from_day, to_day): beta = irradiation_calculator.find_optimal_orientation(day) + total_direct_irradiation = ( + irradiation_calculator.calculate_direct_irradiation(beta, day) + ) logger = logging.getLogger( self.generate_optimal_orientation_csv_report.__name__ ) logger.info( - f"On day {day}," - + f"the solar panel's optimal orientation is {beta} degrees." + f"On day {day}, the solar panel's optimal orientation is " + f"{beta} degrees, and the total direct irradiation is " + f"{total_direct_irradiation} MW/m²." ) # Write the result to the CSV file - writer.writerow([day, beta]) + writer.writerow([day, beta, total_direct_irradiation]) diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index 4ba2a51..5c2407e 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -102,11 +102,11 @@ def test_solar_time( @pytest.mark.parametrize( "day_of_year, expected_irradiance", [ - (1, 1412.104), # January 1st - (81, 1374.918), # March 22nd (equinox) - (172, 1322.623), # June 21st (summer solstice) - (264, 1359.464), # September 23rd (equinox) - (355, 1411.444), # December 21st (winter solstice) + (1, 0.001411444), # January 1st + (81, 0.001374918), # March 22nd (equinox) + (172, 0.00132262), # June 21st (summer solstice) + (264, 0.001359464), # September 23rd (equinox) + (355, 0.001412104), # December 21st (winter solstice) ], ) def test_calculate_extraterrestrial_irradiance( @@ -117,7 +117,7 @@ def test_calculate_extraterrestrial_irradiance( irradiance: float = solar_irradiance.calculate_extraterrestrial_irradiance( day_of_year ) - assert irradiance == pytest.approx(expected_irradiance, abs=1e-3) + assert irradiance == pytest.approx(expected_irradiance, abs=1e-5) @pytest.mark.parametrize( @@ -231,6 +231,65 @@ def test_calculate_sunrise_sunset( assert sunset_hour_angle == pytest.approx(expected_sunset_hour_angle, abs=1e-3) +@pytest.mark.parametrize( + "climate_type," + + "observer_altitude," + + "observer_latitude," + + "day_of_year," + + "panel_orientation," + + "expected_result", + [ + ( + "MIDLATITUDE SUMMER", + 1200, + 35.6892, + 172, + 45.0, + 20.3026, # Tehran Summer, day_of_year=172 (June 21) + ), + ( + "MIDLATITUDE WINTER", + 1200, + 35.6892, + 355, + 45.0, + 19.436, # Tehran Winter, day_of_year=355 (Dec 21) + ), + ( + "TROPICAL", + 26, + 3.5952, + 100, + 45.0, + 13.224, # Medan, day_of_year=100 (April 10) + ), + ( + "SUBARCTIC SUMMER", + 132, + 64.84361, + 200, + 45.0, + 21.371, # Fairbanks Summer, day_of_year=200 (July 19) + ), + ], +) +def test_calculate_direct_irradiation( + climate_type: str, + observer_altitude: int, + observer_latitude: float, + day_of_year: int, + panel_orientation: float, + expected_result: float, +) -> None: + irradiation_calculator = IrradiationCalculator( + climate_type, observer_altitude, observer_latitude + ) + result = irradiation_calculator.calculate_direct_irradiation( + panel_orientation, day_of_year + ) + assert pytest.approx(result, abs=1e-3) == expected_result + + @pytest.mark.parametrize( "climate_type, observer_altitude, observer_latitude, day_of_year, expected_result", [ @@ -303,12 +362,25 @@ def test_generate_optimal_orientation_csv_report(tmpdir) -> None: with open(csv_path, "r") as file: reader = csv.reader(file) header = next(reader) - assert header == ["Day", "Beta (degrees)"] + assert header == ["Day", "Beta (degrees)", "Total Direct Irradiation (MW/m²)"] for i, row in enumerate(reader, start=from_day): - day, beta = row - assert int(day) == i + day, beta, total_direct_irradiation = ( + int(row[0]), + float(row[1]), + float(row[2]), + ) + + assert day == i expected_beta = irradiation_calculator.find_optimal_orientation(i) - assert pytest.approx(float(beta), abs=1e-3) == expected_beta + + expected_total_direct_irradiation = ( + irradiation_calculator.calculate_direct_irradiation(beta, i) + ) + assert pytest.approx(beta, abs=1e-3) == expected_beta + assert ( + pytest.approx(total_direct_irradiation, abs=1e-3) + == expected_total_direct_irradiation + ) def test_plot_optimal_orientation(tmpdir) -> None: @@ -340,6 +412,35 @@ def test_plot_optimal_orientation(tmpdir) -> None: assert img.shape[1] > 0, "The plot image has no content." +def test_plot_total_direct_irradiation(tmpdir) -> None: + # Create a temporary directory for the test + temp_dir: Path = Path(tmpdir) + + # Initialize the Plotter + plotter: Plotter = Plotter() + + # Initialize the IrradiationCalculator for Tehran + irradiation_calculator: IrradiationCalculator = IrradiationCalculator( + "MIDLATITUDE SUMMER", 1200, 35.6892 + ) + + # Define the path for the plot + plot_path: Path = temp_dir / "plot.png" + from_day: int = 60 + to_day: int = 70 + # Call the method to generate the plot + plotter.plot_total_direct_irradiation( + irradiation_calculator, from_day, to_day, plot_path + ) + + # Check the plot file + assert plot_path.exists(), "The plot file was not created." + + img: np.ndarray = plt.imread(plot_path) + assert img.shape[0] > 0, "The plot image has no content." + assert img.shape[1] > 0, "The plot image has no content." + + def test_plot_method() -> None: # Set up the necessary variables plotter: Plotter = Plotter() From c699d7549b109a85faec0c76b20e05a1bcceb7b9 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Mon, 18 Dec 2023 07:26:38 +0330 Subject: [PATCH 5/6] Refactoring documentation and updating docstrings - Reorganized the overall structure of the documentation for better clarity. - Streamlined the module titles to simplify navigation through the docs. - Adjusted the toctree maxdepth parameter for optimal content exposure. - Made consistency improvements across all documentation files. - Removed the redundant pysolorie.logger and pysolorie.rst files. - Enhanced module docstrings with descriptions and relevant references. - Cleaned up the Sphinx apidoc command in setup.cfg for ease of use. --- docs/reference/modules.rst | 15 ++++++++---- .../pysolorie.atmospheric_transmission.rst | 6 ++--- docs/reference/pysolorie.irradiance.rst | 6 ++--- docs/reference/pysolorie.logger.rst | 7 ------ docs/reference/pysolorie.model.rst | 6 ++--- .../pysolorie.numerical_integration.rst | 6 ++--- docs/reference/pysolorie.observer.rst | 6 ++--- docs/reference/pysolorie.plotter.rst | 6 ++--- docs/reference/pysolorie.report.rst | 6 ++--- docs/reference/pysolorie.rst | 23 ------------------- docs/reference/pysolorie.sun_position.rst | 6 ++--- setup.cfg | 7 ------ src/pysolorie/atmospheric_transmission.py | 4 ++++ src/pysolorie/irradiance.py | 13 +++++++++-- src/pysolorie/model.py | 8 ++++++- src/pysolorie/numerical_integration.py | 12 ++++++++++ src/pysolorie/observer.py | 5 ++++ src/pysolorie/report.py | 5 ++++ src/pysolorie/sun_position.py | 11 ++++++++- 19 files changed, 81 insertions(+), 77 deletions(-) delete mode 100644 docs/reference/pysolorie.logger.rst delete mode 100644 docs/reference/pysolorie.rst diff --git a/docs/reference/modules.rst b/docs/reference/modules.rst index b0bc4b4..275bd29 100644 --- a/docs/reference/modules.rst +++ b/docs/reference/modules.rst @@ -1,7 +1,14 @@ -pysolorie -========= +API Reference +============= .. toctree:: - :maxdepth: 4 + :maxdepth: 3 - pysolorie + pysolorie.atmospheric_transmission + pysolorie.irradiance + pysolorie.model + pysolorie.numerical_integration + pysolorie.observer + pysolorie.plotter + pysolorie.report + pysolorie.sun_position diff --git a/docs/reference/pysolorie.atmospheric_transmission.rst b/docs/reference/pysolorie.atmospheric_transmission.rst index 8aa0927..e7d4188 100644 --- a/docs/reference/pysolorie.atmospheric_transmission.rst +++ b/docs/reference/pysolorie.atmospheric_transmission.rst @@ -1,7 +1,5 @@ -pysolorie.atmospheric\_transmission module -========================================== +Atmospheric Transmission +======================== .. automodule:: pysolorie.atmospheric_transmission :members: - :undoc-members: - :show-inheritance: diff --git a/docs/reference/pysolorie.irradiance.rst b/docs/reference/pysolorie.irradiance.rst index 079a24c..3aaa88c 100644 --- a/docs/reference/pysolorie.irradiance.rst +++ b/docs/reference/pysolorie.irradiance.rst @@ -1,7 +1,5 @@ -pysolorie.irradiance module -=========================== +Solar Irradiance +================ .. automodule:: pysolorie.irradiance :members: - :undoc-members: - :show-inheritance: diff --git a/docs/reference/pysolorie.logger.rst b/docs/reference/pysolorie.logger.rst deleted file mode 100644 index 57ad137..0000000 --- a/docs/reference/pysolorie.logger.rst +++ /dev/null @@ -1,7 +0,0 @@ -pysolorie.logger module -======================= - -.. automodule:: pysolorie.logger - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/reference/pysolorie.model.rst b/docs/reference/pysolorie.model.rst index 3b3e480..9952112 100644 --- a/docs/reference/pysolorie.model.rst +++ b/docs/reference/pysolorie.model.rst @@ -1,7 +1,5 @@ -pysolorie.model module -====================== +Hottel Model +============ .. automodule:: pysolorie.model :members: - :undoc-members: - :show-inheritance: diff --git a/docs/reference/pysolorie.numerical_integration.rst b/docs/reference/pysolorie.numerical_integration.rst index 6ca854c..fe4c7cb 100644 --- a/docs/reference/pysolorie.numerical_integration.rst +++ b/docs/reference/pysolorie.numerical_integration.rst @@ -1,7 +1,5 @@ -pysolorie.numerical\_integration module -======================================= +Total Direct Irradiation +======================== .. automodule:: pysolorie.numerical_integration :members: - :undoc-members: - :show-inheritance: diff --git a/docs/reference/pysolorie.observer.rst b/docs/reference/pysolorie.observer.rst index a497b52..9e9bf90 100644 --- a/docs/reference/pysolorie.observer.rst +++ b/docs/reference/pysolorie.observer.rst @@ -1,7 +1,5 @@ -pysolorie.observer module -========================= +Observer +======== .. automodule:: pysolorie.observer :members: - :undoc-members: - :show-inheritance: diff --git a/docs/reference/pysolorie.plotter.rst b/docs/reference/pysolorie.plotter.rst index c775f1d..5d01ca6 100644 --- a/docs/reference/pysolorie.plotter.rst +++ b/docs/reference/pysolorie.plotter.rst @@ -1,7 +1,5 @@ -pysolorie.plotter module -======================== +Plotter +======= .. automodule:: pysolorie.plotter :members: - :undoc-members: - :show-inheritance: diff --git a/docs/reference/pysolorie.report.rst b/docs/reference/pysolorie.report.rst index eb14e2f..bc02c82 100644 --- a/docs/reference/pysolorie.report.rst +++ b/docs/reference/pysolorie.report.rst @@ -1,7 +1,5 @@ -pysolorie.report module -======================= +Report +====== .. automodule:: pysolorie.report :members: - :undoc-members: - :show-inheritance: diff --git a/docs/reference/pysolorie.rst b/docs/reference/pysolorie.rst deleted file mode 100644 index 14be713..0000000 --- a/docs/reference/pysolorie.rst +++ /dev/null @@ -1,23 +0,0 @@ -pysolorie package -================= - -.. automodule:: pysolorie - :members: - :undoc-members: - :show-inheritance: - -Submodules ----------- - -.. toctree:: - :maxdepth: 4 - - pysolorie.atmospheric_transmission - pysolorie.irradiance - pysolorie.logger - pysolorie.model - pysolorie.numerical_integration - pysolorie.observer - pysolorie.plotter - pysolorie.report - pysolorie.sun_position diff --git a/docs/reference/pysolorie.sun_position.rst b/docs/reference/pysolorie.sun_position.rst index 96416cf..deaa6c8 100644 --- a/docs/reference/pysolorie.sun_position.rst +++ b/docs/reference/pysolorie.sun_position.rst @@ -1,7 +1,5 @@ -pysolorie.sun\_position module -============================== +Sun Position +============ .. automodule:: pysolorie.sun_position :members: - :undoc-members: - :show-inheritance: diff --git a/setup.cfg b/setup.cfg index 98d6686..c76b125 100644 --- a/setup.cfg +++ b/setup.cfg @@ -103,11 +103,4 @@ deps = sphinx sphinx-rtd-theme commands = - sphinx-apidoc \ - --force \ - --implicit-namespaces \ - --module-first \ - --separate \ - -o docs/reference/ \ - src/pysolorie/ sphinx-build -W -b html docs/ docs/_build/ diff --git a/src/pysolorie/atmospheric_transmission.py b/src/pysolorie/atmospheric_transmission.py index 670f12b..fc54aac 100644 --- a/src/pysolorie/atmospheric_transmission.py +++ b/src/pysolorie/atmospheric_transmission.py @@ -18,6 +18,10 @@ class AtmosphericTransmission: + r""" + A class to model the atmospheric transmission. + """ + def __init__( self, climate_type: str, diff --git a/src/pysolorie/irradiance.py b/src/pysolorie/irradiance.py index 3fbccf2..f621f89 100644 --- a/src/pysolorie/irradiance.py +++ b/src/pysolorie/irradiance.py @@ -18,6 +18,10 @@ class SolarIrradiance: + r""" + A class to model the solar irradiance. + """ + def __init__(self, sun_position: SunPosition): r""" To instantiate the ``SolarIrradiance`` class, provide the following parameter. @@ -47,9 +51,9 @@ def calculate_extraterrestrial_irradiance(self, day_of_year: int) -> float: | - :math:`SC` is the average solar radiation arriving outside of the Earth's atmosphere, - which is approximately ``1367`` Megawatts per square meter. + which is approximately ``1367`` Megawatts per square meter [1]_. This is also known as the solar constant. - | - The factor 0.033, which is two times the eccentricity of + | - The factor ``0.033``, which is two times the eccentricity of the Earth's orbit around the Sun, accounts for the variation in the Earth-Sun distance due to the Earth's elliptical orbit. | - :math:`n` is the day of the year (i.e., ``day_of_year``) @@ -59,6 +63,11 @@ def calculate_extraterrestrial_irradiance(self, day_of_year: int) -> float: :return: The extraterrestrial solar irradiance in Megawatts per square meter. :rtype: float + + References + ---------- + .. [1] Duffie (Deceased), J., Beckman, W., & Blair, N. (2020). + Solar Engineering of Thermal Processes, Photovoltaics and Wind. Wiley. """ # Solar constant (MW/m^2) diff --git a/src/pysolorie/model.py b/src/pysolorie/model.py index 26d3605..e58e94c 100644 --- a/src/pysolorie/model.py +++ b/src/pysolorie/model.py @@ -18,7 +18,7 @@ class HottelModel: r""" Hottel Model for estimating clear-sky beam radiation transmittance - based on climate type, and observer altitude. + based on climate type, and observer altitude [1]_. Climate Constants are Correction factors for different climate types :math:`r_0`, :math:`r_1`, and :math:`r_k`. @@ -47,6 +47,12 @@ class HottelModel: - 1.03 - 1.01 - 1.00 + + References + ---------- + .. [1] Hottel, H. (1976). A simple model for estimating the transmittance of + direct solar radiation through clear atmospheres. + Solar Energy, 18(2), 129-134. """ def _convert_to_km(self, observer_altitude: int) -> float: diff --git a/src/pysolorie/numerical_integration.py b/src/pysolorie/numerical_integration.py index 31a61a9..e050933 100644 --- a/src/pysolorie/numerical_integration.py +++ b/src/pysolorie/numerical_integration.py @@ -24,6 +24,18 @@ class IrradiationCalculator: + r""" + A class to find the optimal orientation and + calculate the total direct irradiation for a solar panel [1]_. + + References + ---------- + .. [1] Aghamohammadi, A., & Foulaadvand, M. (2023). + Efficiency comparison between tracking and + optimally fixed flat solar collectors. Scientific Reports, 13(1). + + + """ OMEGA = 7.15 * 1e-5 def __init__( diff --git a/src/pysolorie/observer.py b/src/pysolorie/observer.py index ec3b620..e25c6c8 100644 --- a/src/pysolorie/observer.py +++ b/src/pysolorie/observer.py @@ -19,6 +19,11 @@ class Observer: + r""" + A class to model an observer based on horizontal and equatorial pictures + of the sun-earth geometry. + """ + def __init__( self, observer_latitude: Optional[float] = None, diff --git a/src/pysolorie/report.py b/src/pysolorie/report.py index 897133f..e743a84 100644 --- a/src/pysolorie/report.py +++ b/src/pysolorie/report.py @@ -21,6 +21,11 @@ class ReportGenerator: + r""" + A class to generate reports for the optimal orientation + of solar panels and the total direct irradiation. + """ + @logger_decorator def generate_optimal_orientation_csv_report( self, diff --git a/src/pysolorie/sun_position.py b/src/pysolorie/sun_position.py index aee67e0..98393d5 100644 --- a/src/pysolorie/sun_position.py +++ b/src/pysolorie/sun_position.py @@ -15,9 +15,13 @@ class SunPosition: + r""" + A class to model sun position. + """ + def solar_declination(self, day_of_year: int) -> float: r""" - Calculate the solar declination angle in radians. + Calculate the solar declination angle in radians [1]_. The solar declination angle is the angle between the rays of the sun and the plane of the Earth's equator. @@ -34,6 +38,11 @@ def solar_declination(self, day_of_year: int) -> float: :type day_of_year: int :return: The solar declination angle in radians. :rtype: float + + References + ---------- + .. [1] Cooper, P. (1969). The absorption of radiation in solar stills. + Solar Energy, 12(3), 333-346. """ # tilt of the Earth's axis (in degrees) earth_tilt_degrees = 23.45 From e5fb675f97f962736a79dc7ed6360a39a304716b Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Mon, 18 Dec 2023 07:37:40 +0330 Subject: [PATCH 6/6] Ready to release the version 1.4.0 --- docs/changelog.rst | 32 ++++++++++++++++++++++++++++++++ setup.cfg | 2 +- 2 files changed, 33 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 5787e2c..a9765e1 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,38 @@ Changelog ========= + +Version 1.4.0 +------------- + +Release date: 2023-12-18 + +Added +^^^^^ +- Added ``plot_total_direct_irradiation`` method in the Plotter class for plotting total direct irradiation over a specified range of days with an example included in getting_started.rst. + +Changed +^^^^^^^ +- Streamlined ``setup.cfg`` to remove unnecessary sphinx-apidoc commands. +- Optimized numerical integration methods to use radians and improved precision settings. +- Updated CSV report generation in ``ReportGenerator`` to include total direct irradiation in output. + +Documentation +^^^^^^^^^^^^^ +- Implemented significant restructuring and simplifying of the reStructuredText (rst) documentation across many files (modules.rst and individual module documentation). +- Updated module titles to match functionality more accurately, such as "Atmospheric Transmission" and "Hottel Model" for improved clarity in the table of contents. +- Standardized and enhanced docstrings in all module scripts to include detailed descriptions and references where applicable. + + +Testing +^^^^^^^ +- Extended tests in ``test_pysolorie.py`` for additional coverage of new features. + +Bug Fixes +^^^^^^^^^ +- Corrected the value and unit of the solar constant in ``SolarIrradiance`` from Watts to Megawatts per square meter. + + Version 1.3.1 ------------- diff --git a/setup.cfg b/setup.cfg index c76b125..1f488e1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = pysolorie -version = 1.3.1 +version = 1.4.0 description = Orientation Analysis of Solar Panel long_description = file: README.md long_description_content_type = text/markdown