diff --git a/.github/workflows/flake8.yml b/.github/workflows/flake8.yml index edb2a07..76236a4 100755 --- a/.github/workflows/flake8.yml +++ b/.github/workflows/flake8.yml @@ -4,7 +4,7 @@ on: [push] jobs: flake8: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.1.7 - uses: actions/setup-python@v5.1.0 diff --git a/.github/workflows/mypy.yml b/.github/workflows/mypy.yml new file mode 100644 index 0000000..c1d2db1 --- /dev/null +++ b/.github/workflows/mypy.yml @@ -0,0 +1,21 @@ +name: Static Code Analysis + +on: [push] + +jobs: + mypy: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install Pip Dependencies + run: pip install -r requirements.txt + + - name: Run MyPy + run: mypy solar_angles --disallow-untyped-calls --disallow-untyped-defs --check-untyped-defs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f6b83fe..f186e2d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -11,7 +11,7 @@ defaults: jobs: release: - runs-on: ubuntu-24.04 + runs-on: ubuntu-latest steps: - uses: actions/checkout@v4.1.7 - uses: actions/setup-python@v5.1.0 @@ -19,7 +19,7 @@ jobs: python-version: 3.12 - run: pip install wheel setuptools - run: python3 setup.py bdist_wheel sdist - - uses: pypa/gh-action-pypi-publish@v1.9.0 + - uses: pypa/gh-action-pypi-publish@v1.13.0 with: user: __token__ password: ${{ secrets.PYPIPW }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b37ebc8..320ab31 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -10,21 +10,20 @@ jobs: unit_testing: strategy: matrix: - os: [ windows-latest, macos-12, ubuntu-24.04 ] - py: [ "3.11", "3.12" ] + os: [ windows-latest, macos-latest, ubuntu-latest ] runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4.1.7 - name: Set up Python uses: actions/setup-python@v5.1.0 with: - python-version: ${{ matrix.py }} + python-version: 3.14 - name: Install Pip Dependencies from Requirements run: pip install -r requirements.txt - name: Run Tests run: coverage run -m pytest && coverage report -m - name: Coveralls - if: ${{ matrix.os == 'ubuntu-24.04' }} + if: ${{ matrix.os == 'ubuntu-latest' }} run: coveralls --service=github env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index 90749d3..29f915f 100755 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ This is a collection of solar angle and related calculations. ## Source These are based mostly on Chapter 6 of _Heating, Ventilation, and Air Conditioning_ by Faye McQuistion and Jerald Parker, 3rd Edition, 1988, with minor pieces from other versions of the same book. Other sources are noted in the source. All the functions were written from scratch by me. -## Releases [![PyPIRelease](https://github.com/Myoldmopar/SolarCalculations/actions/workflows/release.yml/badge.svg)](https://github.com/Myoldmopar/SolarCalculations/actions/workflows/release.yml) ![PyPI - Version](https://img.shields.io/pypi/v/solar-angles?color=44cc11) +## Releases [![PyPIRelease](https://github.com/Myoldmopar/SolarCalculations/actions/workflows/release.yml/badge.svg)](https://github.com/Myoldmopar/SolarCalculations/actions/workflows/release.yml) x The latest release can be found on the [Releases](https://github.com/Myoldmopar/SolarCalculations/releases/latest) page. All packages are distributed through [PyPi](https://pypi.org/project/solar-angles/). ## Documentation [![Documentation Status](https://readthedocs.org/projects/solarcalculations/badge/?version=latest)](https://solarcalculations.readthedocs.io/en/latest/?badge=latest) diff --git a/requirements.txt b/requirements.txt index 5f61d64..d1cd4be 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,6 +6,7 @@ pytest coverage coveralls flake8 +mypy # for documentation sphinx diff --git a/solar_angles/__init__.py b/solar_angles/__init__.py index 5dd2a90..acfdab3 100755 --- a/solar_angles/__init__.py +++ b/solar_angles/__init__.py @@ -1,2 +1,2 @@ PACKAGE_NAME = "solar_angles" -VERSION = "0.26" +VERSION = "0.30" diff --git a/solar_angles/demos/compare_to_eplus.py b/solar_angles/demos/compare_to_eplus.py index 70003cc..d807b4e 100755 --- a/solar_angles/demos/compare_to_eplus.py +++ b/solar_angles/demos/compare_to_eplus.py @@ -1,12 +1,12 @@ from datetime import datetime import csv -from solar_angles.solar import hour_angle, altitude_angle, azimuth_angle, solar_angle_of_incidence +from solar_angles.solar import hour_angle, altitude_angle, azimuth_angle, solar_angle_of_incidence, Angular # Golden, CO -longitude = 104.85 -standard_meridian = 105 -latitude = 39.57 +longitude = Angular(degrees=104.85) +standard_meridian = Angular(degrees=105) +latitude = Angular(degrees=39.57) with open('/tmp/compare_winter_angles_library.csv', 'w') as csvfile: my_writer = csv.writer(csvfile) @@ -14,9 +14,9 @@ for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor x = hour dt = datetime(2001, 12, 21, hour, 30, 00) - t_hour = hour_angle(dt, False, longitude, standard_meridian).degrees - altitude = altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees - azimuth = azimuth_angle(dt, False, longitude, standard_meridian, latitude).degrees + t_hour = hour_angle(dt, False, longitude, standard_meridian).degrees() + altitude = altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees() + azimuth = azimuth_angle(dt, False, longitude, standard_meridian, latitude).degrees() my_writer.writerow([x, -t_hour, altitude, azimuth]) with open('/tmp/compare_summer_angles_library.csv', 'w') as csvfile: @@ -25,19 +25,21 @@ for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor x = hour dt = datetime(2001, 7, 21, hour, 30, 00) - t_hour = hour_angle(dt, False, longitude, standard_meridian).degrees - altitude = altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees - azimuth = azimuth_angle(dt, False, longitude, standard_meridian, latitude).degrees + t_hour = hour_angle(dt, False, longitude, standard_meridian).degrees() + altitude = altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees() + azimuth = azimuth_angle(dt, False, longitude, standard_meridian, latitude).degrees() my_writer.writerow([x, -t_hour, altitude, azimuth]) with open('/tmp/compare_summer_incidence_library.csv', 'w') as csvfile: my_writer = csv.writer(csvfile) my_writer.writerow(['Hour', 'East Incidence', 'West Incidence']) + az_west = Angular(degrees=270) + az_east = Angular(degrees=90) for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor x = hour dt = datetime(2001, 7, 21, hour, 30, 00) theta_west = solar_angle_of_incidence( - dt, False, longitude, standard_meridian, latitude, 270).degrees + dt, False, longitude, standard_meridian, latitude, az_west) theta_east = solar_angle_of_incidence( - dt, False, longitude, standard_meridian, latitude, 90).degrees + dt, False, longitude, standard_meridian, latitude, az_east) my_writer.writerow([x, theta_east, theta_west]) diff --git a/solar_angles/demos/daylight_hours.py b/solar_angles/demos/daylight_hours.py index a427b3c..fbae742 100755 --- a/solar_angles/demos/daylight_hours.py +++ b/solar_angles/demos/daylight_hours.py @@ -1,29 +1,33 @@ # import the datetime library so we construct proper datetime instances from datetime import datetime +from math import nan # import the plotting library for demonstration -- pip install matplotlib should suffice import matplotlib.pyplot as plt # import the solar_angles library -from solar_angles.solar import altitude_angle +from solar_angles.solar import altitude_angle, Angular -def calculate_sun_up_time(array_of_altitude_angles): +def calculate_sun_up_time(array_of_altitude_angles: list[Angular]) -> float: found_above_time = False - last_alpha = None - for index, alpha in enumerate(array_of_altitude_angles): + last_alpha = -nan + for index, alpha_angle in enumerate(array_of_altitude_angles): + alpha = alpha_angle.degrees() if not found_above_time and alpha > 0: # we've got a match, calculate sun up and return sun_up_time = (index - 1) - (last_alpha / alpha) / (alpha - last_alpha) return sun_up_time else: last_alpha = alpha + return 0.0 -def calculate_sun_down_time(array_of_altitude_angles): +def calculate_sun_down_time(array_of_altitude_angles: list[Angular]) -> float: found_below_time = False - last_alpha = None - for index, alpha in enumerate(array_of_altitude_angles): + last_alpha = -nan + for index, alpha_angle in enumerate(array_of_altitude_angles): + alpha = alpha_angle.degrees() if index < 12: continue if not found_below_time and alpha < 0: @@ -32,12 +36,13 @@ def calculate_sun_down_time(array_of_altitude_angles): return sun_down_time else: last_alpha = alpha + return 0.0 # calculate times in Stillwater, OK -- to demonstrate the effect of longitude not lining up with the std meridian -longitude = 97.05 -standard_meridian = 90 -latitude = 36.11 +longitude = Angular(degrees=97.05) +standard_meridian = Angular(degrees=90) +latitude = Angular(degrees=36.11) x = [] for hour in range(0, 24): x.append(hour) @@ -45,45 +50,45 @@ def calculate_sun_down_time(array_of_altitude_angles): alpha0721 = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor dt = datetime(2001, 7, 21, hour, 00, 00) - alpha0721.append(altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees) + alpha0721.append(altitude_angle(dt, False, longitude, standard_meridian, latitude)) alpha0821 = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor dt = datetime(2001, 8, 21, hour, 00, 00) - alpha0821.append(altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees) + alpha0821.append(altitude_angle(dt, False, longitude, standard_meridian, latitude)) alpha0921 = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor dt = datetime(2001, 9, 21, hour, 00, 00) - alpha0921.append(altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees) + alpha0921.append(altitude_angle(dt, False, longitude, standard_meridian, latitude)) alpha1021 = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor dt = datetime(2001, 10, 21, hour, 00, 00) - alpha1021.append(altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees) + alpha1021.append(altitude_angle(dt, False, longitude, standard_meridian, latitude)) alpha1121 = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor dt = datetime(2001, 11, 21, hour, 00, 00) - alpha1121.append(altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees) + alpha1121.append(altitude_angle(dt, False, longitude, standard_meridian, latitude)) alpha1207 = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor dt = datetime(2001, 12, 7, hour, 00, 00) - alpha1207.append(altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees) + alpha1207.append(altitude_angle(dt, False, longitude, standard_meridian, latitude)) alpha1221 = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor dt = datetime(2001, 12, 21, hour, 00, 00) - alpha1221.append(altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees) - -plt.plot(x, alpha0721, 'purple', label='7/21', linewidth=1) -plt.plot(x, alpha0821, 'blue', label='8/21', linewidth=1) -plt.plot(x, alpha0921, 'green', label='9/21', linewidth=1) -plt.plot(x, alpha1021, 'yellow', label='10/21', linewidth=1) -plt.plot(x, alpha1121, 'orange', label='11/21', linewidth=1) -plt.plot(x, alpha1207, 'red', label='12/7', linewidth=1) -plt.plot(x, alpha1221, 'black', label='12/21', linewidth=1) + alpha1221.append(altitude_angle(dt, False, longitude, standard_meridian, latitude)) + +plt.plot(x, [x.degrees() for x in alpha0721], 'purple', label='7/21', linewidth=1) +plt.plot(x, [x.degrees() for x in alpha0821], 'blue', label='8/21', linewidth=1) +plt.plot(x, [x.degrees() for x in alpha0921], 'green', label='9/21', linewidth=1) +plt.plot(x, [x.degrees() for x in alpha1021], 'yellow', label='10/21', linewidth=1) +plt.plot(x, [x.degrees() for x in alpha1121], 'orange', label='11/21', linewidth=1) +plt.plot(x, [x.degrees() for x in alpha1207], 'red', label='12/7', linewidth=1) +plt.plot(x, [x.degrees() for x in alpha1221], 'black', label='12/21', linewidth=1) plt.xlim([0, 23]) plt.ylim([0, 90]) plt.suptitle("Time Values for Stillwater", fontsize=14, fontweight='bold') @@ -97,21 +102,21 @@ def calculate_sun_down_time(array_of_altitude_angles): alpha1207 = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor dt = datetime(2001, 12, 7, hour, 00, 00) - alpha1207.append(altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees) + alpha1207.append(altitude_angle(dt, False, longitude, standard_meridian, latitude)) alpha1221 = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor dt = datetime(2001, 12, 21, hour, 00, 00) - alpha1221.append(altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees) + alpha1221.append(altitude_angle(dt, False, longitude, standard_meridian, latitude)) alpha0107 = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor dt = datetime(2001, 1, 7, hour, 00, 00) - alpha0107.append(altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees) + alpha0107.append(altitude_angle(dt, False, longitude, standard_meridian, latitude)) -plt.plot(x, alpha1207, 'orange', label='12/7', linewidth=1) -plt.plot(x, alpha1221, 'black', label='12/21', linewidth=1) -plt.plot(x, alpha0107, 'red', label='1/7', linewidth=1) +plt.plot(x, [x.degrees() for x in alpha1207], 'orange', label='12/7', linewidth=1) +plt.plot(x, [x.degrees() for x in alpha1221], 'black', label='12/21', linewidth=1) +plt.plot(x, [x.degrees() for x in alpha0107], 'red', label='1/7', linewidth=1) plt.xlim([7, 18]) plt.ylim([0, 35]) plt.suptitle("Time Values for Stillwater", fontsize=14, fontweight='bold') diff --git a/solar_angles/demos/energyplus_validation.py b/solar_angles/demos/energyplus_validation.py index 9ffc288..05c41fd 100755 --- a/solar_angles/demos/energyplus_validation.py +++ b/solar_angles/demos/energyplus_validation.py @@ -7,24 +7,24 @@ # in this validation, we switch the latitude and longitude midway through the year -def get_latitude(month: int) -> float: +def get_latitude(month: int) -> solar.Angular: if month <= 1: - return 25.0 + return solar.Angular(degrees=25.0) else: - return 45.0 + return solar.Angular(degrees=45.0) -def get_longitude(month: int) -> float: +def get_longitude(month: int) -> solar.Angular: if month <= 1: - return 95.0 + return solar.Angular(degrees=95.0) else: - return 102.0 + return solar.Angular(degrees=102.0) -standard_meridian = 105 -east_wall_normal_from_north = 90 -south_wall_normal_from_north = 180 -west_wall_normal_from_north = 270 +standard_meridian = solar.Angular(degrees=105) +east_wall_normal_from_north = solar.Angular(degrees=90) +south_wall_normal_from_north = solar.Angular(degrees=180) +west_wall_normal_from_north = solar.Angular(degrees=270) with open('/tmp/eplus_validation_location.csv', 'w') as csvfile: my_writer = csv.writer(csvfile) my_writer.writerow( @@ -37,15 +37,15 @@ def get_longitude(month: int) -> float: for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor x = hour dt = datetime(2011, month_num, day, hour, 30, 00) - t_hour = solar.hour_angle(dt, False, thisLong, standard_meridian).degrees - altitude = solar.altitude_angle(dt, False, thisLong, standard_meridian, thisLat).degrees - azimuth = solar.azimuth_angle(dt, False, thisLong, standard_meridian, thisLat).degrees + t_hour = solar.hour_angle(dt, False, thisLong, standard_meridian).degrees() + altitude = solar.altitude_angle(dt, False, thisLong, standard_meridian, thisLat).degrees() + azimuth = solar.azimuth_angle(dt, False, thisLong, standard_meridian, thisLat).degrees() east_theta = solar.solar_angle_of_incidence(dt, False, thisLong, standard_meridian, thisLat, - east_wall_normal_from_north).radians + east_wall_normal_from_north).radians() south_theta = solar.solar_angle_of_incidence(dt, False, thisLong, standard_meridian, thisLat, - south_wall_normal_from_north).radians + south_wall_normal_from_north).radians() west_theta = solar.solar_angle_of_incidence(dt, False, thisLong, standard_meridian, thisLat, - west_wall_normal_from_north).radians + west_wall_normal_from_north).radians() if east_theta is not None: east_theta = math.cos(east_theta) if south_theta is not None: @@ -55,39 +55,39 @@ def get_longitude(month: int) -> float: my_writer.writerow([x, -t_hour, altitude, azimuth, east_theta, south_theta, west_theta]) -def get_wall_orientation(month: int) -> int: +def get_wall_orientation(month: int) -> solar.Angular: + val = 0 if month <= 1: - return 0 + val = 0 elif month == 2: - return 90 + val = 90 elif month == 3: - return 180 + val = 180 elif month == 4: - return 270 + val = 270 elif month == 5: - return 360 - else: - return 0 + val = 360 + return solar.Angular(degrees=val) with open('/tmp/eplus_validation_orientation.csv', 'w') as csvfile: my_writer = csv.writer(csvfile) my_writer.writerow(['Month', 'Date', 'Hour', 'Hour Angle', 'Solar Altitude', 'Solar Azimuth', 'Cos Wall Theta']) for month_num in range(1, 7): # just january through June to get all the way back through 360 and 0 again - thisLat = 39.57 - thisLong = 104.85 + thisLat = solar.Angular(degrees=39.57) + thisLong = solar.Angular(degrees=104.85) for day in range(1, monthrange(2011, month_num)[1] + 1): # just make sure it isn't a leap year for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor x = hour for minute in range(0, 60): # gives zero-based minutes, I think that's right...it should complain if not dt = datetime(2011, month_num, day, hour, minute, 00) - t_hour = solar.hour_angle(dt, False, thisLong, standard_meridian).degrees - altitude = solar.altitude_angle(dt, False, thisLong, standard_meridian, thisLat).degrees - azimuth = solar.azimuth_angle(dt, False, thisLong, standard_meridian, thisLat).degrees + t_hour = solar.hour_angle(dt, False, thisLong, standard_meridian).degrees() + altitude = solar.altitude_angle(dt, False, thisLong, standard_meridian, thisLat).degrees() + azimuth = solar.azimuth_angle(dt, False, thisLong, standard_meridian, thisLat).degrees() wall_degrees_from_north = get_wall_orientation(month_num) wall_theta = solar.solar_angle_of_incidence(dt, False, thisLong, standard_meridian, thisLat, - wall_degrees_from_north).radians + wall_degrees_from_north).radians() if wall_theta is not None: wall_theta = math.cos(wall_theta) my_writer.writerow([month_num, day, x, -t_hour, altitude, azimuth, wall_theta]) @@ -96,20 +96,20 @@ def get_wall_orientation(month: int) -> int: my_writer = csv.writer(csvfile) my_writer.writerow(['Month', 'Date', 'Hour', 'Hour Angle', 'Solar Altitude', 'Solar Azimuth', 'Cos Wall Theta']) for month_num in range(1, 7): # just january through June to get all the way back through 360 and 0 again - thisLat = 25 - thisLong = 95 + thisLat = solar.Angular(degrees=25) + thisLong = solar.Angular(degrees=95) for day in range(1, monthrange(2011, month_num)[1] + 1): # just make sure it isn't a leap year for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor x = hour for minute in range(0, 60): # gives zero-based minutes, I think that's right...it should complain if not dt = datetime(2011, month_num, day, hour, minute, 00) - t_hour = solar.hour_angle(dt, False, thisLong, standard_meridian).degrees - altitude = solar.altitude_angle(dt, False, thisLong, standard_meridian, thisLat).degrees - azimuth = solar.azimuth_angle(dt, False, thisLong, standard_meridian, thisLat).degrees - wall_degrees_from_north = 360 + t_hour = solar.hour_angle(dt, False, thisLong, standard_meridian).degrees() + altitude = solar.altitude_angle(dt, False, thisLong, standard_meridian, thisLat).degrees() + azimuth = solar.azimuth_angle(dt, False, thisLong, standard_meridian, thisLat).degrees() + wall_degrees_from_north = solar.Angular(degrees=360) wall_theta = solar.solar_angle_of_incidence(dt, False, thisLong, standard_meridian, thisLat, - wall_degrees_from_north).radians + wall_degrees_from_north).radians() if wall_theta is not None: wall_theta = math.cos(wall_theta) my_writer.writerow([month_num, day, x, -t_hour, altitude, azimuth, wall_theta]) diff --git a/solar_angles/demos/solar_angles.py b/solar_angles/demos/solar_angles.py index 1d1b688..612de0b 100755 --- a/solar_angles/demos/solar_angles.py +++ b/solar_angles/demos/solar_angles.py @@ -6,9 +6,9 @@ import matplotlib.pyplot as plt # calculate times in Stillwater, OK -- to demonstrate the effect of longitude not lining up with the standard meridian -longitude = 97.05 -standard_meridian = 90 -latitude = 36.11 +longitude = solar.Angular(degrees=97.05) +standard_meridian = solar.Angular(degrees=90) +latitude = solar.Angular(degrees=36.11) x = [] lct = [] lst = [] @@ -33,15 +33,15 @@ plt.close() # calculate hour angle for a summer day in Golden, CO -longitude = 105.2 -standard_meridian = 105 -latitude = 39.75 +longitude = solar.Angular(degrees=105.2) +standard_meridian = solar.Angular(degrees=105) +latitude = solar.Angular(degrees=39.75) x = [] hours = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor x.append(hour) dt = datetime(2001, 6, 21, hour, 00, 00) - hours.append(solar.hour_angle(dt, True, longitude, standard_meridian).degrees) + hours.append(solar.hour_angle(dt, True, longitude, standard_meridian).degrees()) plt.plot(x, hours, 'b', label='Hour Angle') plt.xlim([0, 23]) @@ -56,18 +56,18 @@ plt.close() # calculate solar_angles altitude angles for Winter and Summer days in Golden, CO -longitude = 105.2 -standard_meridian = 105 -latitude = 39.75 +longitude = solar.Angular(degrees=105.2) +standard_meridian = solar.Angular(degrees=105) +latitude = solar.Angular(degrees=39.75) x = [] beta_winter = [] beta_summer = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor x.append(hour) dt = datetime(2001, 12, 21, hour, 00, 00) - beta_winter.append(solar.altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees) + beta_winter.append(solar.altitude_angle(dt, False, longitude, standard_meridian, latitude).degrees()) dt = datetime(2001, 6, 21, hour, 00, 00) - beta_summer.append(solar.altitude_angle(dt, True, longitude, standard_meridian, latitude).degrees) + beta_summer.append(solar.altitude_angle(dt, True, longitude, standard_meridian, latitude).degrees()) plt.plot(x, beta_winter, 'b', label='Winter') plt.plot(x, beta_summer, 'r', label='Summer') @@ -83,15 +83,15 @@ plt.close() # calculate solar_angles azimuth angle for a summer day in Golden, CO -longitude = 105.2 -standard_meridian = 105 -latitude = 39.75 +longitude = solar.Angular(degrees=105.2) +standard_meridian = solar.Angular(degrees=105) +latitude = solar.Angular(degrees=39.75) x = [] solar_az = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor x.append(hour) dt = datetime(2001, 6, 21, hour, 00, 00) - solar_az.append(solar.azimuth_angle(dt, True, longitude, standard_meridian, latitude).degrees) + solar_az.append(solar.azimuth_angle(dt, True, longitude, standard_meridian, latitude).degrees()) plt.plot(x, solar_az, 'b', label='Solar Azimuth Angle') plt.xlim([0, 23]) @@ -106,25 +106,31 @@ plt.close() # calculate wall azimuth angles for a summer day in Golden, CO -longitude = 105.2 -standard_meridian = 105 -latitude = 39.75 +longitude = solar.Angular(degrees=105.2) +standard_meridian = solar.Angular(degrees=105) +latitude = solar.Angular(degrees=39.75) x = [] -east_wall_normal_from_north = 90 -east_az = [] -south_wall_normal_from_north = 180 -south_az = [] -west_wall_normal_from_north = 270 -west_az = [] +east_wall_normal_from_north = solar.Angular(degrees=90) +east_az: list[float] = [] +south_wall_normal_from_north = solar.Angular(degrees=180) +south_az: list[float] = [] +west_wall_normal_from_north = solar.Angular(degrees=270) +west_az: list[float] = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor x.append(hour) dt = datetime(2001, 6, 21, hour, 00, 00) east_az.append( - solar.wall_azimuth_angle(dt, True, longitude, standard_meridian, latitude, east_wall_normal_from_north).degrees) + solar.wall_azimuth_angle( + dt, True, longitude, standard_meridian, latitude, east_wall_normal_from_north + ).degrees() + ) south_az.append(solar.wall_azimuth_angle(dt, True, longitude, standard_meridian, latitude, - south_wall_normal_from_north).degrees) + south_wall_normal_from_north).degrees()) west_az.append( - solar.wall_azimuth_angle(dt, True, longitude, standard_meridian, latitude, west_wall_normal_from_north).degrees) + solar.wall_azimuth_angle( + dt, True, longitude, standard_meridian, latitude, west_wall_normal_from_north + ).degrees() + ) plt.plot(x, east_az, 'r', label='East Wall Azimuth Angle') plt.plot(x, south_az, 'g', label='South Wall Azimuth Angle') @@ -142,22 +148,25 @@ plt.close() # calculate solar_angles angle of incidence for a summer day in Golden, CO -longitude = 105.2 -standard_meridian = 105 -latitude = 39.75 +longitude = solar.Angular(degrees=105.2) +standard_meridian = solar.Angular(degrees=105) +latitude = solar.Angular(degrees=39.75) x = [] -east_wall_normal_from_north = 90 -east_theta = [] +east_wall_normal_from_north = solar.Angular(degrees=90) +east_theta: list[float] = [] east_az = [] -alt = [] +alt: list[float] = [] for hour in range(0, 24): # gives zero-based hours as expected in the datetime constructor x.append(hour) dt = datetime(2001, 6, 21, hour, 00, 00) east_az.append( - solar.wall_azimuth_angle(dt, True, longitude, standard_meridian, latitude, east_wall_normal_from_north).degrees) + solar.wall_azimuth_angle( + dt, True, longitude, standard_meridian, latitude, east_wall_normal_from_north + ).degrees() + ) east_theta.append(solar.solar_angle_of_incidence(dt, True, longitude, standard_meridian, latitude, - east_wall_normal_from_north).degrees) - alt.append(solar.altitude_angle(dt, True, longitude, standard_meridian, latitude).degrees) + east_wall_normal_from_north).degrees()) + alt.append(solar.altitude_angle(dt, True, longitude, standard_meridian, latitude).degrees()) plt.plot(x, alt, 'r', label='Solar Altitude Angle') plt.plot(x, east_az, 'g', label='East Wall Azimuth Angle') diff --git a/solar_angles/solar.py b/solar_angles/solar.py index 6f78d29..5d6d375 100755 --- a/solar_angles/solar.py +++ b/solar_angles/solar.py @@ -1,5 +1,6 @@ import math from datetime import datetime +from math import isnan # The calculations here are based on Chapter 6 of @@ -29,35 +30,42 @@ class Angular: otherwise a ValueError is thrown. """ - def __init__(self, radians=None, degrees=None): + def __init__(self, radians: float = math.nan, degrees: float = math.nan) -> None: """ Constructor for the class. Call it with either radians or degrees, not both. >>> a = Angular(radians=math.pi) >>> b = Angular(degrees=180) """ + self.valued = False + self._radians = math.nan + self._degrees = math.nan - if not radians and not degrees: - self.valued = False - self.radians = None - self.degrees = None - elif radians and not degrees: + if isnan(radians) and isnan(degrees): + raise ValueError("Neither Radians or Degrees given; failing") + elif isnan(degrees) and not isnan(radians): self.valued = True - self.radians = radians - self.degrees = math.degrees(radians) - elif degrees and not radians: + self._radians = radians + self._degrees = math.degrees(radians) + elif isnan(radians) and not isnan(degrees): self.valued = True - self.radians = math.radians(degrees) - self.degrees = degrees + self._radians = math.radians(degrees) + self._degrees = degrees else: # degrees and radians if abs(math.degrees(radians) - degrees) > 0.01: raise ValueError("Radians and Degrees both given but don't agree") self.valued = True - self.radians = radians - self.degrees = degrees + self._radians = radians + self._degrees = degrees def __str__(self) -> str: - return f"{self.valued=}, {self.radians=}, {self.degrees=}" + return f"{self.valued=}, {self._radians=}, {self._degrees=}" + + def radians(self) -> float: + return self._radians + + def degrees(self) -> float: + return self._degrees def day_of_year(time_stamp: datetime) -> int: @@ -120,13 +128,11 @@ def local_civil_time(time_stamp: datetime, daylight_savings_on: bool, longitude: :returns: [hours] Returns the local civil time in hours for the given date/time/location """ - if not all([x.valued for x in [longitude, standard_meridian]]): - raise ValueError("Invalid arguments to local_civil_time, must all be valid Angular objects") civil_hour = time_stamp.time().hour if daylight_savings_on: civil_hour -= 1 local_civil_time_hours = civil_hour + time_stamp.time().minute / 60.0 + time_stamp.time().second / 3600.0 - 4 * ( - longitude.degrees - standard_meridian.degrees) / 60.0 + longitude.degrees() - standard_meridian.degrees()) / 60.0 return local_civil_time_hours @@ -146,8 +152,6 @@ def local_solar_time(time_stamp: datetime, daylight_savings_on: bool, longitude: :returns: [hours] Returns the local solar time in hours for the given date/time/location """ - if not all([x.valued for x in [longitude, standard_meridian]]): - raise ValueError("Invalid arguments to local_solar_time, must all be valid Angular objects") return local_civil_time( time_stamp, daylight_savings_on, longitude, standard_meridian ) + equation_of_time(time_stamp) / 60.0 @@ -170,8 +174,6 @@ def hour_angle(time_stamp: datetime, daylight_savings_on: bool, longitude: Angul :returns: The hour angle in an Angular with both radian and degree versions """ - if not all([x.valued for x in [longitude, standard_meridian]]): - raise ValueError("Invalid arguments to hour_angle, must all be valid Angular objects") local_solar_time_hours = local_solar_time(time_stamp, daylight_savings_on, longitude, standard_meridian) hour_angle_deg = 15.0 * (local_solar_time_hours - 12) return Angular(degrees=hour_angle_deg) @@ -195,13 +197,11 @@ def altitude_angle(time_stamp: datetime, daylight_savings_on: bool, longitude: A :returns: [Angular] The solar altitude angle in an Angular with both radian and degree versions """ - if not all([x.valued for x in [longitude, standard_meridian, latitude]]): - raise ValueError("Invalid arguments to altitude_angle, must all be valid Angular objects") - declination_radians = declination_angle(time_stamp).radians - hour_radians = hour_angle(time_stamp, daylight_savings_on, longitude, standard_meridian).radians + declination_radians = declination_angle(time_stamp).radians() + hour_radians = hour_angle(time_stamp, daylight_savings_on, longitude, standard_meridian).radians() altitude_radians = math.asin( - math.cos(latitude.radians) * math.cos(declination_radians) * math.cos(hour_radians) + math.sin( - latitude.radians) * math.sin(declination_radians)) + math.cos(latitude.radians()) * math.cos(declination_radians) * math.cos(hour_radians) + math.sin( + latitude.radians()) * math.sin(declination_radians)) return Angular(radians=altitude_radians) @@ -210,7 +210,8 @@ def azimuth_angle(time_stamp: datetime, daylight_savings_on: bool, longitude: An """ Calculates the current solar azimuth angle for a given set of time and location conditions. The solar azimuth angle is the angle in the horizontal plane between due north and the sun. - It is measured clockwise, so that east is +90 degrees and west is +270 degrees. + It is measured clockwise, so that east is +90 degrees and west is +270 degrees. Throws if the + angle cannot be calculated because the sun is down :param time_stamp: The current date and time to be used in this calculation of day of year. :param daylight_savings_on: A flag if the current time is a daylight savings number. @@ -223,18 +224,15 @@ def azimuth_angle(time_stamp: datetime, daylight_savings_on: bool, longitude: An For Golden, CO, the variable should be = 39.75 degrees. :returns: [Angular] The solar azimuth angle in an Angular with both radian and degree versions. - NOTE: If the sun is down, the Float values in the dictionary are None. """ - if not all([x.valued for x in [longitude, standard_meridian, latitude]]): - raise ValueError("Invalid arguments to azimuth_angle, must all be valid Angular objects") - declination_radians = declination_angle(time_stamp).radians + declination_radians = declination_angle(time_stamp).radians() altitude = altitude_angle(time_stamp, daylight_savings_on, longitude, standard_meridian, latitude) - if altitude.degrees < 0: # sun is down - return Angular() - hour_radians = hour_angle(time_stamp, daylight_savings_on, longitude, standard_meridian).radians + if altitude.degrees() < 0: # sun is down + raise ValueError("Cannot calculate azimuth angle because sun is down") + hour_radians = hour_angle(time_stamp, daylight_savings_on, longitude, standard_meridian).radians() acos_from_south = math.acos( - (math.sin(altitude.radians) * math.sin(latitude.radians) - math.sin(declination_radians)) / ( - math.cos(altitude.radians) * math.cos(latitude.radians))) + (math.sin(altitude.radians()) * math.sin(latitude.radians()) - math.sin(declination_radians)) / ( + math.cos(altitude.radians()) * math.cos(latitude.radians()))) if hour_radians < 0: azimuth_from_south = acos_from_south else: @@ -248,7 +246,7 @@ def wall_azimuth_angle(time_stamp: datetime, daylight_savings_on: bool, longitud """ Calculates the current wall azimuth angle for a given set of time/location conditions, and a surface orientation. The wall azimuth angle is the angle in the horizontal plane between the solar azimuth - and the vertical wall's outward facing normal vector. + and the vertical wall's outward facing normal vector. Throws if the sun is behind the surface or the sun is down. :param time_stamp: The current date and time to be used in this calculation of day of year. :param daylight_savings_on: A flag if the current time is a daylight savings number. @@ -264,17 +262,15 @@ def wall_azimuth_angle(time_stamp: datetime, daylight_savings_on: bool, longitud (southwest facing surface: 225 degrees, northwest facing surface: 315 degrees) :returns: [Angular] The wall azimuth angle in an Angular with both radian and degree versions. - NOTE: If the sun is behind the surface, the Float values in the object are None. """ - if not all([x.valued for x in [longitude, standard_meridian, latitude, surface_azimuth]]): - raise ValueError("Invalid arguments to wall_azimuth_angle, must all be valid Angular objects") - this_surface_azimuth_deg = surface_azimuth.degrees % 360 - solar_azimuth = azimuth_angle(time_stamp, daylight_savings_on, longitude, standard_meridian, latitude).degrees - if solar_azimuth is None: # sun is down - return Angular() + this_surface_azimuth_deg = surface_azimuth.degrees() % 360 + try: + solar_azimuth = azimuth_angle(time_stamp, daylight_savings_on, longitude, standard_meridian, latitude).degrees() + except ValueError: # TODO: Make SunIsDownException + raise ValueError("Cannot calculate wall azimuth angle because sun is down") from None wall_azimuth_degrees = solar_azimuth - this_surface_azimuth_deg if wall_azimuth_degrees > 90 or wall_azimuth_degrees < -90: - return Angular() + raise ValueError("Cannot calculate wall azimuth angle because sun is behind surface") return Angular(degrees=wall_azimuth_degrees) @@ -284,7 +280,7 @@ def solar_angle_of_incidence(time_stamp: datetime, daylight_savings_on: bool, lo """ Calculates the solar angle of incidence for a given set of time and location conditions, and a surface orientation. The solar angle of incidence is the angle between the solar ray vector incident on the surface, - and the outward facing surface normal vector. + and the outward facing surface normal vector. Throws if the wall azimuth cannot be calculated :param time_stamp: The current date and time to be used in this calculation of day of year. :param daylight_savings_on: A flag if the current time is a daylight savings number. @@ -300,15 +296,13 @@ def solar_angle_of_incidence(time_stamp: datetime, daylight_savings_on: bool, lo (southwest facing surface: 225 degrees, northwest facing surface: 315 degrees) :returns: [Angular] The solar angle of incidence in an Angular with both radian & degree versions. - NOTE: If the sun is down, or behind the surface, the Float values in the object are None. """ - if not all([x.valued for x in [longitude, standard_meridian, latitude, surface_azimuth]]): - raise ValueError("Invalid arguments to solar_angle_of_incidence, must all be valid Angular objects") - wall_azimuth_rad = wall_azimuth_angle(time_stamp, daylight_savings_on, longitude, standard_meridian, latitude, - surface_azimuth).radians - if wall_azimuth_rad is None: - return Angular() - altitude_rad = altitude_angle(time_stamp, daylight_savings_on, longitude, standard_meridian, latitude).radians + try: + wall_azimuth_rad = wall_azimuth_angle(time_stamp, daylight_savings_on, longitude, standard_meridian, latitude, + surface_azimuth).radians() + except ValueError: # TODO: Make SunIsDownException + raise ValueError("Cannot calculate wall azimuth angle because sun is down") from None + altitude_rad = altitude_angle(time_stamp, daylight_savings_on, longitude, standard_meridian, latitude).radians() incidence_angle_radians = math.acos(math.cos(altitude_rad) * math.cos(wall_azimuth_rad)) return Angular(radians=incidence_angle_radians) @@ -338,8 +332,6 @@ def direct_radiation_on_surface(time_stamp: datetime, daylight_savings_on: bool, :returns: The incident direct radiation on the surface. The units of this return value match the units of the parameter :horizontal_direct_irradiation: """ - if not all([x.valued for x in [longitude, standard_meridian, latitude, surface_azimuth]]): - raise ValueError("Invalid arguments to direct_radiation_on_surface, must all be valid Angular objects") theta = solar_angle_of_incidence(time_stamp, daylight_savings_on, longitude, standard_meridian, latitude, - surface_azimuth).radians + surface_azimuth).radians() return horizontal_direct_irradiation * math.cos(theta) diff --git a/solar_angles/test/test_solar.py b/solar_angles/test/test_solar.py index af7f8a2..44ffff4 100755 --- a/solar_angles/test/test_solar.py +++ b/solar_angles/test/test_solar.py @@ -20,26 +20,27 @@ class TestAngularValueType(TestCase): - def test_construction(self): - self.assertFalse(Angular().valued) + def test_construction(self) -> None: + with self.assertRaises(ValueError): + Angular() self.assertTrue(Angular(degrees=1).valued) self.assertTrue(Angular(radians=1).valued) self.assertTrue(Angular(degrees=180, radians=3.14159).valued) with self.assertRaises(ValueError): Angular(degrees=180, radians=2 * 3.14) - def test_string(self): + def test_string(self) -> None: a = Angular(degrees=1) self.assertIsInstance(str(a), str) class TestDayOfYear(TestCase): - def test_first_day_of_year(self): + def test_first_day_of_year(self) -> None: self.assertEqual(day_of_year(datetime(1999, 1, 1, 00, 00, 00)), 1) self.assertEqual(day_of_year(datetime(2000, 1, 1, 00, 00, 00)), 1) - def test_last_day_of_year(self): + def test_last_day_of_year(self) -> None: # regular year self.assertEqual(day_of_year(datetime(1995, 12, 31, 00, 00, 00)), 365) # leap year @@ -49,15 +50,17 @@ def test_last_day_of_year(self): # yes leap year on millenniums though! self.assertEqual(day_of_year(datetime(2000, 12, 31, 00, 00, 00)), 366) - def test_bad_input(self): + def test_bad_input(self) -> None: with self.assertRaises(TypeError): + # can't just pass in a date # noinspection PyTypeChecker - day_of_year(date(2006, 5, 20)) # can't just pass in a date + day_of_year(date(2006, 5, 20)) # type: ignore[arg-type] with self.assertRaises(TypeError): + # can't just pass in a time # noinspection PyTypeChecker - day_of_year(time(12, 0, 0)) # can't just pass in a time + day_of_year(time(12, 0, 0)) # type: ignore[arg-type] - def test_right_now(self): + def test_right_now(self) -> None: # we don't wrap this in an assertEqual because we don't know what the output will be # if it throws, that's a problem and the unittest framework will catch it day_of_year(datetime.now()) @@ -66,7 +69,7 @@ def test_right_now(self): class TestEquationOfTime(TestCase): # validation from Table 6-1 of the reference listed above - def test_equation_of_time_on_the_21s(self): + def test_equation_of_time_on_the_21s(self) -> None: tolerance = 0.5 # 30 seconds... self.assertAlmostEqual(equation_of_time(datetime(2001, 1, 21, 00, 00, 00)), -11.2, delta=tolerance) self.assertAlmostEqual(equation_of_time(datetime(2001, 2, 21, 00, 00, 00)), -13.9, delta=tolerance) @@ -85,33 +88,33 @@ def test_equation_of_time_on_the_21s(self): class TestDeclinationAngle(TestCase): # validation from Table 6-1 of the reference listed above - def test_declinations_on_the_21s(self): + def test_declinations_on_the_21s(self) -> None: tolerance = 1.25 # 1.25 degrees... - self.assertAlmostEqual(declination_angle(datetime(2001, 1, 21, 00, 00, 00)).degrees, -20.2, delta=tolerance) - self.assertAlmostEqual(declination_angle(datetime(2001, 2, 21, 00, 00, 00)).degrees, -10.8, delta=tolerance) - self.assertAlmostEqual(declination_angle(datetime(2001, 3, 21, 00, 00, 00)).degrees, 0.0, delta=tolerance) - self.assertAlmostEqual(declination_angle(datetime(2001, 4, 21, 00, 00, 00)).degrees, 11.6, delta=tolerance) - self.assertAlmostEqual(declination_angle(datetime(2001, 5, 21, 00, 00, 00)).degrees, 20.0, delta=tolerance) - self.assertAlmostEqual(declination_angle(datetime(2001, 6, 21, 00, 00, 00)).degrees, 23.5, delta=tolerance) - self.assertAlmostEqual(declination_angle(datetime(2001, 7, 21, 00, 00, 00)).degrees, 20.6, delta=tolerance) - self.assertAlmostEqual(declination_angle(datetime(2001, 8, 21, 00, 00, 00)).degrees, 12.3, delta=tolerance) - self.assertAlmostEqual(declination_angle(datetime(2001, 9, 21, 00, 00, 00)).degrees, 0.0, delta=tolerance) - self.assertAlmostEqual(declination_angle(datetime(2001, 10, 21, 00, 00, 00)).degrees, -10.5, delta=tolerance) - self.assertAlmostEqual(declination_angle(datetime(2001, 11, 21, 00, 00, 00)).degrees, -19.8, delta=tolerance) - self.assertAlmostEqual(declination_angle(datetime(2001, 12, 21, 00, 00, 00)).degrees, -23.5, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 1, 21, 00, 00, 00))._degrees, -20.2, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 2, 21, 00, 00, 00))._degrees, -10.8, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 3, 21, 00, 00, 00))._degrees, 0.0, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 4, 21, 00, 00, 00))._degrees, 11.6, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 5, 21, 00, 00, 00))._degrees, 20.0, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 6, 21, 00, 00, 00))._degrees, 23.5, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 7, 21, 00, 00, 00))._degrees, 20.6, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 8, 21, 00, 00, 00))._degrees, 12.3, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 9, 21, 00, 00, 00))._degrees, 0.0, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 10, 21, 00, 00, 00))._degrees, -10.5, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 11, 21, 00, 00, 00))._degrees, -19.8, delta=tolerance) + self.assertAlmostEqual(declination_angle(datetime(2001, 12, 21, 00, 00, 00))._degrees, -23.5, delta=tolerance) class TestLocalCivilTime(TestCase): # validation from example 6-1 of the 5th Edition of McQuiston - def test_example_5_6_1(self): + def test_example_5_6_1(self) -> None: dt = datetime(2001, 2, 21, 11, 00, 00) dst_on = True longitude = Angular(degrees=95) standard_meridian = Angular(degrees=90) self.assertAlmostEqual(local_civil_time(dt, dst_on, longitude, standard_meridian), 9.67, delta=0.01) - def test_bad_arguments(self): + def test_bad_arguments(self) -> None: with self.assertRaises(ValueError): local_civil_time(datetime.now(), True, Angular(), Angular()) @@ -119,14 +122,14 @@ def test_bad_arguments(self): class TestLocalSolarTime(TestCase): # validation from example 6-1 of the 5th Edition of McQuiston - def test_example_5_6_1(self): + def test_example_5_6_1(self) -> None: dt = datetime(2001, 2, 21, 11, 00, 00) dst_on = True longitude = Angular(degrees=95) standard_meridian = Angular(degrees=90) self.assertAlmostEqual(local_solar_time(dt, dst_on, longitude, standard_meridian), 9.43, delta=0.01) - def test_bad_arguments(self): + def test_bad_arguments(self) -> None: with self.assertRaises(ValueError): local_solar_time(datetime.now(), True, Angular(), Angular()) @@ -134,7 +137,7 @@ def test_bad_arguments(self): class TestHourAngle(TestCase): # validation from example 6-2 of the 5th Edition of McQuiston - def test_example_5_6_2(self): + def test_example_5_6_2(self) -> None: dt = datetime(2001, 7, 21, 10, 00, 00) dst_on = True longitude = Angular(degrees=85) @@ -142,19 +145,19 @@ def test_example_5_6_2(self): self.assertAlmostEqual(local_civil_time(dt, dst_on, longitude, standard_meridian), 9.3, delta=0.1) self.assertAlmostEqual(equation_of_time(dt), -6.2, delta=0.2) self.assertAlmostEqual(local_solar_time(dt, dst_on, longitude, standard_meridian), 9.23, delta=0.01) - self.assertAlmostEqual(hour_angle(dt, dst_on, longitude, standard_meridian).degrees, -41.5, + self.assertAlmostEqual(hour_angle(dt, dst_on, longitude, standard_meridian)._degrees, -41.5, delta=0.1) # we are using negative in the morning; positive in the afternoon # test solar_angles noon on standard meridian, should be zero right? - def test_solar_noon(self): + def test_solar_noon(self) -> None: # chose June 15 because EOT goes near zero on that date dt = datetime(2001, 6, 15, 12, 0, 0) dst_on = False longitude = Angular(degrees=90) standard_meridian = Angular(degrees=90) - self.assertAlmostEqual(hour_angle(dt, dst_on, longitude, standard_meridian).degrees, 0, delta=0.1) + self.assertAlmostEqual(hour_angle(dt, dst_on, longitude, standard_meridian)._degrees, 0, delta=0.1) - def test_bad_arguments(self): + def test_bad_arguments(self) -> None: with self.assertRaises(ValueError): hour_angle(datetime.now(), True, Angular(), Angular()) @@ -162,17 +165,17 @@ def test_bad_arguments(self): class TestAltitudeAngle(TestCase): # validation from example 6-2 of the 5th Edition of McQuiston - def test_example_5_6_2(self): + def test_example_5_6_2(self) -> None: dt = datetime(2001, 7, 21, 10, 00, 00) dst_on = True longitude = Angular(degrees=85) standard_meridian = Angular(degrees=90) latitude = Angular(degrees=40) self.assertAlmostEqual( - altitude_angle(dt, dst_on, longitude, standard_meridian, latitude).degrees, 49.7, delta=0.1 + altitude_angle(dt, dst_on, longitude, standard_meridian, latitude)._degrees, 49.7, delta=0.1 ) - def test_bad_arguments(self): + def test_bad_arguments(self) -> None: with self.assertRaises(ValueError): altitude_angle(datetime.now(), True, Angular(), Angular(), Angular()) @@ -180,19 +183,19 @@ def test_bad_arguments(self): class TestAzimuthAngle(TestCase): # validation from example 6-2 of the 5th Edition of McQuiston - def test_example_5_6_2(self): + def test_example_5_6_2(self) -> None: dt = datetime(2001, 7, 21, 10, 00, 00) dst_on = True longitude = Angular(degrees=85) standard_meridian = Angular(degrees=90) latitude = Angular(degrees=40) expected_azimuth_from_south = Angular(degrees=73.7) - expected_azimuth_from_north = 180 - expected_azimuth_from_south.degrees - self.assertAlmostEqual(azimuth_angle(dt, dst_on, longitude, standard_meridian, latitude).degrees, + expected_azimuth_from_north = 180 - expected_azimuth_from_south._degrees + self.assertAlmostEqual(azimuth_angle(dt, dst_on, longitude, standard_meridian, latitude)._degrees, expected_azimuth_from_north, delta=0.1) # how about an afternoon one? - def test_afternoon(self): + def test_afternoon(self) -> None: dt = datetime(2001, 7, 21, 16, 00, 00) dst_on = True longitude = Angular(degrees=85) @@ -202,15 +205,16 @@ def test_afternoon(self): self.assertTrue(azimuth_angle(dt, dst_on, longitude, standard_meridian, latitude).valued) # test one with the sun down to get a null-ish response - def test_sun_is_down(self): + def test_sun_is_down(self) -> None: dt = datetime(2001, 3, 21, 22, 00, 00) dst_on = True longitude = Angular(degrees=85) standard_meridian = Angular(degrees=90) latitude = Angular(degrees=40) - self.assertFalse(azimuth_angle(dt, dst_on, longitude, standard_meridian, latitude).valued) + with self.assertRaises(ValueError): + azimuth_angle(dt, dst_on, longitude, standard_meridian, latitude) - def test_bad_arguments(self): + def test_bad_arguments(self) -> None: with self.assertRaises(ValueError): azimuth_angle(datetime.now(), True, Angular(), Angular(), Angular()) @@ -218,7 +222,7 @@ def test_bad_arguments(self): class TestWallAzimuthAngle(TestCase): # test south? facing wall where the solar_angles azimuth is known from a prior unit test - def test_gamma_south_facing(self): + def test_gamma_south_facing(self) -> None: dt = datetime(2001, 7, 21, 10, 00, 00) dst_on = True longitude = Angular(degrees=85) @@ -226,34 +230,36 @@ def test_gamma_south_facing(self): latitude = Angular(degrees=40) wall_normal = Angular(degrees=90) expected_solar_azimuth = 180 - 73.7 - expected_wall_azimuth = expected_solar_azimuth - wall_normal.degrees + expected_wall_azimuth = expected_solar_azimuth - wall_normal._degrees self.assertAlmostEqual( - wall_azimuth_angle(dt, dst_on, longitude, standard_meridian, latitude, wall_normal).degrees, + wall_azimuth_angle(dt, dst_on, longitude, standard_meridian, latitude, wall_normal)._degrees, expected_wall_azimuth, delta=0.1 ) # test north facing wall where the azimuth should be behind the wall - def test_gamma_north_facing(self): + def test_gamma_north_facing(self) -> None: dt = datetime(2001, 7, 21, 10, 00, 00) dst_on = True longitude = Angular(degrees=85) standard_meridian = Angular(degrees=90) latitude = Angular(degrees=40) wall_normal = Angular(degrees=270) - self.assertFalse(wall_azimuth_angle(dt, dst_on, longitude, standard_meridian, latitude, wall_normal).valued) + with self.assertRaises(ValueError): + wall_azimuth_angle(dt, dst_on, longitude, standard_meridian, latitude, wall_normal) # test one with the sun down to get a null-ish response - def test_sun_is_down(self): + def test_sun_is_down(self) -> None: dt = datetime(2001, 3, 21, 22, 00, 00) dst_on = True longitude = Angular(degrees=85) standard_meridian = Angular(degrees=90) latitude = Angular(degrees=40) wall_normal = Angular(degrees=90) - self.assertFalse(wall_azimuth_angle(dt, dst_on, longitude, standard_meridian, latitude, wall_normal).valued) + with self.assertRaises(ValueError): + wall_azimuth_angle(dt, dst_on, longitude, standard_meridian, latitude, wall_normal) - def test_bad_arguments(self): + def test_bad_arguments(self) -> None: with self.assertRaises(ValueError): wall_azimuth_angle(datetime.now(), True, Angular(), Angular(), Angular(), Angular()) @@ -261,7 +267,7 @@ def test_bad_arguments(self): class TestSolarAngleOfIncidence(TestCase): # test east facing surface where solar_angles azimuth and altitude are known from prior unit tests - def test_theta_south_facing(self): + def test_theta_south_facing(self) -> None: dt = datetime(2001, 7, 21, 10, 00, 00) dst_on = True longitude = Angular(degrees=85) @@ -269,59 +275,58 @@ def test_theta_south_facing(self): latitude = Angular(degrees=40) wall_normal = Angular(degrees=90) expected_solar_azimuth = 180 - 73.7 - expected_wall_azimuth = radians(expected_solar_azimuth - wall_normal.degrees) + expected_wall_azimuth = radians(expected_solar_azimuth - wall_normal._degrees) expected_solar_altitude = radians(49.7) expected_theta = acos(cos(expected_wall_azimuth) * cos(expected_solar_altitude)) - angle = solar_angle_of_incidence(dt, dst_on, longitude, standard_meridian, latitude, wall_normal).radians + angle = solar_angle_of_incidence(dt, dst_on, longitude, standard_meridian, latitude, wall_normal)._radians self.assertAlmostEqual(angle, expected_theta, delta=0.001) # test case for azimuth specified greater than 360 - def test_over_rotated_surface(self): + def test_over_rotated_surface(self) -> None: dt = datetime(2001, 7, 21, 10, 00, 00) dst_on = True longitude = Angular(degrees=85) standard_meridian = Angular(degrees=90) latitude = Angular(degrees=40) wall_normal = Angular(degrees=90) # south, degrees - base_theta = solar_angle_of_incidence(dt, dst_on, longitude, standard_meridian, latitude, wall_normal).radians + base_theta = solar_angle_of_incidence(dt, dst_on, longitude, standard_meridian, latitude, wall_normal)._radians wall_normal = Angular(degrees=90+360) # south, degrees over_rotated_theta = solar_angle_of_incidence(dt, dst_on, longitude, standard_meridian, latitude, - wall_normal).radians + wall_normal)._radians self.assertAlmostEqual(over_rotated_theta, base_theta, delta=0.001) # test one with the sun down to get a null-ish response - def test_sun_is_down(self): + def test_sun_is_down(self) -> None: dt = datetime(2001, 3, 21, 22, 00, 00) dst_on = True longitude = Angular(degrees=85) standard_meridian = Angular(degrees=90) latitude = Angular(degrees=40) wall_normal = Angular(degrees=90) # north, degrees - self.assertFalse( - solar_angle_of_incidence(dt, dst_on, longitude, standard_meridian, latitude, wall_normal).valued - ) + with self.assertRaises(ValueError): + solar_angle_of_incidence(dt, dst_on, longitude, standard_meridian, latitude, wall_normal) - def test_bad_arguments(self): + def test_bad_arguments(self) -> None: with self.assertRaises(ValueError): solar_angle_of_incidence(datetime.now(), True, Angular(), Angular(), Angular(), Angular()) class TestRadiationOnSurface(TestCase): - def test_direct_radiation_on_surface_south_facing(self): + def test_direct_radiation_on_surface_south_facing(self) -> None: dt = datetime(2001, 7, 21, 10, 00, 00) dst_on = True longitude = Angular(degrees=85) standard_meridian = Angular(degrees=90) latitude = Angular(degrees=40) wall_normal = Angular(degrees=180) # south, degrees - theta = solar_angle_of_incidence(dt, dst_on, longitude, standard_meridian, latitude, wall_normal).radians + theta = solar_angle_of_incidence(dt, dst_on, longitude, standard_meridian, latitude, wall_normal)._radians insolation = 293 # watts self.assertAlmostEqual( direct_radiation_on_surface(dt, dst_on, longitude, standard_meridian, latitude, wall_normal, insolation), insolation * cos(theta), delta=0.1) - def test_bad_arguments(self): + def test_bad_arguments(self) -> None: with self.assertRaises(ValueError): direct_radiation_on_surface( datetime.now(), True, Angular(), Angular(), Angular(), Angular(), 1000