# 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 .exceptions import InvalidObserverLatitudeError, MissingObserverLatitudeError
from .sun_position import SunPosition
[docs]
class Observer:
r"""
A class to model an observer based on horizontal and equatorial pictures
of the sun-earth geometry.
"""
def __init__(
self,
observer_latitude: Optional[float] = None,
observer_longitude: Optional[float] = None,
):
"""
To instantiate the ``Observer`` class, provide the following parameter.
: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 = observer_latitude
self.observer_longitude = observer_longitude
self.sun_position = SunPosition()
[docs]
def calculate_zenith_angle(self, day_of_year: int, solar_time: float) -> float:
r"""
Calculate the solar zenith angle.
The solar zenith angle is calculated using the formula:
.. math::
\cos(\theta_z) = \sin(\phi) \times \sin(\delta)
+ \cos(\phi) \times \cos(\delta) \times \cos(\omega)
| - :math:`\theta_z` is the solar zenith angle
| - :math:`\phi` is the latitude of the observer
| - :math:`\delta` is the solar declination
| - :math:`\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
"""
observer_latitude = self._ensure_latitude_provided()
solar_declination = self.sun_position.solar_declination(day_of_year)
hour_angle = self.sun_position.hour_angle(solar_time)
return math.acos(
math.sin(observer_latitude) * math.sin(solar_declination)
+ math.cos(observer_latitude)
* math.cos(solar_declination)
* math.cos(hour_angle)
)
[docs]
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:
.. math::
\cos(\omega) = -\tan(\phi) \times \tan(\delta)
| - :math:`\omega` is the hour angle
| - :math:`\phi` is the latitude of the observer
| - :math:`\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._ensure_latitude_provided()
solar_declination = self.sun_position.solar_declination(day_of_year)
tan_product = -math.tan(observer_latitude) * math.tan(solar_declination)
if tan_product > 1:
return 0, 0
if tan_product < -1:
return -math.pi, math.pi
hour_angle = math.acos(tan_product)
sunrise = -hour_angle
sunset = hour_angle
return sunrise, sunset
def _ensure_latitude_provided(self) -> float:
if self.observer_latitude is None:
raise MissingObserverLatitudeError()
if not math.radians(-88) <= self.observer_latitude <= math.radians(88):
raise InvalidObserverLatitudeError()
return self.observer_latitude
_observer_latitude: Optional[float]
@property
def observer_latitude(self) -> Optional[float]:
"""
Getter for the observer's latitude.
:return: The observer's latitude in radians.
:rtype: Optional[float]
"""
return self._observer_latitude
@observer_latitude.setter
def observer_latitude(self, value: Optional[float]):
"""
Setter for the observer's latitude. Converts the value to radians if not None.
:param value: The observer's latitude in degrees.
:type value: Optional[float]
"""
self._observer_latitude = math.radians(value) if value is not None else None
_observer_longitude: Optional[float]
@property
def observer_longitude(self) -> Optional[float]:
"""
Getter for the observer's longitude.
:return: The observer's longitude in radians.
:rtype: Optional[float]
"""
return self._observer_longitude
@observer_longitude.setter
def observer_longitude(self, value: Optional[float]):
"""
Setter for the observer's longitude. Converts the value to radians if not None.
:param value: The observer's longitude in degrees.
:type value: Optional[float]
"""
self._observer_longitude = math.radians(value) if value is not None else None