From d1d000afaa0bed23e29b54c3d87f251658f526cc Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Thu, 31 Aug 2023 09:39:15 +0000 Subject: [PATCH 01/31] Add Apache License 2.0 --- LICENSE | 201 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From dfc34a6e0ca5e44aa7ecf907383ae3b48959c3f6 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Thu, 31 Aug 2023 11:09:05 +0000 Subject: [PATCH 02/31] Prepare source code for building package This commit prepares the source code for building a package by creating the necessary configuration files. The setup.cfg file defines the package metadata and dependencies, the pyproject.toml file defines the build system and dependencies, and the manifest.in file defines the graft point and excludes certain files. --- MANIFEST.in | 2 ++ pyproject.toml | 3 +++ setup.cfg | 23 +++++++++++++++++++++++ 3 files changed, 28 insertions(+) create mode 100644 MANIFEST.in create mode 100644 pyproject.toml create mode 100644 setup.cfg diff --git a/MANIFEST.in b/MANIFEST.in new file mode 100644 index 0000000..c5c8468 --- /dev/null +++ b/MANIFEST.in @@ -0,0 +1,2 @@ +graft src +recursive-exclude __pycache__ *.py[cod] diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9787c3b --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..2ce2f01 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,23 @@ +[metadata] +name = pysolorie +version = 1.0.0 +description = Orientation Analysis of Solar Panel +long_description = file: README.md +long_description_content_type = text/markdown +url = https://github.com/aaghamohammadi/pysolorie +author = Alireza Aghamohammadi +author_email = "Alireza Aghamohammadi" +license = {file = "LICENSE" } +classifiers = + License :: OSI Approved :: Apache Software License + +[options] +package_dir = + =src +packages = find: +include_package_data = True + +[options.packages.find] +where = src +exclude = + tests* From c196546e143975433b1955b76d44724606636b64 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Thu, 31 Aug 2023 11:11:36 +0000 Subject: [PATCH 03/31] Rename requirements.txt to dev-requirements.txt and update dependencies This commit renames the requirements.txt file to dev-requirements.txt and updates the dependencies listed in the file. --- requirements.txt => dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) rename requirements.txt => dev-requirements.txt (56%) diff --git a/requirements.txt b/dev-requirements.txt similarity index 56% rename from requirements.txt rename to dev-requirements.txt index c9d8775..a7a47bd 100644 --- a/requirements.txt +++ b/dev-requirements.txt @@ -1 +1,2 @@ +build>=0.10.0 pre-commit>=3.3.3 From 98f49c5ed006911115d8ac9562af8b6ec3a987b2 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Thu, 31 Aug 2023 11:14:00 +0000 Subject: [PATCH 04/31] Add copyright and license information to /src/pysolorie/__init__.py This commit adds copyright and license information to the /src/pysolorie/__init__.py file,specifying that the software is licensed under the Apache License, Version 2.0. --- src/pysolorie/__init__.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/pysolorie/__init__.py diff --git a/src/pysolorie/__init__.py b/src/pysolorie/__init__.py new file mode 100644 index 0000000..7150904 --- /dev/null +++ b/src/pysolorie/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Alireza Aghamohammadi + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. From 7e5a1e968ccdf1752bc0e58dfec5238b971f094a Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Tue, 5 Sep 2023 11:40:19 +0000 Subject: [PATCH 05/31] Update setup.cfg Updated the setup.cfg file to include configurations for pytest, coverage, and tox. These changes specify test paths, coverage source packages, branch coverage, reporting settings, and the tox environment. --- setup.cfg | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/setup.cfg b/setup.cfg index 2ce2f01..23a3a92 100644 --- a/setup.cfg +++ b/setup.cfg @@ -21,3 +21,32 @@ include_package_data = True where = src exclude = tests* + +[tool:pytest] +testpaths = tests +addopts = --cov --strict-markers +xfail_strict = True + +[coverage:run] +source = pysolorie +branch = True + +[coverage:report] +show_missing = True +skip_covered = True + +[coverage:paths] +source = + src/pysolorie + */site-packages/pysolorie + +[tox:tox] +envlist = py38 +isolated_build = True + +[testenv] +deps = + pytest + pytest-cov +commands = + pytest {posargs} From f342ae013ed71afb153b0d3606255ec07a348c65 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Tue, 5 Sep 2023 11:54:39 +0000 Subject: [PATCH 06/31] Add mypy type checking, code formatting, and linting configurations to setup.cfg Added a new section [mypy] to configure mypy, a static type-checking tool for Python. The python_version setting specifies the target Python version for type checking. The warn_unused_configs option enables warnings for unused mypy configuration options.The show_error_context option displays error messages with additional context information. The pretty option formats error messages in a more readable manner. The namespace_packages option enables namespace package support during type checking. The check_untyped_defs option raises errors for untyped function and method definitions. Introduced a new test environment [testenv:typecheck] to run mypy type checking. The dependencies mypy and pytest are specified for the typecheck environment. The mypy command is executed with the --ignore-missing-imports flag to perform type checking on the src and tests directories. Included a new test environment [testenv:format] to run code formatting checks. The environment skips installing dependencies and uses the black code formatter. The black command with the --check and --diff options is executed to check and display formatting changes in the src and tests directories. Added a new test environment [testenv:lint] to run code linting checks. The environment skips installing dependencies and uses the flake8 and flake8-bugbear linters. The flake8 command is executed to perform linting on the src and tests directories. --- setup.cfg | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/setup.cfg b/setup.cfg index 23a3a92..64ba2dc 100644 --- a/setup.cfg +++ b/setup.cfg @@ -22,6 +22,14 @@ where = src exclude = tests* +[mypy] +python_version = 3.8 +warn_unused_configs = True +show_error_context = True +pretty = True +namespace_packages = True +check_untyped_defs = True + [tool:pytest] testpaths = tests addopts = --cov --strict-markers @@ -50,3 +58,25 @@ deps = pytest-cov commands = pytest {posargs} + +[testenv:typecheck] +deps = + mypy + pytest +commands = + mypy --ignore-missing-imports {posargs:src tests} + +[testenv:format] +skip_install = True +deps = + black +commands = + black {posargs:--check --diff src tests} + +[testenv:lint] +skip_install = True +deps = + flake8 + flake8-bugbear +commands = + flake8 {posargs:src tests} From 5f5a9d6bcb0bfb28c427e6285fdd0425be780f43 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Tue, 5 Sep 2023 12:00:50 +0000 Subject: [PATCH 07/31] Add tox to the development requirements --- dev-requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/dev-requirements.txt b/dev-requirements.txt index a7a47bd..68d4cc7 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,2 +1,3 @@ build>=0.10.0 pre-commit>=3.3.3 +tox>=4.11.1 From 3a43da0d144918ce00dabf695c134392faafa5c0 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Sat, 23 Sep 2023 08:49:49 +0000 Subject: [PATCH 08/31] Add HottelModel class for clear-sky radiation transmittance estimation This commit introduces the HottelModel class, which implements the Hottel Model for estimating clear-sky beam radiation transmittance based on climate type and observer altitude. --- src/pysolorie/__init__.py | 4 ++ src/pysolorie/pysolorie.py | 135 +++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+) create mode 100644 src/pysolorie/pysolorie.py diff --git a/src/pysolorie/__init__.py b/src/pysolorie/__init__.py index 7150904..61dd9fe 100644 --- a/src/pysolorie/__init__.py +++ b/src/pysolorie/__init__.py @@ -11,3 +11,7 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. + +from .pysolorie import HottelModel + +__all__ = ["HottelModel"] diff --git a/src/pysolorie/pysolorie.py b/src/pysolorie/pysolorie.py new file mode 100644 index 0000000..f4352a9 --- /dev/null +++ b/src/pysolorie/pysolorie.py @@ -0,0 +1,135 @@ +# Copyright 2023 Alireza Aghamohammadi + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from typing import Dict, Tuple + + +class HottelModel: + r""" + Hottel Model for estimating clear-sky beam radiation transmittance + based on climate type, and observer altitude. + + :ivar CLIMATE_CONSTANTS: Correction factors for different climate types (\(r_0\), \(r_1\), and \(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), + } + + def _convert_to_km(self, observer_altitude: int) -> float: + r""" + Convert altitude from meters to kilometers. + + :param observer_altitude: Altitude of the observer in meters. + :type observer_altitude: float + :return: Altitude in kilometers. + :rtype: float + """ + return observer_altitude / 1000.0 + + def _calculate_a0_star(self, observer_altitude: int) -> float: + r""" + Calculate \(a_0^*\) based on observer altitude. + + Formula: + \[ + a_0^* = 0.4237 - 0.00821 \cdot (6 - A)^2 + \] + + :param observer_altitude: Altitude of the observer in meters. + :type observer_altitude: float + :return: \(a_0^*\) value. + :rtype: float + """ + observer_altitude_km = self._convert_to_km(observer_altitude) + observer_altitude_diff = 6.0 - observer_altitude_km + return 0.4237 - 0.00821 * observer_altitude_diff**2 + + def _calculate_a1_star(self, observer_altitude: int) -> float: + r""" + Calculate \(a_1^*\) based on observer altitude. + + Formula: + \[ + a_1^* = 0.5055 + 0.00595 \cdot (6.5 - A)^2 + \] + + :param observer_altitude: Altitude of the observer in meters. + :type observer_altitude: float + :return: \(a_1^*\) value. + :rtype: float + """ + observer_altitude_km = self._convert_to_km(observer_altitude) + observer_altitude_diff = 6.5 - observer_altitude_km + return 0.5055 + 0.00595 * observer_altitude_diff**2 + + def _calculate_k_star(self, observer_altitude: int) -> float: + r""" + Calculate \(k^*\) based on observer altitude. + + Formula: + \[ + k^* = 0.2711 + 0.01858 \cdot (2.5 - A)^2 + \] + + :param observer_altitude: Altitude of the observer in meters. + :type observer_altitude: float + :return: \(k^*\) value. + :rtype: float + """ + observer_altitude_km = self._convert_to_km(observer_altitude) + observer_altitude_diff = 2.5 - observer_altitude_km + return 0.2711 + 0.01858 * observer_altitude_diff**2 + + def calculate_transmittance_components( + self, climate_type: str, observer_altitude: int + ) -> Tuple[float, float, float]: + r""" + Calculate the components of clear-sky beam radiation transmittance (\(a_0\), \(a_1\), and \(k\)) + based on climate type and observer altitude. + + Correction factors adjust the clear-sky beam radiation transmittance components. + + Formula: + \[ + a_0 = r_0 \cdot a_0^* + a_1 = r_1 \cdot a_1^* + k = r_k \cdot k^* + \] + + :param climate_type: Climate type (e.g., "TROPICAL"). + :type climate_type: str + :param observer_altitude: Altitude of the observer in meters. + :type observer_altitude: float + :return: Components of clear-sky beam radiation transmittance (\(a_0\), \(a_1\), \(k\)). + :rtype: tuple of floats + :raises ValueError: If an invalid climate type is provided. + """ + if climate_type.upper() not in self.CLIMATE_CONSTANTS: + raise ValueError("Invalid climate type") + + r0, r1, rk = self.CLIMATE_CONSTANTS[climate_type.upper()] + + a0_star = self._calculate_a0_star(observer_altitude) + a1_star = self._calculate_a1_star(observer_altitude) + k_star = self._calculate_k_star(observer_altitude) + + a0 = r0 * a0_star + a1 = r1 * a1_star + k = rk * k_star + + return a0, a1, k From 38d985ddc73bf3bee761ba4ab75eca237bfa4440 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Sat, 23 Sep 2023 08:52:14 +0000 Subject: [PATCH 09/31] Apply code formatting changes and improve linting configuration - In pyproject.toml, added black configuration for line length (120) - In setup.cfg, increased max-line-length for flake8 to 120 characters. - Enabled isort checks for code formatting. --- pyproject.toml | 4 ++++ setup.cfg | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 9787c3b..01e74c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,7 @@ [build-system] requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" + +[tool.black] +line-length = 120 +target-version = ['py38'] diff --git a/setup.cfg b/setup.cfg index 64ba2dc..29e60f3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,9 @@ pretty = True namespace_packages = True check_untyped_defs = True +[flake8] +max-line-length = 120 + [tool:pytest] testpaths = tests addopts = --cov --strict-markers @@ -64,14 +67,16 @@ deps = mypy pytest commands = - mypy --ignore-missing-imports {posargs:src tests} + mypy {posargs:src tests} [testenv:format] skip_install = True deps = black + isort commands = black {posargs:--check --diff src tests} + isort {posargs:--check --diff --profile=black src tests} [testenv:lint] skip_install = True From d8a76626cca5bc84e221eaada84aa587966c86ed Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Sat, 23 Sep 2023 08:54:16 +0000 Subject: [PATCH 10/31] Add test cases for HottelModel in test_pysolorie.py - Included parameterized tests for different climate types and observer altitudes, validating calculated results. - Added a test case to check for proper error handling when an invalid climate type is provided. --- tests/__init__.py | 13 +++++++++++++ tests/test_pysolorie.py | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) create mode 100644 tests/__init__.py create mode 100644 tests/test_pysolorie.py diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..7150904 --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1,13 @@ +# Copyright 2023 Alireza Aghamohammadi + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py new file mode 100644 index 0000000..fa03016 --- /dev/null +++ b/tests/test_pysolorie.py @@ -0,0 +1,38 @@ +# Copyright 2023 Alireza Aghamohammadi + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import pytest + +import pysolorie + +hottel_model = pysolorie.HottelModel() + + +@pytest.mark.parametrize( + "climate_type, observer_altitude, expected_result", + [ + ("MIDLATITUDE SUMMER", 1200, (0.228, 0.666, 0.309)), # Tehran Summer + ("MIDLATITUDE WINTER", 1200, (0.242, 0.679, 0.303)), # Tehran Winter + ("TROPICAL", 63, (0.128, 0.737, 0.389)), # Medan + ("TROPICAL", 136, (0.134, 0.732, 0.382)), # Fairbanks + ], +) +def test_calculate_transmittance_components(climate_type, observer_altitude, expected_result): + result = hottel_model.calculate_transmittance_components(climate_type, observer_altitude) + assert pytest.approx(result, abs=1e-3) == expected_result + + +def test_invalid_climate_type(): + with pytest.raises(ValueError, match="Invalid climate type"): + hottel_model.calculate_transmittance_components("INVALID", 1000) From b3a7de9993345a65ebc3225aa536ae508971b25e Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Thu, 16 Nov 2023 15:14:13 +0330 Subject: [PATCH 11/31] Fix climate type for Fairbanks in test cases In the test cases for pysolorie, the climate type for Fairbanks was incorrectly set to 'TROPICAL'. This commit corrects the climate type to 'SUBARCTIC SUMMER', which is more accurate for the location. --- tests/test_pysolorie.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index fa03016..f9e11a1 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -25,7 +25,7 @@ ("MIDLATITUDE SUMMER", 1200, (0.228, 0.666, 0.309)), # Tehran Summer ("MIDLATITUDE WINTER", 1200, (0.242, 0.679, 0.303)), # Tehran Winter ("TROPICAL", 63, (0.128, 0.737, 0.389)), # Medan - ("TROPICAL", 136, (0.134, 0.732, 0.382)), # Fairbanks + ("SUBARCTIC SUMMER", 136, (0.140, 0.739, 0.379)), # Fairbanks ], ) def test_calculate_transmittance_components(climate_type, observer_altitude, expected_result): From 5826fe2c711b2d2b91415fe2ad72c16c96f7e8c7 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Thu, 16 Nov 2023 15:16:07 +0330 Subject: [PATCH 12/31] Refactor pysolorie.py to model.py In this commit, the file pysolorie.py has been refactored and renamed to model.py. The import statement in __init__.py has been updated accordingly to reflect this change. --- src/pysolorie/__init__.py | 2 +- src/pysolorie/{pysolorie.py => model.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/pysolorie/{pysolorie.py => model.py} (100%) diff --git a/src/pysolorie/__init__.py b/src/pysolorie/__init__.py index 61dd9fe..9063594 100644 --- a/src/pysolorie/__init__.py +++ b/src/pysolorie/__init__.py @@ -12,6 +12,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .pysolorie import HottelModel +from .model import HottelModel __all__ = ["HottelModel"] diff --git a/src/pysolorie/pysolorie.py b/src/pysolorie/model.py similarity index 100% rename from src/pysolorie/pysolorie.py rename to src/pysolorie/model.py From 96bed3c3590e87812e649910478b8c9f5b7a256e Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Thu, 16 Nov 2023 15:17:48 +0330 Subject: [PATCH 13/31] Upgrade Python version from 3.8 to 3.10 In this commit, the Python version used for tox, mypy, and black has been upgraded from 3.8 to 3.10. The changes are reflected in the pyproject.toml and setup.cfg files. --- pyproject.toml | 2 +- setup.cfg | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 01e74c2..aab8637 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,4 +4,4 @@ build-backend = "setuptools.build_meta" [tool.black] line-length = 120 -target-version = ['py38'] +target-version = ['py310'] diff --git a/setup.cfg b/setup.cfg index 29e60f3..f71d96e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,7 @@ exclude = tests* [mypy] -python_version = 3.8 +python_version = 3.10 warn_unused_configs = True show_error_context = True pretty = True @@ -52,7 +52,7 @@ source = */site-packages/pysolorie [tox:tox] -envlist = py38 +envlist = py310 isolated_build = True [testenv] From 5c45c9e624cf0b63d4d97bd70cc83cea0f595371 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Mon, 20 Nov 2023 16:03:55 +0330 Subject: [PATCH 14/31] Add SunPosition class Added a new file `sun_position.py` in the `src/pysolorie` directory. This file contains the `SunPosition` class, which has methods to calculate the solar declination angle and the hour angle based on the day of the year and the solar time. Updated `__init__.py` in the `src/pysolorie` directory to import the `SunPosition` class and include it in the `__all__` list. This makes the `SunPosition` class part of the public API of the `pysolorie` package. Added new test cases for the `SunPosition` class. --- src/pysolorie/__init__.py | 3 +- src/pysolorie/sun_position.py | 83 +++++++++++++++++++++++++++++++++++ tests/test_pysolorie.py | 22 +++++++++- 3 files changed, 105 insertions(+), 3 deletions(-) create mode 100644 src/pysolorie/sun_position.py diff --git a/src/pysolorie/__init__.py b/src/pysolorie/__init__.py index 9063594..b641ed0 100644 --- a/src/pysolorie/__init__.py +++ b/src/pysolorie/__init__.py @@ -13,5 +13,6 @@ # limitations under the License. from .model import HottelModel +from .sun_position import SunPosition -__all__ = ["HottelModel"] +__all__ = ["HottelModel", "SunPosition"] diff --git a/src/pysolorie/sun_position.py b/src/pysolorie/sun_position.py new file mode 100644 index 0000000..052c953 --- /dev/null +++ b/src/pysolorie/sun_position.py @@ -0,0 +1,83 @@ +# Copyright 2023 Alireza Aghamohammadi + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + + +class SunPosition: + def __init__(self, day_of_year: int, solar_time: float): + """ + Initialize the SunPosition class. + + :param day_of_year: The day of the year. + :type day_of_year: int + :param solar_time: The solar time in hours. + :type solar_time: float + """ + self.day_of_year = day_of_year + self.solar_time = solar_time + + @property + def solar_declination(self) -> float: + r""" + Calculate the solar declination angle in radians. + + The solar declination angle is the angle between the rays of the sun and the plane of the Earth's equator. + + Formula: + \[ + \delta = 23.45 * \sin \left( \frac{2 \pi}{365} * (284 + n) \right) + \] + + :return: The solar declination angle in radians. + :rtype: float + """ + # Maximum tilt of the Earth's axis (in degrees) + max_earth_tilt_degrees = 23.45 + + # Convert the tilt to radians + max_earth_tilt_radians = math.radians(max_earth_tilt_degrees) + + # Offset to ensure declination angle is zero at the March equinox + equinox_offset_days = 284 + + # Calculate the solar declination angle + solar_declination = max_earth_tilt_radians * math.sin( + (2 * math.pi) * (equinox_offset_days + self.day_of_year) / 365 + ) + + return solar_declination + + @property + def hour_angle(self) -> float: + r""" + Calculate the hour angle based on the solar time. + + The hour angle is a measure of time, expressed in angular terms, from solar noon. + + Formula: + \[ + \omega = (t - 12) * 15 + \] + + :return: The hour angle in degrees. + :rtype: float + """ + # The Earth rotates by 15 degrees per hour + earth_rotation_rate = 15 + + # Calculate the hour angle + hour_angle = (self.solar_time - 12) * earth_rotation_rate + + return hour_angle diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index f9e11a1..f3a7c2d 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -12,11 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. + import pytest -import pysolorie +from pysolorie import HottelModel, SunPosition -hottel_model = pysolorie.HottelModel() +hottel_model = HottelModel() @pytest.mark.parametrize( @@ -36,3 +37,20 @@ def test_calculate_transmittance_components(climate_type, observer_altitude, exp def test_invalid_climate_type(): with pytest.raises(ValueError, match="Invalid climate type"): hottel_model.calculate_transmittance_components("INVALID", 1000) + + +@pytest.mark.parametrize( + "day_of_year, solar_time, expected_declination, expected_hour_angle", + [ + (1, 12, -0.4014257279586958, 0), # January 1st at noon + (81, 10, 0, -30), # March 22nd at 2pm (equinox) + (81, 12, 0, 0), # March 22nd at noon (equinox) + (1, 13, -0.4014257279586958, 15), # January 1st at 1pm + ], +) +def test_sun_position(day_of_year, solar_time, expected_declination, expected_hour_angle): + sun_position = SunPosition(day_of_year, solar_time) + declination = sun_position.solar_declination + hour_angle = sun_position.hour_angle + assert declination == pytest.approx(expected_declination, abs=1e-3) + assert hour_angle == expected_hour_angle From 5cd007cddafb033bc9721fe41d8fa8971ac9f109 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Tue, 21 Nov 2023 16:55:24 +0330 Subject: [PATCH 15/31] Refactor SunPosition class and update tests Refactored the `SunPosition` class to remove the constructor and make `solar_declination` and `hour_angle` methods take parameters instead of using instance variables. Updated the `solar_declination` method to directly use the tilt of the Earth's axis in degrees, which simplifies the formula and makes it more readable. Updated the `hour_angle` method to calculate the hour angle in radians instead of degrees, and to use the solar time in seconds instead of hours. Updated the tests to reflect these changes and added the `math` module for the tests. --- src/pysolorie/sun_position.py | 47 ++++++++++++++--------------------- tests/test_pysolorie.py | 17 +++++++------ 2 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/pysolorie/sun_position.py b/src/pysolorie/sun_position.py index 052c953..cf44a16 100644 --- a/src/pysolorie/sun_position.py +++ b/src/pysolorie/sun_position.py @@ -16,20 +16,7 @@ class SunPosition: - def __init__(self, day_of_year: int, solar_time: float): - """ - Initialize the SunPosition class. - - :param day_of_year: The day of the year. - :type day_of_year: int - :param solar_time: The solar time in hours. - :type solar_time: float - """ - self.day_of_year = day_of_year - self.solar_time = solar_time - - @property - def solar_declination(self) -> float: + def solar_declination(self, day_of_year: int) -> float: r""" Calculate the solar declination angle in radians. @@ -37,30 +24,29 @@ def solar_declination(self) -> float: Formula: \[ - \delta = 23.45 * \sin \left( \frac{2 \pi}{365} * (284 + n) \right) + \delta = \sin \left( \frac{2 \pi}{365} * (284 + day_of_year) \right) * \left(\frac{23.45 \pi}{180}\right) \] + :param day_of_year: The day of the year. + :type day_of_year: int :return: The solar declination angle in radians. :rtype: float """ - # Maximum tilt of the Earth's axis (in degrees) - max_earth_tilt_degrees = 23.45 + # tilt of the Earth's axis (in degrees) + earth_tilt_degrees = 23.45 # Convert the tilt to radians - max_earth_tilt_radians = math.radians(max_earth_tilt_degrees) + earth_tilt_radians = math.radians(earth_tilt_degrees) # Offset to ensure declination angle is zero at the March equinox equinox_offset_days = 284 # Calculate the solar declination angle - solar_declination = max_earth_tilt_radians * math.sin( - (2 * math.pi) * (equinox_offset_days + self.day_of_year) / 365 - ) + solar_declination = math.sin((2 * math.pi) * (equinox_offset_days + day_of_year) / 365) * earth_tilt_radians return solar_declination - @property - def hour_angle(self) -> float: + def hour_angle(self, solar_time: float) -> float: r""" Calculate the hour angle based on the solar time. @@ -68,16 +54,21 @@ def hour_angle(self) -> float: Formula: \[ - \omega = (t - 12) * 15 + \omega = (t - seconds_in_half_day) * \frac{\pi}{seconds_in_half_day} \] - :return: The hour angle in degrees. + :param solar_time: The solar time in seconds. + :type solar_time: float + :return: The hour angle in radians. :rtype: float """ - # The Earth rotates by 15 degrees per hour - earth_rotation_rate = 15 + # The number of seconds in half a day (12 hours) + seconds_in_half_day = 12 * 60 * 60 + + # The Earth rotates by pi/seconds_in_half_day radians per second + earth_rotation_rate = math.pi / seconds_in_half_day # Calculate the hour angle - hour_angle = (self.solar_time - 12) * earth_rotation_rate + hour_angle = (solar_time - seconds_in_half_day) * earth_rotation_rate return hour_angle diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index f3a7c2d..fa5cbe5 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -12,6 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import math import pytest @@ -42,15 +43,15 @@ def test_invalid_climate_type(): @pytest.mark.parametrize( "day_of_year, solar_time, expected_declination, expected_hour_angle", [ - (1, 12, -0.4014257279586958, 0), # January 1st at noon - (81, 10, 0, -30), # March 22nd at 2pm (equinox) - (81, 12, 0, 0), # March 22nd at noon (equinox) - (1, 13, -0.4014257279586958, 15), # January 1st at 1pm + (1, 12 * 60 * 60, -0.4014257279586958, 0), # January 1st at noon + (81, 10 * 60 * 60, 0, -math.pi / 6), # March 22nd at 10am (equinox) + (81, 12 * 60 * 60, 0, 0), # March 22nd at noon (equinox) + (1, 13 * 60 * 60, -0.4014257279586958, math.pi / 12), # January 1st at 1pm ], ) def test_sun_position(day_of_year, solar_time, expected_declination, expected_hour_angle): - sun_position = SunPosition(day_of_year, solar_time) - declination = sun_position.solar_declination - hour_angle = sun_position.hour_angle + sun_position = SunPosition() + declination = sun_position.solar_declination(day_of_year) + hour_angle = sun_position.hour_angle(solar_time) assert declination == pytest.approx(expected_declination, abs=1e-3) - assert hour_angle == expected_hour_angle + assert hour_angle == pytest.approx(expected_hour_angle, abs=1e-3) From 572a296ff75bfc7f917683fac553a0f9cda8a046 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Mon, 27 Nov 2023 16:17:57 +0330 Subject: [PATCH 16/31] Add Type Annotations to Test Functions In this commit, type annotations were added to the test functions in the `test_pysolorie.py` file. This was done to improve the readability and maintainability of the code by providing more explicit information about the types of values that functions are expected to handle. Changes include: 1. Importing the `Tuple` type from the `typing` module. 2. Adding type annotations to the `test_calculate_transmittance_components` function. The function now explicitly states that it expects `climate_type` to be a string, `observer_altitude` to be an integer, and `expected_result` to be a tuple of three floats. It also specifies that it does not return a value. 3. Adding type annotations to the `test_invalid_climate_type` function to indicate that it does not return a value. --- tests/test_pysolorie.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index fa5cbe5..fffe446 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -13,6 +13,7 @@ # limitations under the License. import math +from typing import Tuple import pytest @@ -30,12 +31,17 @@ ("SUBARCTIC SUMMER", 136, (0.140, 0.739, 0.379)), # Fairbanks ], ) -def test_calculate_transmittance_components(climate_type, observer_altitude, expected_result): - result = hottel_model.calculate_transmittance_components(climate_type, observer_altitude) +def test_calculate_transmittance_components( + climate_type: str, observer_altitude: int, expected_result: Tuple[float, float, float] +) -> None: + hottel_model: HottelModel = HottelModel() + result: Tuple[float, float, float] = hottel_model.calculate_transmittance_components( + climate_type, observer_altitude + ) assert pytest.approx(result, abs=1e-3) == expected_result -def test_invalid_climate_type(): +def test_invalid_climate_type() -> None: with pytest.raises(ValueError, match="Invalid climate type"): hottel_model.calculate_transmittance_components("INVALID", 1000) From 1c301c3050513b8b68feefb546eb1b224b11b55a Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Mon, 27 Nov 2023 16:24:32 +0330 Subject: [PATCH 17/31] Add Type Annotations to Sun Position Test Function In this commit, type annotations were added to the `test_sun_position` function in the `test_pysolorie.py` file. Changes include: 1. Adding type annotations to the `test_sun_position` function. The function now explicitly states that it expects `day_of_year` to be an integer, `solar_time` to be an integer, `expected_declination` to be a float, and `expected_hour_angle` to be a float. It also specifies that it does not return a value. 2. Instantiating the `SunPosition` class and assigning it to the `sun_position` variable. 3. Calling the `solar_declination` and `hour_angle` methods on the `sun_position` object and assigning their return values to the `declination` and `hour_angle` variables, respectively. --- tests/test_pysolorie.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index fffe446..9695063 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -55,9 +55,11 @@ def test_invalid_climate_type() -> None: (1, 13 * 60 * 60, -0.4014257279586958, math.pi / 12), # January 1st at 1pm ], ) -def test_sun_position(day_of_year, solar_time, expected_declination, expected_hour_angle): - sun_position = SunPosition() - declination = sun_position.solar_declination(day_of_year) - hour_angle = sun_position.hour_angle(solar_time) +def test_sun_position( + day_of_year: int, solar_time: int, expected_declination: float, expected_hour_angle: float +) -> None: + sun_position: SunPosition = SunPosition() + declination: float = sun_position.solar_declination(day_of_year) + hour_angle: float = sun_position.hour_angle(solar_time) assert declination == pytest.approx(expected_declination, abs=1e-3) assert hour_angle == pytest.approx(expected_hour_angle, abs=1e-3) From 92222c1ce49a8f9b0808a13be8b0322a329c9174 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Mon, 27 Nov 2023 16:25:55 +0330 Subject: [PATCH 18/31] Refactor Test Suite and Add New Test for Solar Irradiance Calculation Add a new test for the calculation of extraterrestrial irradiance. Changes include: 1. The import statement was refactored to import `SolarIrradiance` from the `pysolorie` module. 2. The global instantiation of `HottelModel` was removed. Instead, `HottelModel` is now instantiated within the `test_invalid_climate_type` function. This change improves the isolation of the tests and ensures that each test is working with a fresh instance of `HottelModel`. 3. A new test function, `test_calculate_extraterrestrial_irradiance`, was added. This function tests the `calculate_extraterrestrial_irradiance` method of the `SolarIrradiance` class. The test is parameterized with different days of the year and the expected irradiance for those days. --- tests/test_pysolorie.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index 9695063..b386bca 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -17,9 +17,7 @@ import pytest -from pysolorie import HottelModel, SunPosition - -hottel_model = HottelModel() +from pysolorie import HottelModel, SolarIrradiance, SunPosition @pytest.mark.parametrize( @@ -43,6 +41,7 @@ def test_calculate_transmittance_components( def test_invalid_climate_type() -> None: with pytest.raises(ValueError, match="Invalid climate type"): + hottel_model: HottelModel = HottelModel() hottel_model.calculate_transmittance_components("INVALID", 1000) @@ -63,3 +62,20 @@ def test_sun_position( hour_angle: float = sun_position.hour_angle(solar_time) assert declination == pytest.approx(expected_declination, abs=1e-3) assert hour_angle == pytest.approx(expected_hour_angle, abs=1e-3) + + +@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) + ], +) +def test_calculate_extraterrestrial_irradiance(day_of_year: int, expected_irradiance: float) -> None: + sun_position = SunPosition() + solar_irradiance = SolarIrradiance(sun_position) + irradiance: float = solar_irradiance.calculate_extraterrestrial_irradiance(day_of_year) + assert irradiance == pytest.approx(expected_irradiance, abs=1e-3) From cd18786181ca3dbd4c80565a17ddac8aa1316c60 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Mon, 27 Nov 2023 16:28:38 +0330 Subject: [PATCH 19/31] Add SolarIrradiance Class and Update Module Imports This commit includes several changes to the `pysolorie` module to add a new class for calculating solar irradiance and update the module's imports. Changes include: 1. The import statement in the `__init__.py` file was updated to import the `SolarIrradiance` class from the `irradiance` module. The `__all__` list was also updated to include `SolarIrradiance`. 2. A new file, `irradiance.py`, was added to the `pysolorie` module. This file contains the `SolarIrradiance` class, which includes methods for calculating the extraterrestrial solar irradiance for a given day of the year. The `SolarIrradiance` class includes a method `calculate_extraterrestrial_irradiance` that calculates the amount of solar energy received per unit area on a surface perpendicular to the Sun's rays outside Earth's atmosphere. The calculation takes into account the variation in the Earth-Sun distance due to the Earth's elliptical orbit. --- src/pysolorie/__init__.py | 3 +- src/pysolorie/irradiance.py | 64 +++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+), 1 deletion(-) create mode 100644 src/pysolorie/irradiance.py diff --git a/src/pysolorie/__init__.py b/src/pysolorie/__init__.py index b641ed0..b150ffe 100644 --- a/src/pysolorie/__init__.py +++ b/src/pysolorie/__init__.py @@ -12,7 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from .irradiance import SolarIrradiance from .model import HottelModel from .sun_position import SunPosition -__all__ = ["HottelModel", "SunPosition"] +__all__ = ["HottelModel", "SunPosition", "SolarIrradiance"] diff --git a/src/pysolorie/irradiance.py b/src/pysolorie/irradiance.py new file mode 100644 index 0000000..fc73b29 --- /dev/null +++ b/src/pysolorie/irradiance.py @@ -0,0 +1,64 @@ +# Copyright 2023 Alireza Aghamohammadi + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math + +from .sun_position import SunPosition + + +class SolarIrradiance: + def __init__(self, sun_position: SunPosition): + """ + Initialize the SolarIrradiance class. + + :param sun_position: An instance of the SunPosition class. + :type sun_position: SunPosition + """ + self.sun_position = sun_position + + 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, 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 formula used is: + \[ + E = SOLAR_CONSTANT * (1 + 0.33 * cos (2 * pi * day_of_year / 365)) + \] + + where: + - SOLAR_CONSTANT is the average solar radiation arriving outside 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. + + :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. + :rtype: float + """ + # Solar constant (W/m^2) + SOLAR_CONSTANT = 1367 + + # Factor to account for the Earth's orbital eccentricity + earth_orbital_eccentricity = 0.033 + + # Calculate the extraterrestrial solar irradiance + extraterrestrial_irradiance = SOLAR_CONSTANT * ( + 1 + earth_orbital_eccentricity * math.cos(2 * math.pi * day_of_year / 365) + ) + + return extraterrestrial_irradiance From df56ba9cd6adc5dbc4e155c38113f2492a001306 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Tue, 5 Dec 2023 18:26:48 +0330 Subject: [PATCH 20/31] Add Observer class and update tests - Added a new Observer class in the pysolorie module. This class includes methods to initialize the observer's latitude and longitude, and to calculate the zenith angle. - The Observer class takes optional parameters for the observer's latitude and longitude, and converts these values to radians. - The calculate_zenith_angle method in the Observer class calculates the zenith angle using the observer's latitude, the solar declination, and the hour angle. - Updated the test_pysolorie.py file to include tests for the new Observer class. - The tests cover various scenarios including different locations and times. --- .pre-commit-config.yaml | 2 +- pyproject.toml | 2 +- setup.cfg | 2 +- src/pysolorie/observer.py | 77 +++++++++++++++++++++++++++++++++++++++ tests/test_pysolorie.py | 73 +++++++++++++++++++++++++++++++++---- 5 files changed, 145 insertions(+), 11 deletions(-) create mode 100644 src/pysolorie/observer.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71c9e92..657ffda 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ repos: - id: trailing-whitespace - id: requirements-txt-fixer - repo: https://github.com/psf/black - rev: 23.7.0 + rev: 23.11.0 hooks: - id: black diff --git a/pyproject.toml b/pyproject.toml index aab8637..c066da7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,5 +3,5 @@ requires = ["setuptools", "wheel"] build-backend = "setuptools.build_meta" [tool.black] -line-length = 120 +line-length = 88 target-version = ['py310'] diff --git a/setup.cfg b/setup.cfg index f71d96e..0d24b6e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,7 @@ namespace_packages = True check_untyped_defs = True [flake8] -max-line-length = 120 +max-line-length = 88 [tool:pytest] testpaths = tests diff --git a/src/pysolorie/observer.py b/src/pysolorie/observer.py new file mode 100644 index 0000000..d6db028 --- /dev/null +++ b/src/pysolorie/observer.py @@ -0,0 +1,77 @@ +# Copyright 2023 Alireza Aghamohammadi + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from typing import Optional + +from .sun_position import SunPosition + + +class Observer: + def __init__( + self, + observer_latitude: Optional[float] = None, + observer_longitude: Optional[float] = None, + ): + """ + Initialize the Observer class. + + :param observer_latitude: The latitude of the observer in degrees (optional). + :type observer_latitude: Optional[float] + :param observer_longitude: The longitude of the observer in degrees (optional). + :type observer_longitude: Optional[float] + """ + self.observer_latitude = ( + math.radians(observer_latitude) if observer_latitude is not None else None + ) + self.observer_longitude = ( + math.radians(observer_longitude) if observer_longitude is not None else None + ) + self.sun_position = SunPosition() + + def calculate_zenith_angle(self, day_of_year: int, solar_time: float) -> float: + """ + Calculate the zenith angle. + + The zenith angle is calculated using the formula: + + .. math:: + \cos(\theta_z) = \sin(\phi) \cdot \sin(\delta) + \cos(\phi) \cdot \cos(\delta) \cdot \cos(\omega) + + where: + \(\theta_z\) is the zenith angle, + \(\phi\) is the latitude of the observer, + \(\delta\) is the solar declination, and + \(\omega\) is the hour angle. + + :param day_of_year: The day of the year. + :type day_of_year: int + :param solar_time: The solar time in seconds. + :type solar_time: float + :return: The zenith angle in radians. + :rtype: float + """ + if self.observer_latitude is None: + raise ValueError( + "Observer latitude must be provided to calculate zenith angle." + ) + + solar_declination = self.sun_position.solar_declination(day_of_year) + hour_angle = self.sun_position.hour_angle(solar_time) + return math.acos( + math.sin(self.observer_latitude) * math.sin(solar_declination) + + math.cos(self.observer_latitude) + * math.cos(solar_declination) + * math.cos(hour_angle) + ) diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index b386bca..f567582 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -17,7 +17,7 @@ import pytest -from pysolorie import HottelModel, SolarIrradiance, SunPosition +from pysolorie import HottelModel, Observer, SolarIrradiance, SunPosition @pytest.mark.parametrize( @@ -30,12 +30,14 @@ ], ) def test_calculate_transmittance_components( - climate_type: str, observer_altitude: int, expected_result: Tuple[float, float, float] + climate_type: str, + observer_altitude: int, + expected_result: Tuple[float, float, float], ) -> None: hottel_model: HottelModel = HottelModel() - result: Tuple[float, float, float] = hottel_model.calculate_transmittance_components( - climate_type, observer_altitude - ) + result: Tuple[ + float, float, float + ] = hottel_model.calculate_transmittance_components(climate_type, observer_altitude) assert pytest.approx(result, abs=1e-3) == expected_result @@ -55,7 +57,10 @@ def test_invalid_climate_type() -> None: ], ) def test_sun_position( - day_of_year: int, solar_time: int, expected_declination: float, expected_hour_angle: float + day_of_year: int, + solar_time: int, + expected_declination: float, + expected_hour_angle: float, ) -> None: sun_position: SunPosition = SunPosition() declination: float = sun_position.solar_declination(day_of_year) @@ -74,8 +79,60 @@ def test_sun_position( (355, 1411.444), # December 21st (winter solstice) ], ) -def test_calculate_extraterrestrial_irradiance(day_of_year: int, expected_irradiance: float) -> None: +def test_calculate_extraterrestrial_irradiance( + day_of_year: int, expected_irradiance: float +) -> None: sun_position = SunPosition() solar_irradiance = SolarIrradiance(sun_position) - irradiance: float = solar_irradiance.calculate_extraterrestrial_irradiance(day_of_year) + irradiance: float = solar_irradiance.calculate_extraterrestrial_irradiance( + day_of_year + ) assert irradiance == pytest.approx(expected_irradiance, abs=1e-3) + + +@pytest.mark.parametrize( + "observer_latitude, observer_longitude, day_of_year, solar_time, expected_zenith_angle", + [ + ( + 35.69, + 51.39, + 81, + 12 * 60 * 60, + 0.623, + ), # Test at Tehran on the 81st day of the year at noon + ( + 3.59, + 98.67, + 81, + 10 * 60 * 60, + 0.527, + ), # Test at Medan on the 81st day of the year at 10am + ( + 64.84, + -147.72, + 1, + 13 * 60 * 60, + 1.547, + ), # Test at Fairbanks on the 1st day of the year at 1pm + (90, 0, 1, 13 * 60 * 60, 1.972), # Test at North Pole on January 1st at 1pm + ], +) +def test_calculate_zenith_angle( + observer_latitude: float, + observer_longitude: float, + day_of_year: int, + solar_time: int, + expected_zenith_angle: float, +) -> None: + observer: Observer = Observer(observer_latitude, observer_longitude) + zenith_angle: float = observer.calculate_zenith_angle(day_of_year, solar_time) + assert zenith_angle == pytest.approx(expected_zenith_angle, abs=1e-3) + + +def test_calculate_zenith_angle_without_latitude(): + observer = Observer(None, 0) + with pytest.raises( + ValueError, + match="Observer latitude must be provided to calculate zenith angle.", + ): + observer.calculate_zenith_angle(1, 12 * 60 * 60) From 92f511289f3564a05ec67f945864b4dd9ce0187e Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Tue, 5 Dec 2023 18:28:43 +0330 Subject: [PATCH 21/31] Refactor code and update documentation - Added the Observer class to the __init__.py file in the pysolorie module. - Updated the __all__ variable in the __init__.py file to include the Observer class. - Refactored the HottelModel class in the model.py file to improve readability and maintainability. This includes updating the formulas used to calculate a0_star, a1_star, and k_star, and simplifying the return statement in the calculate_transmittance_components method. - Updated the documentation in the model.py and sun_position.py files to use the Sphinx math directive for better readability of mathematical formulas. --- src/pysolorie/__init__.py | 3 ++- src/pysolorie/model.py | 42 ++++++++++++++++------------------- src/pysolorie/sun_position.py | 22 +++++++++--------- 3 files changed, 33 insertions(+), 34 deletions(-) diff --git a/src/pysolorie/__init__.py b/src/pysolorie/__init__.py index b150ffe..37b60c4 100644 --- a/src/pysolorie/__init__.py +++ b/src/pysolorie/__init__.py @@ -14,6 +14,7 @@ from .irradiance import SolarIrradiance from .model import HottelModel +from .observer import Observer from .sun_position import SunPosition -__all__ = ["HottelModel", "SunPosition", "SolarIrradiance"] +__all__ = ["HottelModel", "SunPosition", "SolarIrradiance", "Observer"] diff --git a/src/pysolorie/model.py b/src/pysolorie/model.py index f4352a9..0d0d672 100644 --- a/src/pysolorie/model.py +++ b/src/pysolorie/model.py @@ -45,10 +45,10 @@ def _calculate_a0_star(self, observer_altitude: int) -> float: r""" Calculate \(a_0^*\) based on observer altitude. - Formula: - \[ - a_0^* = 0.4237 - 0.00821 \cdot (6 - A)^2 - \] + The formula used to calculate \(a_0^*\) is: + + .. math:: + a_0^* = 0.4237 - 0.00821 \cdot (6 - A)^2 :param observer_altitude: Altitude of the observer in meters. :type observer_altitude: float @@ -63,10 +63,10 @@ def _calculate_a1_star(self, observer_altitude: int) -> float: r""" Calculate \(a_1^*\) based on observer altitude. - Formula: - \[ - a_1^* = 0.5055 + 0.00595 \cdot (6.5 - A)^2 - \] + The formula used to calculate \(a_1^*\) is: + + .. math:: + a_1^* = 0.5055 + 0.00595 \cdot (6.5 - A)^2 :param observer_altitude: Altitude of the observer in meters. :type observer_altitude: float @@ -81,10 +81,10 @@ def _calculate_k_star(self, observer_altitude: int) -> float: r""" Calculate \(k^*\) based on observer altitude. - Formula: - \[ - k^* = 0.2711 + 0.01858 \cdot (2.5 - A)^2 - \] + The formula used to calculate \(k^*\) is: + + .. math:: + k^* = 0.2711 + 0.01858 \cdot (2.5 - A)^2 :param observer_altitude: Altitude of the observer in meters. :type observer_altitude: float @@ -104,12 +104,12 @@ def calculate_transmittance_components( Correction factors adjust the clear-sky beam radiation transmittance components. - Formula: - \[ - a_0 = r_0 \cdot a_0^* - a_1 = r_1 \cdot a_1^* - k = r_k \cdot k^* - \] + The formulas used to calculate the components are: + + .. math:: + a_0 = r_0 \cdot a_0^* + a_1 = r_1 \cdot a_1^* + k = r_k \cdot k^* :param climate_type: Climate type (e.g., "TROPICAL"). :type climate_type: str @@ -128,8 +128,4 @@ def calculate_transmittance_components( a1_star = self._calculate_a1_star(observer_altitude) k_star = self._calculate_k_star(observer_altitude) - a0 = r0 * a0_star - a1 = r1 * a1_star - k = rk * k_star - - return a0, a1, k + return r0 * a0_star, r1 * a1_star, rk * k_star diff --git a/src/pysolorie/sun_position.py b/src/pysolorie/sun_position.py index cf44a16..d0faf33 100644 --- a/src/pysolorie/sun_position.py +++ b/src/pysolorie/sun_position.py @@ -11,7 +11,6 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. - import math @@ -22,10 +21,10 @@ def solar_declination(self, day_of_year: int) -> float: The solar declination angle is the angle between the rays of the sun and the plane of the Earth's equator. - Formula: - \[ - \delta = \sin \left( \frac{2 \pi}{365} * (284 + day_of_year) \right) * \left(\frac{23.45 \pi}{180}\right) - \] + The formula used to calculate the solar declination angle is: + + .. math:: + \delta = \sin \left( \frac{2 \pi}{365} \times (284 + \text{{day\_of\_year}}) \right) \times \left(\frac{23.45 \pi}{180}\right) :param day_of_year: The day of the year. :type day_of_year: int @@ -42,7 +41,10 @@ def solar_declination(self, day_of_year: int) -> float: equinox_offset_days = 284 # Calculate the solar declination angle - solar_declination = math.sin((2 * math.pi) * (equinox_offset_days + day_of_year) / 365) * earth_tilt_radians + solar_declination = ( + math.sin((2 * math.pi) * (equinox_offset_days + day_of_year) / 365) + * earth_tilt_radians + ) return solar_declination @@ -52,10 +54,10 @@ def hour_angle(self, solar_time: float) -> float: The hour angle is a measure of time, expressed in angular terms, from solar noon. - Formula: - \[ - \omega = (t - seconds_in_half_day) * \frac{\pi}{seconds_in_half_day} - \] + The formula used to calculate the hour angle is: + + .. math:: + \omega = (t - \text{{seconds\_in\_half\_day}}) \times \frac{\pi}{\text{{seconds\_in\_half\_day}}} :param solar_time: The solar time in seconds. :type solar_time: float From 138d58dd9484132f40745974e80c6d58431ac428 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Wed, 6 Dec 2023 18:01:18 +0330 Subject: [PATCH 22/31] Updated the order of arguments in the isort command in setup.cfg The --profile=black argument has been moved to the end of the command line. This change ensures that the isort command is correctly formatted --- setup.cfg | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 0d24b6e..3bc1e37 100644 --- a/setup.cfg +++ b/setup.cfg @@ -76,7 +76,7 @@ deps = isort commands = black {posargs:--check --diff src tests} - isort {posargs:--check --diff --profile=black src tests} + isort {posargs:--check --diff src tests} --profile=black [testenv:lint] skip_install = True From 9f21165f4412e0d274cbcf3f3b928fecfaf13c48 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Wed, 6 Dec 2023 18:04:22 +0330 Subject: [PATCH 23/31] Added AtmosphericTransmission class. This commit introduces a new file atmospheric_transmission.py which includes the AtmosphericTransmission class. This class calculates the effective atmospheric transmission coefficient of the direct beam. The __init__.py file has been updated to import the AtmosphericTransmission class and include it in the __all__ list. The __all__ list has also been reformatted for better readability. Additionally, tests for the AtmosphericTransmission class have been added. The tests use the pytest.mark.parametrize decorator to test the calculate_transmittance method of the AtmosphericTransmission class with different parameters. These tests ensure that the new AtmosphericTransmission class is adequately tested, maintaining the robustness of the pysolorie package. --- src/pysolorie/__init__.py | 9 ++- src/pysolorie/atmospheric_transmission.py | 74 +++++++++++++++++++++++ tests/test_pysolorie.py | 34 ++++++++++- 3 files changed, 115 insertions(+), 2 deletions(-) create mode 100644 src/pysolorie/atmospheric_transmission.py diff --git a/src/pysolorie/__init__.py b/src/pysolorie/__init__.py index 37b60c4..fb25418 100644 --- a/src/pysolorie/__init__.py +++ b/src/pysolorie/__init__.py @@ -12,9 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. +from .atmospheric_transmission import AtmosphericTransmission from .irradiance import SolarIrradiance from .model import HottelModel from .observer import Observer from .sun_position import SunPosition -__all__ = ["HottelModel", "SunPosition", "SolarIrradiance", "Observer"] +__all__ = [ + "HottelModel", + "SunPosition", + "SolarIrradiance", + "Observer", + "AtmosphericTransmission", +] diff --git a/src/pysolorie/atmospheric_transmission.py b/src/pysolorie/atmospheric_transmission.py new file mode 100644 index 0000000..af87431 --- /dev/null +++ b/src/pysolorie/atmospheric_transmission.py @@ -0,0 +1,74 @@ +# Copyright 2023 Alireza Aghamohammadi + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import math + +from .model import HottelModel +from .observer import Observer + + +class AtmosphericTransmission: + """ + This class calculates the effective atmospheric transmission coefficient of the direct beam. + """ + + def __init__( + self, + climate_type: str, + observer_altitude: int, + observer_latitude: float, + ): + r""" + Initialize the AtmosphericTransmission class. + + :param climate_type: The type of climate. + :type climate_type: str + :param observer_altitude: The altitude of the observer in meters. + :type observer_altitude: int + :param observer_latitude: The latitude of the observer in degrees. + :type observer_latitude: float + """ + self.hottel_model = HottelModel() + ( + self.a0, + self.a1, + self.k, + ) = self.hottel_model.calculate_transmittance_components( + climate_type, observer_altitude + ) + self.observer = Observer(observer_latitude) + + def calculate_transmittance(self, day_of_year: int, solar_time: float) -> float: + r""" + Calculate the effective atmospheric transmission coefficient of the direct beam. + + The effective atmospheric transmission coefficient of the direct beam is calculated using the formula: + + .. math:: + \tau_b = a_0 + a_1 \cdot \exp\left(-\frac{k}{\cos(\theta_z)}\right) + + where: + \(\tau_b\) is the effective atmospheric transmission coefficient of the direct beam, + \(a_0\), \(a_1\), and \(k\) are the components of clear-sky beam radiation transmittance, + and \(\theta_z\) is the zenith angle. + + :param day_of_year: The day of the year. + :type day_of_year: int + :param solar_time: The solar time in seconds. + :type solar_time: float + :return: The effective atmospheric transmission coefficient of the direct beam. + :rtype: float + """ + zenith_angle = self.observer.calculate_zenith_angle(day_of_year, solar_time) + cos_zenith_angle = math.cos(zenith_angle) + return self.a0 + self.a1 * math.exp(-self.k / cos_zenith_angle) diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index f567582..556e612 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -17,7 +17,13 @@ import pytest -from pysolorie import HottelModel, Observer, SolarIrradiance, SunPosition +from pysolorie import ( + AtmosphericTransmission, + HottelModel, + Observer, + SolarIrradiance, + SunPosition, +) @pytest.mark.parametrize( @@ -136,3 +142,29 @@ def test_calculate_zenith_angle_without_latitude(): match="Observer latitude must be provided to calculate zenith angle.", ): observer.calculate_zenith_angle(1, 12 * 60 * 60) + + +@pytest.mark.parametrize( + "climate_type, observer_altitude, observer_latitude, day_of_year, solar_time, expected_transmittance", + [ + ("MIDLATITUDE SUMMER", 1200, 35.69, 81, 12 * 60 * 60, 0.683), # Tehran Summer + ("MIDLATITUDE WINTER", 1200, 35.69, 355, 12 * 60 * 60, 0.618), # Tehran Winter + ("TROPICAL", 63, 3.59, 81, 10 * 60 * 60, 0.597), # Medan + ("SUBARCTIC SUMMER", 136, 64.84, 1, 13 * 60 * 60, 0.140), # Fairbanks + ], +) +def test_calculate_transmittance( + climate_type: str, + observer_altitude: int, + observer_latitude: float, + day_of_year: int, + solar_time: float, + expected_transmittance: float, +) -> None: + atmospheric_transmission: AtmosphericTransmission = AtmosphericTransmission( + climate_type, observer_altitude, observer_latitude + ) + result: float = atmospheric_transmission.calculate_transmittance( + day_of_year, solar_time + ) + assert pytest.approx(result, abs=1e-3) == expected_transmittance From 8f2ece2f3de404c1fd8d231fc6fe84299c094964 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Wed, 6 Dec 2023 18:07:42 +0330 Subject: [PATCH 24/31] Expanded Test Coverage for calculate_extraterrestrial_irradiance Method In this commit, we have expanded the test coverage for the calculate_extraterrestrial_irradiance method in the test_pysolorie.py file. A new test case has been added for Tehran on the 355th day of the year at noon. --- tests/test_pysolorie.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index 556e612..5f412e2 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -106,6 +106,13 @@ def test_calculate_extraterrestrial_irradiance( 12 * 60 * 60, 0.623, ), # Test at Tehran on the 81st day of the year at noon + ( + 35.69, + 51.39, + 355, + 12 * 60 * 60, + 1.032, + ), # Test at Tehran on the 355th day of the year at noon ( 3.59, 98.67, From 06c89c1513de5d89c68f0419636ad7f8255559b6 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Thu, 7 Dec 2023 15:09:30 +0330 Subject: [PATCH 25/31] Add solar_time function and tests to SunPosition class MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Added a new function `solar_time` to the `SunPosition` class in `src/pysolorie/sun_position.py`. - This function calculates the solar time based on the hour angle. - The formula used in this function is: `t = ω * seconds_in_half_day / π + seconds_in_half_day`. - Also added corresponding test cases for the `solar_time` function in `tests/test_pysolorie.py`. - These tests check the function with different hour angles and verify that the function returns the correct solar time. --- src/pysolorie/sun_position.py | 27 +++++++++++++++++++++++++++ tests/test_pysolorie.py | 17 +++++++++++++++++ 2 files changed, 44 insertions(+) diff --git a/src/pysolorie/sun_position.py b/src/pysolorie/sun_position.py index d0faf33..6468cc5 100644 --- a/src/pysolorie/sun_position.py +++ b/src/pysolorie/sun_position.py @@ -74,3 +74,30 @@ def hour_angle(self, solar_time: float) -> float: hour_angle = (solar_time - seconds_in_half_day) * earth_rotation_rate return hour_angle + + def solar_time(self, hour_angle: float) -> float: + r""" + Calculate the solar time based on the hour angle. + + The solar time is a measure of time, expressed in seconds, from solar noon. + + The formula used to calculate the solar time is: + + .. math:: + t = \omega \times \frac{\text{{seconds\_in\_half\_day}}}{\pi} + \text{{seconds\_in\_half\_day}} + + :param hour_angle: The hour angle in radians. + :type hour_angle: float + :return: The solar time in seconds. + :rtype: float + """ + # The number of seconds in half a day (12 hours) + seconds_in_half_day = 12 * 60 * 60 + + # The Earth rotates by pi/seconds_in_half_day radians per second + earth_rotation_rate = math.pi / seconds_in_half_day + + # Calculate the solar time + solar_time = hour_angle / earth_rotation_rate + seconds_in_half_day + + return solar_time diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index 5f412e2..867b7d5 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -75,6 +75,23 @@ def test_sun_position( assert hour_angle == pytest.approx(expected_hour_angle, abs=1e-3) +@pytest.mark.parametrize( + "hour_angle, expected_solar_time", + [ + (0, 12 * 60 * 60), # solar noon + (-math.pi / 6, 10 * 60 * 60), # 10am + (math.pi / 12, 13 * 60 * 60), # 1pm + ], +) +def test_solar_time( + hour_angle: float, + expected_solar_time: int, +) -> None: + sun_position: SunPosition = SunPosition() + solar_time: float = sun_position.solar_time(hour_angle) + assert solar_time == pytest.approx(expected_solar_time, abs=1e-3) + + @pytest.mark.parametrize( "day_of_year, expected_irradiance", [ From 87c7810d102cffd8b653b130dc82f07203edca66 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Thu, 7 Dec 2023 16:05:32 +0330 Subject: [PATCH 26/31] Add sunrise/sunset calculation - Refactored the `Observer` class in `src/pysolorie/observer.py` to improve code readability and eliminate duplication. - Extracted latitude validation into a separate method `_validate_latitude`. - The `_validate_latitude` method now returns the latitude after validation, which helps with mypy type checking. - Added a new method `calculate_sunrise_sunset` to calculate the hour angle at sunrise and sunset. - The `calculate_sunrise_sunset` method returns a tuple with the hour angle at sunrise and sunset, making the return values more readable. - Updated the error message in the `calculate_zenith_angle_without_latitude` test in `tests/test_pysolorie.py` to match the new error message in `_validate_latitude`. - Added new test cases for the `calculate_sunrise_sunset` method in `tests/test_pysolorie.py`. --- src/pysolorie/observer.py | 45 +++++++++++++++++++++++++++++++++------ tests/test_pysolorie.py | 23 +++++++++++++++++++- 2 files changed, 61 insertions(+), 7 deletions(-) diff --git a/src/pysolorie/observer.py b/src/pysolorie/observer.py index d6db028..d6f6b96 100644 --- a/src/pysolorie/observer.py +++ b/src/pysolorie/observer.py @@ -62,16 +62,49 @@ def calculate_zenith_angle(self, day_of_year: int, solar_time: float) -> float: :return: The zenith angle in radians. :rtype: float """ - if self.observer_latitude is None: - raise ValueError( - "Observer latitude must be provided to calculate zenith angle." - ) + observer_latitude = self._validate_latitude() solar_declination = self.sun_position.solar_declination(day_of_year) hour_angle = self.sun_position.hour_angle(solar_time) return math.acos( - math.sin(self.observer_latitude) * math.sin(solar_declination) - + math.cos(self.observer_latitude) + math.sin(observer_latitude) * math.sin(solar_declination) + + math.cos(observer_latitude) * math.cos(solar_declination) * math.cos(hour_angle) ) + + def calculate_sunrise_sunset(self, day_of_year: int) -> tuple: + """ + Calculate the hour angle at sunrise and sunset. + + The hour angle at sunrise and sunset is calculated using the formula: + + .. math:: + \cos(\omega) = -\tan(\phi) \cdot \tan(\delta) + + where: + \(\omega\) is the hour angle, + \(\phi\) is the latitude of the observer, and + \(\delta\) is the solar declination. + + :param day_of_year: The day of the year. + :type day_of_year: int + :return: The hour angle at sunrise and sunset in radians. + :rtype: tuple + """ + observer_latitude = self._validate_latitude() + + solar_declination = self.sun_position.solar_declination(day_of_year) + hour_angle = math.acos( + -math.tan(observer_latitude) * math.tan(solar_declination) + ) + + sunrise = -hour_angle + sunset = hour_angle + + return sunrise, sunset + + def _validate_latitude(self) -> float: + if self.observer_latitude is None: + raise ValueError("Observer latitude must be provided.") + return self.observer_latitude diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index 867b7d5..903009b 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -163,7 +163,7 @@ def test_calculate_zenith_angle_without_latitude(): observer = Observer(None, 0) with pytest.raises( ValueError, - match="Observer latitude must be provided to calculate zenith angle.", + match="Observer latitude must be provided.", ): observer.calculate_zenith_angle(1, 12 * 60 * 60) @@ -192,3 +192,24 @@ def test_calculate_transmittance( day_of_year, solar_time ) assert pytest.approx(result, abs=1e-3) == expected_transmittance + + +@pytest.mark.parametrize( + "day_of_year, expected_sunrise_hour_angle, expected_sunset_hour_angle", + [ + (1, -1.261, 1.261), # January 1st + (81, -math.pi / 2, math.pi / 2), # March 22nd (equinox) + (172, -1.888, 1.888), # June 21st (solstice) + ], +) +def test_calculate_sunrise_sunset( + day_of_year: int, + expected_sunrise_hour_angle: float, + expected_sunset_hour_angle: float, +) -> None: + observer: Observer = Observer(observer_latitude=35.69) # Tehran + sunrise_hour_angle, sunset_hour_angle = observer.calculate_sunrise_sunset( + day_of_year + ) + assert sunrise_hour_angle == pytest.approx(expected_sunrise_hour_angle, abs=1e-3) + assert sunset_hour_angle == pytest.approx(expected_sunset_hour_angle, abs=1e-3) From 3b9403de97680266736cb3cc924786098a858bcf Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Fri, 8 Dec 2023 19:31:01 +0330 Subject: [PATCH 27/31] Implement IrradiationCalculator Added SciPy as a dependency in dev requirements and setup config. Imported IrradiationCalculator module in the pysolorie package. Refactored AtmosphericTransmission to handle division by zero in transmittance calculation. Updated SolarIrradiance class documentation with LaTeX formatted equations. Created new file for the numerical_integration module with appropriate copyright header and implemented IrradiationCalculator. Optimized functions in numerical_integration and added integration tests for optimal solar panel orientation. Fixed test parameters for observer altitude in tropical climate test case. --- dev-requirements.txt | 1 + setup.cfg | 2 + src/pysolorie/__init__.py | 2 + src/pysolorie/atmospheric_transmission.py | 8 +- src/pysolorie/irradiance.py | 20 +-- src/pysolorie/numerical_integration.py | 155 ++++++++++++++++++++++ tests/test_pysolorie.py | 50 ++++++- 7 files changed, 226 insertions(+), 12 deletions(-) create mode 100644 src/pysolorie/numerical_integration.py diff --git a/dev-requirements.txt b/dev-requirements.txt index 68d4cc7..bc6bd27 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -1,3 +1,4 @@ build>=0.10.0 pre-commit>=3.3.3 +scipy>=1.11.4 tox>=4.11.1 diff --git a/setup.cfg b/setup.cfg index 3bc1e37..02ab908 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,8 @@ package_dir = =src packages = find: include_package_data = True +install_requires = + scipy>=1.11.4 [options.packages.find] where = src diff --git a/src/pysolorie/__init__.py b/src/pysolorie/__init__.py index fb25418..aad9c11 100644 --- a/src/pysolorie/__init__.py +++ b/src/pysolorie/__init__.py @@ -15,6 +15,7 @@ from .atmospheric_transmission import AtmosphericTransmission from .irradiance import SolarIrradiance from .model import HottelModel +from .numerical_integration import IrradiationCalculator from .observer import Observer from .sun_position import SunPosition @@ -24,4 +25,5 @@ "SolarIrradiance", "Observer", "AtmosphericTransmission", + "IrradiationCalculator", ] diff --git a/src/pysolorie/atmospheric_transmission.py b/src/pysolorie/atmospheric_transmission.py index af87431..9a878c4 100644 --- a/src/pysolorie/atmospheric_transmission.py +++ b/src/pysolorie/atmospheric_transmission.py @@ -49,7 +49,7 @@ def __init__( self.observer = Observer(observer_latitude) def calculate_transmittance(self, day_of_year: int, solar_time: float) -> float: - r""" + """ Calculate the effective atmospheric transmission coefficient of the direct beam. The effective atmospheric transmission coefficient of the direct beam is calculated using the formula: @@ -71,4 +71,8 @@ def calculate_transmittance(self, day_of_year: int, solar_time: float) -> float: """ zenith_angle = self.observer.calculate_zenith_angle(day_of_year, solar_time) cos_zenith_angle = math.cos(zenith_angle) - return self.a0 + self.a1 * math.exp(-self.k / cos_zenith_angle) + EPSILON = 1e-8 # Small constant to prevent division by zero + if abs(cos_zenith_angle) < EPSILON: + return self.a0 + else: + return self.a0 + self.a1 * math.exp(-self.k / cos_zenith_angle) diff --git a/src/pysolorie/irradiance.py b/src/pysolorie/irradiance.py index fc73b29..7db43be 100644 --- a/src/pysolorie/irradiance.py +++ b/src/pysolorie/irradiance.py @@ -19,7 +19,7 @@ class SolarIrradiance: def __init__(self, sun_position: SunPosition): - """ + r""" Initialize the SolarIrradiance class. :param sun_position: An instance of the SunPosition class. @@ -31,25 +31,27 @@ 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, E, is the amount of solar energy received + The extraterrestrial solar irradiance, \(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 formula used is: - \[ - E = SOLAR_CONSTANT * (1 + 0.33 * cos (2 * pi * day_of_year / 365)) - \] + + .. math:: + + E = \text{{SOLAR\_CONSTANT}} \times (1 + 0.33 \times \cos (2 \times \pi \times \frac{{\text{{day\_of\_year}}}}{365})) where: - - SOLAR_CONSTANT is the average solar radiation arriving outside of the Earth's atmosphere, - which is approximately 1367 Watts per square meter. This is also known as the solar constant. + - \(\text{{SOLAR\_CONSTANT}}\) is the average solar radiation arriving outside 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. - :param day_of_year: The day of the year, ranging from 1 to 365. - :type day_of_year: int + :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. :rtype: float """ + # Solar constant (W/m^2) SOLAR_CONSTANT = 1367 diff --git a/src/pysolorie/numerical_integration.py b/src/pysolorie/numerical_integration.py new file mode 100644 index 0000000..e66d19f --- /dev/null +++ b/src/pysolorie/numerical_integration.py @@ -0,0 +1,155 @@ +# Copyright 2023 Alireza Aghamohammadi + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import math +from typing import Tuple + +import numpy as np +from scipy import integrate, optimize # type: ignore + +from .atmospheric_transmission import AtmosphericTransmission +from .irradiance import SolarIrradiance +from .observer import Observer +from .sun_position import SunPosition + + +class IrradiationCalculator: + OMEGA = 7.15 * 1e-5 + + def __init__( + self, climate_type: str, observer_altitude: int, observer_latitude: float + ): + """ + Initialize the IrradiationCalculator class. + + :param climate_type: The type of climate. + :type climate_type: str + :param observer_altitude: The altitude of the observer in meters. + :type observer_altitude: int + :param observer_latitude: The latitude of the observer in degrees. + :type observer_latitude: float + """ + self._observer = Observer(observer_latitude=observer_latitude) + self._sun_position = SunPosition() + self._solar_irradiance = SolarIrradiance(self._sun_position) + self._atmospheric_transmission = AtmosphericTransmission( + climate_type, observer_altitude, observer_latitude + ) + + @staticmethod + def _heaviside(x: float) -> int: + """ + Heaviside step function. + + :param x: Input to the function. + :type x: float + :return: 1 if x >= 0, else 0. + :rtype: int + """ + return 1 if x >= 0 else 0 + + def _calculate_irradiance_component( + self, hour_angle: float, panel_orientation: float, day_of_year: int + ) -> float: + r""" + Calculate a component of the irradiance integral. + + :param hour_angle: The hour angle. + :type hour_angle: float + :param panel_orientation: The orientation of the solar panel in radians. + :type panel_orientation: float + :param day_of_year: The day of the year. + :type day_of_year: int + :return: A component of the irradiance integral. + :rtype: float + """ + observer_latitude = self._observer._validate_latitude() + solar_declination = self._sun_position.solar_declination(day_of_year) + cos_theta = math.sin(solar_declination) * math.sin( + observer_latitude - panel_orientation + ) + math.cos(solar_declination) * math.cos(hour_angle) * math.cos( + observer_latitude - panel_orientation + ) + transmittance = self._atmospheric_transmission.calculate_transmittance( + day_of_year, self._sun_position.solar_time(hour_angle) + ) + irradiance = self._solar_irradiance.calculate_extraterrestrial_irradiance( + day_of_year + ) + return ( + irradiance + * transmittance + * cos_theta + * self._heaviside(cos_theta) + / self.OMEGA + ) + + def calculate_direct_irradiation( + self, panel_orientation: float, day_of_year: int + ) -> float: + r""" + Calculate the total direct irradiation for a given solar panel orientation (beta). + + The total direct irradiation is calculated using the formula: + + .. math:: + E_b(n,\phi) = \frac{E}{\Omega} \int_{\omega_s}^{\omega_t} \cos(θ) H(\cos(θ)) \times \tau_b d\omega + + + where: + n is the day of year + \(\phi\) is the latitude of the observer, and + \(E\) is the amount of solar energy received + \Omega is a constant equal to 7.15 * 1e-5 + theta is incidence angle + \omega_s is the sunrise hour angle + \omega_t is the sunset hour angle + H is the heaviside step function + + + :param panel_orientation: The orientation of the solar panel in radians. + :type panel_orientation: float + :param day_of_year: The day of the year. + :type day_of_year: int + :return: The total direct irradiation (negative because we want to maximize). + :rtype: float + """ + sunrise_hour_angle, sunset_hour_angle = self._observer.calculate_sunrise_sunset( + day_of_year + ) + irradiance_components = [ + self._calculate_irradiance_component( + hour_angle, panel_orientation, day_of_year + ) + for hour_angle in np.arange(sunrise_hour_angle, sunset_hour_angle, 0.01) + ] + return integrate.simpson(irradiance_components, dx=0.01) + + def find_optimal_orientation(self, day_of_year: int) -> float: + """ + Find the optimal orientation (beta) that maximizes the total direct irradiation. + + :param day_of_year: The day of the year. + :type day_of_year: int + :return: The optimal orientation (beta) in radians. + :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 + return optimal_beta diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index 903009b..d35cc5a 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -20,6 +20,7 @@ from pysolorie import ( AtmosphericTransmission, HottelModel, + IrradiationCalculator, Observer, SolarIrradiance, SunPosition, @@ -31,7 +32,7 @@ [ ("MIDLATITUDE SUMMER", 1200, (0.228, 0.666, 0.309)), # Tehran Summer ("MIDLATITUDE WINTER", 1200, (0.242, 0.679, 0.303)), # Tehran Winter - ("TROPICAL", 63, (0.128, 0.737, 0.389)), # Medan + ("TROPICAL", 26, (0.124, 0.739, 0.392)), # Medan ("SUBARCTIC SUMMER", 136, (0.140, 0.739, 0.379)), # Fairbanks ], ) @@ -213,3 +214,50 @@ def test_calculate_sunrise_sunset( ) assert sunrise_hour_angle == pytest.approx(expected_sunrise_hour_angle, abs=1e-3) 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, expected_result", + [ + ( + "MIDLATITUDE SUMMER", + 1200, + 35.6892, + 172, + 0.004, + ), # Tehran Summer, day_of_year=172 (June 21) + ( + "MIDLATITUDE WINTER", + 1200, + 35.6892, + 355, + 1.114, + ), # Tehran Winter, day_of_year=355 (Dec 21) + ( + "TROPICAL", + 26, + 3.5952, + 100, + -0.116, + ), # Medan, day_of_year=100 (April 10) + ( + "SUBARCTIC SUMMER", + 132, + 64.84361, + 200, + 0.569, + ), # Fairbanks Summer, day_of_year=200 (July 19) + ], +) +def test_find_optimal_orientation( + climate_type: str, + observer_altitude: int, + observer_latitude: float, + day_of_year: int, + expected_result: float, +) -> None: + irradiation_calculator = IrradiationCalculator( + climate_type, observer_altitude, observer_latitude + ) + result = irradiation_calculator.find_optimal_orientation(day_of_year) + assert pytest.approx(result, abs=1e-3) == expected_result From 6152505459e9ef329e8678b1391fe61c59a0b2fa Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Sat, 9 Dec 2023 18:02:16 +0330 Subject: [PATCH 28/31] Refactor docstrings, maths expressions, and remove unused imports for flake8 compliance - Standardized multi-line docstrings for consistent line breaks. - Refactored multi-line maths expressions to improve readability. - Removed an unused 'typing' import from numerical_integration.py. - Eliminated an import from scipy for unused 'optimize' in numerical_integration.py. - Adjusted annotation spacing to comply with flake8 and flake8-bugbear recommendations. --- src/pysolorie/atmospheric_transmission.py | 13 ++++++++----- src/pysolorie/irradiance.py | 17 +++++++++++------ src/pysolorie/model.py | 10 ++++++---- src/pysolorie/numerical_integration.py | 9 +++++---- src/pysolorie/observer.py | 7 ++++--- src/pysolorie/sun_position.py | 16 +++++++++++----- tests/test_pysolorie.py | 13 +++++++++++-- 7 files changed, 56 insertions(+), 29 deletions(-) diff --git a/src/pysolorie/atmospheric_transmission.py b/src/pysolorie/atmospheric_transmission.py index 9a878c4..cbec258 100644 --- a/src/pysolorie/atmospheric_transmission.py +++ b/src/pysolorie/atmospheric_transmission.py @@ -19,7 +19,8 @@ class AtmosphericTransmission: """ - This class calculates the effective atmospheric transmission coefficient of the direct beam. + This class calculates the effective atmospheric transmission coefficient + of the direct beam. """ def __init__( @@ -49,17 +50,19 @@ def __init__( self.observer = Observer(observer_latitude) def calculate_transmittance(self, day_of_year: int, solar_time: float) -> float: - """ + r""" Calculate the effective atmospheric transmission coefficient of the direct beam. - The effective atmospheric transmission coefficient of the direct beam is calculated using the formula: + The effective atmospheric transmission coefficient of the direct beam + is calculated using the formula: .. math:: \tau_b = a_0 + a_1 \cdot \exp\left(-\frac{k}{\cos(\theta_z)}\right) where: - \(\tau_b\) is the effective atmospheric transmission coefficient of the direct beam, - \(a_0\), \(a_1\), and \(k\) are the components of clear-sky beam radiation transmittance, + \(\tau_b\) is the effective atmospheric transmission coefficient + of the direct beam, \(a_0\), \(a_1\), and \(k\) + are the components of clear-sky beam radiation transmittance, and \(\theta_z\) is the zenith angle. :param day_of_year: The day of the year. diff --git a/src/pysolorie/irradiance.py b/src/pysolorie/irradiance.py index 7db43be..1b04a10 100644 --- a/src/pysolorie/irradiance.py +++ b/src/pysolorie/irradiance.py @@ -31,19 +31,24 @@ 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, \(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, \(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 formula used is: .. math:: - E = \text{{SOLAR\_CONSTANT}} \times (1 + 0.33 \times \cos (2 \times \pi \times \frac{{\text{{day\_of\_year}}}}{365})) + E = \text{{SOLAR\_CONSTANT}} + \times (1 + 0.33 \times \cos (2 + \times \pi \times \frac{{\text{{day\_of\_year}}}}{365})) where: - - \(\text{{SOLAR\_CONSTANT}}\) is the average solar radiation arriving outside 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. + \(\text{{SOLAR\_CONSTANT}}\) is the average solar radiation arriving outside + 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. :param day\_of\_year: The day of the year, ranging from 1 to 365. :type day\_of\_year: int diff --git a/src/pysolorie/model.py b/src/pysolorie/model.py index 0d0d672..dec37a8 100644 --- a/src/pysolorie/model.py +++ b/src/pysolorie/model.py @@ -20,7 +20,8 @@ class HottelModel: Hottel Model for estimating clear-sky beam radiation transmittance based on climate type, and observer altitude. - :ivar CLIMATE_CONSTANTS: Correction factors for different climate types (\(r_0\), \(r_1\), and \(r_k\)). + :ivar CLIMATE_CONSTANTS: Correction factors for different climate types + (\(r_0\), \(r_1\), and \(r_k\)). """ CLIMATE_CONSTANTS: Dict[str, Tuple[float, float, float]] = { @@ -99,8 +100,8 @@ def calculate_transmittance_components( self, climate_type: str, observer_altitude: int ) -> Tuple[float, float, float]: r""" - Calculate the components of clear-sky beam radiation transmittance (\(a_0\), \(a_1\), and \(k\)) - based on climate type and observer altitude. + Calculate the components of clear-sky beam radiation transmittance + (\(a_0\), \(a_1\), and \(k\)) based on climate type and observer altitude. Correction factors adjust the clear-sky beam radiation transmittance components. @@ -115,7 +116,8 @@ def calculate_transmittance_components( :type climate_type: str :param observer_altitude: Altitude of the observer in meters. :type observer_altitude: float - :return: Components of clear-sky beam radiation transmittance (\(a_0\), \(a_1\), \(k\)). + :return: Components of clear-sky beam radiation transmittance + (\(a_0\), \(a_1\), \(k\)). :rtype: tuple of floats :raises ValueError: If an invalid climate type is provided. """ diff --git a/src/pysolorie/numerical_integration.py b/src/pysolorie/numerical_integration.py index e66d19f..68cf4c7 100644 --- a/src/pysolorie/numerical_integration.py +++ b/src/pysolorie/numerical_integration.py @@ -13,10 +13,9 @@ # limitations under the License. import math -from typing import Tuple import numpy as np -from scipy import integrate, optimize # type: ignore +from scipy import integrate # type: ignore from .atmospheric_transmission import AtmosphericTransmission from .irradiance import SolarIrradiance @@ -99,12 +98,14 @@ def calculate_direct_irradiation( self, panel_orientation: float, day_of_year: int ) -> float: r""" - Calculate the total direct irradiation for a given solar panel orientation (beta). + Calculate the total direct irradiation + for a given solar panel orientation (beta). The total direct irradiation is calculated using the formula: .. math:: - E_b(n,\phi) = \frac{E}{\Omega} \int_{\omega_s}^{\omega_t} \cos(θ) H(\cos(θ)) \times \tau_b d\omega + E_b(n,\phi) = \frac{E}{\Omega} \int_{\omega_s}^{\omega_t} + \cos(θ) H(\cos(θ)) \times \tau_b d\omega where: diff --git a/src/pysolorie/observer.py b/src/pysolorie/observer.py index d6f6b96..051eb70 100644 --- a/src/pysolorie/observer.py +++ b/src/pysolorie/observer.py @@ -41,13 +41,14 @@ def __init__( self.sun_position = SunPosition() def calculate_zenith_angle(self, day_of_year: int, solar_time: float) -> float: - """ + r""" Calculate the zenith angle. The zenith angle is calculated using the formula: .. math:: - \cos(\theta_z) = \sin(\phi) \cdot \sin(\delta) + \cos(\phi) \cdot \cos(\delta) \cdot \cos(\omega) + \cos(\theta_z) = \sin(\phi) \cdot \sin(\delta) + + \cos(\phi) \cdot \cos(\delta) \cdot \cos(\omega) where: \(\theta_z\) is the zenith angle, @@ -74,7 +75,7 @@ def calculate_zenith_angle(self, day_of_year: int, solar_time: float) -> float: ) def calculate_sunrise_sunset(self, day_of_year: int) -> tuple: - """ + r""" Calculate the hour angle at sunrise and sunset. The hour angle at sunrise and sunset is calculated using the formula: diff --git a/src/pysolorie/sun_position.py b/src/pysolorie/sun_position.py index 6468cc5..62af9ac 100644 --- a/src/pysolorie/sun_position.py +++ b/src/pysolorie/sun_position.py @@ -19,12 +19,15 @@ def solar_declination(self, day_of_year: int) -> float: r""" Calculate the solar declination angle in radians. - The solar declination angle is the angle between the rays of the sun and the plane of the Earth's equator. + The solar declination angle is the angle between the rays of the sun + and the plane of the Earth's equator. The formula used to calculate the solar declination angle is: .. math:: - \delta = \sin \left( \frac{2 \pi}{365} \times (284 + \text{{day\_of\_year}}) \right) \times \left(\frac{23.45 \pi}{180}\right) + \delta = \sin \left( \frac{2 \pi}{365} + \times (284 + \text{{day\_of\_year}}) \right) + \times \left(\frac{23.45 \pi}{180}\right) :param day_of_year: The day of the year. :type day_of_year: int @@ -52,12 +55,14 @@ def hour_angle(self, solar_time: float) -> float: r""" Calculate the hour angle based on the solar time. - The hour angle is a measure of time, expressed in angular terms, from solar noon. + The hour angle is a measure of time, expressed in angular terms, + from solar noon. The formula used to calculate the hour angle is: .. math:: - \omega = (t - \text{{seconds\_in\_half\_day}}) \times \frac{\pi}{\text{{seconds\_in\_half\_day}}} + \omega = (t - \text{{seconds\_in\_half\_day}}) + \times \frac{\pi}{\text{{seconds\_in\_half\_day}}} :param solar_time: The solar time in seconds. :type solar_time: float @@ -84,7 +89,8 @@ def solar_time(self, hour_angle: float) -> float: The formula used to calculate the solar time is: .. math:: - t = \omega \times \frac{\text{{seconds\_in\_half\_day}}}{\pi} + \text{{seconds\_in\_half\_day}} + t = \omega \times \frac{\text{{seconds\_in\_half\_day}}}{\pi} + + \text{{seconds\_in\_half\_day}} :param hour_angle: The hour angle in radians. :type hour_angle: float diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index d35cc5a..f5b2d66 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -115,7 +115,11 @@ def test_calculate_extraterrestrial_irradiance( @pytest.mark.parametrize( - "observer_latitude, observer_longitude, day_of_year, solar_time, expected_zenith_angle", + "observer_latitude," + + "observer_longitude," + + "day_of_year," + + "solar_time," + + "expected_zenith_angle", [ ( 35.69, @@ -170,7 +174,12 @@ def test_calculate_zenith_angle_without_latitude(): @pytest.mark.parametrize( - "climate_type, observer_altitude, observer_latitude, day_of_year, solar_time, expected_transmittance", + "climate_type," + + "observer_altitude," + + "observer_latitude," + + "day_of_year," + + "solar_time," + + "expected_transmittance", [ ("MIDLATITUDE SUMMER", 1200, 35.69, 81, 12 * 60 * 60, 0.683), # Tehran Summer ("MIDLATITUDE WINTER", 1200, 35.69, 355, 12 * 60 * 60, 0.618), # Tehran Winter From 94894b4b2717da3b86e61ebfcb692fdc51a8baac Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Sat, 9 Dec 2023 18:20:29 +0330 Subject: [PATCH 29/31] Update IrradiationCalculator outputs and test cases to improve correctness - Clarified the docstring for the total direct irradiation return value. - Changed the unit of the optimal orientation (beta) from radians to degrees in both the method return statement and corresponding docstring. - Adjusted test case values for sunrise and sunset calculations, converting them from radians to degrees for consistency and readability. --- src/pysolorie/numerical_integration.py | 6 +++--- tests/test_pysolorie.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pysolorie/numerical_integration.py b/src/pysolorie/numerical_integration.py index 68cf4c7..0ab1b55 100644 --- a/src/pysolorie/numerical_integration.py +++ b/src/pysolorie/numerical_integration.py @@ -123,7 +123,7 @@ def calculate_direct_irradiation( :type panel_orientation: float :param day_of_year: The day of the year. :type day_of_year: int - :return: The total direct irradiation (negative because we want to maximize). + :return: The total direct irradiation. :rtype: float """ sunrise_hour_angle, sunset_hour_angle = self._observer.calculate_sunrise_sunset( @@ -143,7 +143,7 @@ def find_optimal_orientation(self, day_of_year: int) -> float: :param day_of_year: The day of the year. :type day_of_year: int - :return: The optimal orientation (beta) in radians. + :return: The optimal orientation (beta) in degrees. :rtype: float """ betas = np.arange(-math.pi / 2, math.pi / 2, 0.005) # Discretize beta @@ -153,4 +153,4 @@ def find_optimal_orientation(self, day_of_year: int) -> float: optimal_beta = betas[ np.argmax(irradiations) ] # Find beta that gives max irradiation - return optimal_beta + return math.degrees(optimal_beta) diff --git a/tests/test_pysolorie.py b/tests/test_pysolorie.py index f5b2d66..b8a2142 100644 --- a/tests/test_pysolorie.py +++ b/tests/test_pysolorie.py @@ -233,28 +233,28 @@ def test_calculate_sunrise_sunset( 1200, 35.6892, 172, - 0.004, + 0.241, ), # Tehran Summer, day_of_year=172 (June 21) ( "MIDLATITUDE WINTER", 1200, 35.6892, 355, - 1.114, + 63.839, ), # Tehran Winter, day_of_year=355 (Dec 21) ( "TROPICAL", 26, 3.5952, 100, - -0.116, + -6.635, ), # Medan, day_of_year=100 (April 10) ( "SUBARCTIC SUMMER", 132, 64.84361, 200, - 0.569, + 32.613, ), # Fairbanks Summer, day_of_year=200 (July 19) ], ) From b0a6301717f817d6826733f1e271f4f3f83229d1 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Sat, 9 Dec 2023 18:47:51 +0330 Subject: [PATCH 30/31] Add new GitHub Actions workflow 'Quality Checks' and update tox environment list - Created a new continuous integration workflow named 'Quality Checks' to run on 'push' and 'pull_request' events. - Added jobs for code formatting with black, linting with flake8, type checking with mypy, and testing across multiple Python versions: 3.8, 3.9, 3.10, and 3.11 using tox. - Set up matrix strategy in the testing job to run tests concurrently across the specified Python versions without cancelling all jobs if any one of them fails. - Updated `tox.ini` to include Python 3.8, 3.9, 3.10, and 3.11 in the environment list. --- .github/workflows/quality_checks.yml | 79 ++++++++++++++++++++++++++++ setup.cfg | 3 +- 2 files changed, 81 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/quality_checks.yml diff --git a/.github/workflows/quality_checks.yml b/.github/workflows/quality_checks.yml new file mode 100644 index 0000000..01eb412 --- /dev/null +++ b/.github/workflows/quality_checks.yml @@ -0,0 +1,79 @@ +name: Quality Checks + +on: + - push + - pull_request + +jobs: + format: + name: Check formatting + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install tox + run: python -m pip install tox + + - name: Format with black + run: tox -e format + + + lint: + name: Lint + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install tox + run: python -m pip install tox + + - name: Run linter with flake8 + run: tox -e lint + + typecheck: + name: Type check + runs-on: ubuntu-22.04 + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install tox + run: python -m pip install tox + + - name: Type check with mypy + run: python -m tox -e typecheck + + test: + name: Test + runs-on: ubuntu-22.04 + strategy: + fail-fast: false # Whether to cancel all jobs if any matrix job fails + matrix: + include: + - {python-version: "3.11", toxenv: "py311"} + - {python-version: "3.10", toxenv: "py310"} + - {python-version: "3.9", toxenv: "py39"} + - {python-version: "3.8", toxenv: "py38"} + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install tox + run: python -m pip install tox + + - name: Execute tests with pytest + run: tox -e ${{ matrix.toxenv }} diff --git a/setup.cfg b/setup.cfg index 02ab908..d16b4ad 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,9 +54,10 @@ source = */site-packages/pysolorie [tox:tox] -envlist = py310 +envlist = py38, py39, py310, py311 isolated_build = True + [testenv] deps = pytest From 98a27d93aa50e1ba6fae0725eb39a36c2324fc60 Mon Sep 17 00:00:00 2001 From: Alireza Aghamohammadi Date: Sat, 9 Dec 2023 18:54:44 +0330 Subject: [PATCH 31/31] Drop Python 3.8 support due to scipy requirements This commit removes Python 3.8 from the testing matrix in both GitHub Actions and tox configuration due to the `scipy` package requiring Python 3.9 or higher. Please update your environments to use Python 3.9 or later. --- .github/workflows/quality_checks.yml | 1 - setup.cfg | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/quality_checks.yml b/.github/workflows/quality_checks.yml index 01eb412..4fdb121 100644 --- a/.github/workflows/quality_checks.yml +++ b/.github/workflows/quality_checks.yml @@ -64,7 +64,6 @@ jobs: - {python-version: "3.11", toxenv: "py311"} - {python-version: "3.10", toxenv: "py310"} - {python-version: "3.9", toxenv: "py39"} - - {python-version: "3.8", toxenv: "py38"} steps: - uses: actions/checkout@v4 diff --git a/setup.cfg b/setup.cfg index d16b4ad..287b8ec 100644 --- a/setup.cfg +++ b/setup.cfg @@ -54,7 +54,7 @@ source = */site-packages/pysolorie [tox:tox] -envlist = py38, py39, py310, py311 +envlist = py39, py310, py311 isolated_build = True