Ejemplo n.º 1
0
def calculate_hour_angle_degrees(
    local_standard_time: Union[datetime, str, Iterable[Union[datetime, str]]],
    longitude_degrees: Union[int, float],
) -> Union[float, Iterable[float]]:
    """
    The hour angle is the angular displacement of the
    sun east (negative) or west (positive) of the local
    meridian due to rotation of the earth on its axis at
    15 degrees per hour, which must be between
    -180 and 180 degrees (+/- 15 degreees/hour * 12 hours).

    :param local_standard_time: A `datetime` object,
        containing a timezone offset, representing the time that
        will be converted to solar time.
    :param longitude_degrees: A numeric value representing a location's
        angular distance west of the meridian at Greenwich, England.
        `longitude_degrees` should be between 0 and 360 degrees.

    :returns: A float value representing the angular displacement
        of the sun.
    """

    # The `convert_to_solar_time()` method will validate the inputs
    solar_ts = convert_to_solar_time(local_standard_time, longitude_degrees)

    # Create a datetime object for noon on the same date as `solar_ts`
    try:
        solar_noon = np.array([
            datetime(
                year=x.date().year,
                month=x.date().month,
                day=x.date().day,
                hour=12,
                minute=0,
                second=0,
                tzinfo=x.tzinfo,
            ) for x in solar_ts
        ])
    except TypeError:
        solar_noon = datetime(
            year=solar_ts.date().year,
            month=solar_ts.date().month,
            day=solar_ts.date().day,
            hour=12,
            minute=0,
            second=0,
            tzinfo=solar_ts.tzinfo,
        )

    # Calculate the difference (in hours) and multiply by 15
    if not (isinstance(solar_noon, np.ndarray)):
        hours_diff = (solar_ts - solar_noon).total_seconds() / 3600
    else:
        hours_diff = np.array([(x - solar_noon[i]).total_seconds() / 3600
                               for i, x in enumerate(solar_ts)])
    hour_angle = hours_diff * 15.0

    # Valiate `hour_angle`
    validate_numeric_value(hour_angle, minimum=-180, maximum=180)
    return hour_angle
Ejemplo n.º 2
0
def test_validate_numeric_value_iterable():
    """Functional test to ensure validate_numeric_value()
    runs properly for range-valid iterables."""
    validate_numeric_value([0, 10, 20, 30], minimum=-1e6, maximum=1e6)
    validate_numeric_value([-1000.5, 0, 10.4, -20.7],
                           minimum=-1e6,
                           maximum=1e6)
Ejemplo n.º 3
0
def test_outside_range():
    """Test to ensure that a ValueError is thrown
    when a value less than `minimum` or greater than
    `maximum` is provided."""
    with pytest.raises(ValueError):
        # Check `minimum` requirement.
        assert validate_numeric_value(-10, minimum=0, maximum=10)
    with pytest.raises(ValueError):
        # Check `maximum` requirement
        assert validate_numeric_value(10, minimum=0, maximum=9)
Ejemplo n.º 4
0
def calculate_declination_degrees(
    B_degrees: Union[int, float, Iterable[Union[int, float]]]
) -> Union[float, Iterable[float]]:
    """
    The declination is the angular position of the sun
    at solar noon with respect to the plane of the
    equator (north is positive).  The declination angle
    must be between -23.45 and 23.45 degrees.
    The equation is from Spencer (1971).

    The equation used is from Duffie & Beckman (2006)
    Equation 1.6.1b.

    :param B_degrees: A numeric value (generally a float)
        which is calculated based on the day of the year,
        in units of degrees.

    :returns: A float value representing the
        declination angle of the sun.
    """

    # Type-check `B_degrees`
    ensure_numeric(
        B_degrees,
        valid_types=[int, float, np.number],
        nan_acceptable=False,
        inf_acceptable=False,
    )
    # Range-check `B_degrees` and `G_sc`
    validate_numeric_value(
        B_degrees,
        minimum=calculate_B_degrees(1),
        maximum=calculate_B_degrees(366),
    )

    # Convert `B_degrees` to radians for use in the calculation
    B_radians = np.radians(B_degrees)
    declination_degrees = (180.0 / np.pi *
                           (0.006918 - (0.399912 * np.cos(B_radians)) +
                            (0.070257 * np.sin(B_radians)) -
                            (0.006758 * np.cos(2 * B_radians)) +
                            (0.000907 * np.sin(2 * B_radians)) -
                            (0.002697 * np.cos(3 * B_radians)) +
                            (0.00148 * np.sin(3 * B_radians))))

    # Range-check `declination_degrees` before returning
    validate_numeric_value(declination_degrees, minimum=-23.45, maximum=23.45)

    return declination_degrees
Ejemplo n.º 5
0
def calculate_solar_altitude_degrees(
    solar_zenith_degrees: Union[float, Iterable[float]]
) -> Union[float, Iterable[float]]:
    """
    The solar altitude is the angle complementing the
    solar zenith angle.  Therefore, it is the angle
    between the horizontal and the line to the sun.

    :param solar_zenith_degrees: A float value representing the
        sun's current zenith angle,
        which must be between 0 and 90 degrees.

    :returns: A float value representing the solar altitude angle in degrees.
    """

    # Validate `solar_zenith_degrees`
    validate_numeric_value(value=solar_zenith_degrees, minimum=0, maximum=90)

    try:
        return 90.0 - solar_zenith_degrees
    except TypeError:
        return 90.0 - np.array(solar_zenith_degrees)
Ejemplo n.º 6
0
def calculate_E_min(
    B_degrees: Union[int, float, Iterable[Union[int, float]]]
) -> Union[float, Iterable[float]]:
    """
    E is the equation of time (in minutes), which is
    based on the day of the year.
    The equation is given by Spencer (1971).

    The equation used is from Duffie & Beckman (2006)
    Equation 1.5.3.

    :param B_degrees: A numeric value (generally a float)
        which is calculated based on the day of the year,
        in units of degrees.

    :returns: A float value representing the equation of
        time for the given `B_degrees`, in units of minutes.
    """

    # Type-check `B_degrees`
    ensure_numeric(
        B_degrees,
        valid_types=[int, float, np.number],
        nan_acceptable=False,
        inf_acceptable=False,
    )
    # Range-check `B_degrees`
    validate_numeric_value(
        B_degrees,
        minimum=calculate_B_degrees(1),
        maximum=calculate_B_degrees(366),
    )
    # Convert `B_degrees` to radians for use in the calculation
    B_radians = np.radians(B_degrees)
    return 229.2 * (0.000075 + (0.001868 * np.cos(B_radians)) -
                    (0.032077 * np.sin(B_radians)) -
                    (0.014615 * np.cos(2 * B_radians)) -
                    (0.04089 * np.sin(2 * B_radians)))
Ejemplo n.º 7
0
def calculate_air_mass(
    solar_zenith_degrees: Union[int, float, Iterable[Union[int, float]]],
    site_altitude_m: Union[int, float] = 0,
) -> Union[float, Iterable[float]]:
    """
    Air mass is the ratio of the mass of atmosphere through which
    beam radiation passes to the mass it would pass through if
    the sun were at the zenith (i.e., directly overhead).

    The equation used is from a footnote provided in
    Duffie & Beckman (2006) providing an empirical relationship
    from Kasten and Young (1989).

    :param solar_zenith_degrees: A numeric value representing the
        sun's current zenith angle,
        which must be between 0 and 90 degrees.
    :param site_altitude_m: A numeric value representing the
        altitude above sea level (0 m, the default),
        which must be at least -413 m
        (the lowest land elevation, on the shore of the Dead Sea).

    :returns: A float value representing the air mass.
    """

    # Validate `solar_zenith_degrees` and `site_altitude_m`
    validate_numeric_value(value=solar_zenith_degrees, minimum=0, maximum=90)
    validate_numeric_value(value=site_altitude_m, minimum=-413, maximum=None)

    try:
        return np.exp(-0.0001184 * site_altitude_m) / (
            np.cos(np.radians(solar_zenith_degrees)) +
            (0.5057 * (96.080 - solar_zenith_degrees)**-1.634))
    except TypeError:
        return np.exp(-0.0001184 * site_altitude_m) / (
            np.cos(np.radians(np.array(solar_zenith_degrees))) +
            (0.5057 * (96.080 - np.array(solar_zenith_degrees))**-1.634))
Ejemplo n.º 8
0
def calculate_B_degrees(
        day_number: Union[int,
                          Iterable[int]]) -> Union[float, Iterable[float]]:
    """
    B is a preliminary value used in calculating the extraterrestrial
    radiation incident on the plane normal to the radiation on the
    `day_number` day of the year (G_on),
    per an equation given by Spencer (1971).

    The equation used is from Duffie & Beckman (2006)
    Equation 1.4.2.

    :param day_number: An integer representing the day number
        (of the year) of a specific date.  For example,
        January 1 corresponds to day number 1, and
        December 31 corresponds to day number 365
        (or 366, if a leap year).

    :returns: A float value, in units of degrees.
    """

    # Ensure `day_number` is an integer
    ensure_numeric(
        day_number,
        valid_types=[int, np.number],
        nan_acceptable=False,
        inf_acceptable=False,
    )
    # Ensure `day_number` is in the proper range
    validate_numeric_value(day_number, minimum=1, maximum=366)

    try:
        return (day_number - 1) * 360.0 / 365.0
    except TypeError:
        # When `day_number` is an iterable, but not a numpy array
        return (np.array(day_number) - 1) * 360.0 / 365
Ejemplo n.º 9
0
def calculate_solar_zenith_degrees(
    latitude_degrees: Union[int, float],
    declination_degrees: Union[int, float, Iterable[Union[int, float]]],
    hour_angle_degrees: Union[int, float, Iterable[Union[int, float]]],
) -> Union[float, Iterable[float]]:
    """
    The solar zenith angle is the angle between
    the vertical and the line to the sun, that is,
    the angle of incidence of beam radiation
    on a horizontal surface.  The solar zenith
    angle must be between 0 and 90 degrees, because
    anything greater than 90 degrees indicates
    the sun is below the horizon.

    The equation used is from Duffie & Beckman (2006)
    Equation 1.6.5.

    :param latitude_degrees: A numeric value representing a location's
        position north (positive) or south (negative) of the equator,
        which must be between -90 and 90 degrees.
    :param declination_degrees: A numeric value representing
        the declination angle of the sun,
        which must be between -23.45 and 23.45 degrees.
    :param hour_angle_degrees: A numeric value corresponding
        to the angular displacement of the sun east (negative)
        or west (positive) of the local meridian due to rotation
        of the earth on its axis at 15 degrees per hour,
        which must be between -180 and 180 degrees.

    :returns: A float value representing the solar zenith angle in degrees.
    """

    # Validate arguments
    validate_numeric_value(value=latitude_degrees, minimum=-90, maximum=90)
    validate_numeric_value(value=declination_degrees,
                           minimum=-23.45,
                           maximum=23.45)
    validate_numeric_value(value=hour_angle_degrees, minimum=-180, maximum=180)

    calculated_zenith = np.degrees(
        np.arccos((np.cos(np.radians(latitude_degrees)) *
                   np.cos(np.radians(declination_degrees)) *
                   np.cos(np.radians(hour_angle_degrees))) +
                  (np.sin(np.radians(latitude_degrees)) *
                   np.sin(np.radians(declination_degrees)))))

    return np.minimum(calculated_zenith, 90.0)
Ejemplo n.º 10
0
def calculate_solar_noon_in_local_standard_time(
    local_standard_time: Union[datetime, str, Iterable[Union[datetime, str]]],
    longitude_degrees: Union[int, float],
) -> Union[datetime, Iterable[datetime]]:
    """
    Method to calculate solar noon given a local standard timestamp
    (including date and time zone offset from UTC) and a location's
    longitude (in degrees west).

    The equation used is from Duffie & Beckman (2006)
    Equation 1.5.2.

    :param local_standard_time: A `datetime` object,
        containing a timezone offset, representing the time that
        will be converted to solar time.
    :param longitude_degrees: A numeric value representing a location's
        angular distance west of the meridian at Greenwich, England.
        `longitude_degrees` should be between 0 and 360 degrees.

    :returns: A datetime object representing the local standard
        time that corresponds to solar noon for the given
        date in `local_standard_time` and `longitude_degrees`.
    """

    # Type- and range-check `longitude_degrees`
    validate_numeric_value(longitude_degrees, minimum=0, maximum=360)
    # Validate `local_standard_time`
    local_ts = validate_datetime(datetime_object=local_standard_time)

    if isinstance(local_ts, pd.Series):
        # Ensure local_ts has time zone information
        if local_ts.dt.tz is None:
            raise ValueError(
                """`local_standard_time` must provide a time zone offset,
            such as `1/1/2019 12:00 PM -06:00`.""")

        # Calculate offset from UTC, using timezone offset in `local_ts`
        utc_offset = (local_ts.dt.tz.utcoffset(local_ts).total_seconds() //
                      3_600)
    else:
        # Ensure local_ts has time zone information
        if local_ts.tzinfo is None:
            raise ValueError(
                """`local_standard_time` must provide a time zone offset,
            such as `1/1/2019 12:00 PM -06:00`.""")

        # Calculate offset from UTC, using timezone offset in `local_ts`
        utc_offset = (local_ts.tzinfo.utcoffset(local_ts).total_seconds() //
                      3_600)
    """Determine the standard meridian for the given `longitude_degrees`,
    which corresponds to 15 degrees per hour offset."""
    standard_meridian = 15 * abs(utc_offset)

    E = calculate_E_min(
        calculate_B_degrees(calculate_day_number(local_standard_time)))
    longitude_correction_mins = 4.0 * (standard_meridian - longitude_degrees)

    # Create a datetime object for noon on the same date as `solar_ts`
    try:
        solar_noon = np.array([
            datetime(
                year=x.date().year,
                month=x.date().month,
                day=x.date().day,
                hour=12,
                minute=0,
                second=0,
                tzinfo=x.tzinfo,
            ) for x in local_ts
        ])
    except TypeError:
        solar_noon = datetime(
            year=local_ts.date().year,
            month=local_ts.date().month,
            day=local_ts.date().day,
            hour=12,
            minute=0,
            second=0,
            tzinfo=local_ts.tzinfo,
        )

    if isinstance(solar_noon, np.ndarray):
        result = np.array([
            x - timedelta(minutes=E[i] + longitude_correction_mins)
            for i, x in enumerate(solar_noon)
        ])
    else:
        result = solar_noon - timedelta(minutes=E + longitude_correction_mins)

    return result
Ejemplo n.º 11
0
def calculate_solar_azimuth_degrees(
    hour_angle_degrees: Union[int, float, Iterable[Union[int, float]]],
    latitude_degrees: Union[int, float],
    declination_degrees: Union[int, float, Iterable[Union[int, float]]],
) -> Union[float, Iterable[float]]:
    """
    The solar azimuth angle is the angular displacement from south
    of the projection of beam radiation on the horizontal plane.
    Displacements east of south are negative, and
    displacements west of south are positive.

    The equation used is from Duffie & Beckman (2006)
    Equation 1.6.6.

    :param hour_angle_degrees: A numeric value corresponding
        to the angular displacement of the sun east (negative)
        or west (positive) of the local meridian due to rotation
        of the earth on its axis at 15 degrees per hour,
        which must be between -180 and 180 degrees.
    :param latitude_degrees: A numeric value representing a location's
        position north (positive) or south (negative) of the equator,
        which must be between -90 and 90 degrees.
    :param declination_degrees: A numeric value representing
        the declination angle of the sun,
        which must be between -23.45 and 23.45 degrees.

    :returns: A float value representing the solar azimuth angle.
    """

    # Validate arguments
    validate_numeric_value(value=hour_angle_degrees, minimum=-180, maximum=180)
    validate_numeric_value(value=latitude_degrees, minimum=-90, maximum=90)
    validate_numeric_value(value=declination_degrees,
                           minimum=-23.45,
                           maximum=23.45)

    # Calculate solar zenith angle
    solar_zenith_degrees = calculate_solar_zenith_degrees(
        latitude_degrees=latitude_degrees,
        declination_degrees=declination_degrees,
        hour_angle_degrees=hour_angle_degrees,
    )

    # copysign(x, y) returns `x` with the sign of `y`
    pre = np.copysign(1, hour_angle_degrees) * np.abs(
        np.degrees(
            np.arccos(((np.cos(np.radians(solar_zenith_degrees)) *
                        np.sin(np.radians(latitude_degrees))) -
                       np.sin(np.radians(declination_degrees))) /
                      (np.sin(np.radians(solar_zenith_degrees)) *
                       np.cos(np.radians(latitude_degrees))))))
    """Zero division can occur when the sun
        is directly overhead, which is possible:
        on the equator, on an equinox, at solar noon.
        In this case, just return 0."""
    if isinstance(pre, (np.ndarray, pd.Series)):
        pre[~np.isfinite(pre)] = 0.0
    else:
        if not (np.isfinite(pre)):
            pre = 0.0
    return pre
Ejemplo n.º 12
0
def convert_to_solar_time(
    local_standard_time: Union[datetime, str, Iterable[Union[datetime, str]]],
    longitude_degrees: Union[int, float],
) -> Union[datetime, Iterable[datetime]]:
    """
    Method to calculate solar time given a local standard timestamp
    (including date and time zone offset from UTC) and a location's
    longitude (in degrees west).

    The equation used is from Duffie & Beckman (2006)
    Equation 1.5.2.

    :param local_standard_time: A `datetime` object,
        containing a timezone offset, representing the time that
        will be converted to solar time.
    :param longitude_degrees: A numeric value representing a location's
        angular distance west of the meridian at Greenwich, England.
        `longitude_degrees` should be between 0 and 360 degrees.

    :returns: A datetime object representing the solar time
        corresponding to `local_standard_time` at the given
        `longitude_degrees`.
    """

    # Type- and range-check `longitude_degrees`
    validate_numeric_value(longitude_degrees, minimum=0, maximum=360)
    # Validate `local_standard_time`
    local_ts = validate_datetime(datetime_object=local_standard_time)

    if isinstance(local_ts, pd.Series):
        # Ensure local_ts has time zone information
        if local_ts.dt.tz is None:
            raise ValueError(
                """`local_standard_time` must provide a time zone offset,
            such as `1/1/2019 12:00 PM -06:00`.""")

        # Calculate offset from UTC, using timezone offset in `local_ts`
        utc_offset = (local_ts.dt.tz.utcoffset(local_ts).total_seconds() //
                      3_600)
    else:
        # Ensure local_ts has time zone information
        if local_ts.tzinfo is None:
            raise ValueError(
                """`local_standard_time` must provide a time zone offset,
            such as `1/1/2019 12:00 PM -06:00`.""")

        # Calculate offset from UTC, using timezone offset in `local_ts`
        utc_offset = (local_ts.tzinfo.utcoffset(local_ts).total_seconds() //
                      3_600)
    """Determine the standard meridian for the given `longitude_degrees`,
    which corresponds to 15 degrees per hour offset."""
    standard_meridian = 15 * np.abs(utc_offset)

    E = calculate_E_min(
        calculate_B_degrees(calculate_day_number(local_standard_time)))
    longitude_correction_mins = 4.0 * (standard_meridian - longitude_degrees)

    try:
        return local_ts + timedelta(minutes=longitude_correction_mins + E)
    except TypeError:
        # When working with iterables
        return [
            x + timedelta(minutes=longitude_correction_mins + E[i])
            for i, x in enumerate(local_ts)
        ]
Ejemplo n.º 13
0
        B_degrees,
        valid_types=[int, float, np.number],
        nan_acceptable=False,
        inf_acceptable=False,
    )
    ensure_numeric(
        G_sc,
        valid_types=[int, float, np.number],
        nan_acceptable=False,
        inf_acceptable=False,
    )

    # Range-check `B_degrees` and `G_sc`
    validate_numeric_value(
        B_degrees,
        minimum=calculate_B_degrees(1),
        maximum=calculate_B_degrees(366),
    )
    validate_numeric_value(G_sc, minimum=0, maximum=None)

    # Convert `B_degrees` to radians for use in the calculation
    B_radians = np.radians(B_degrees)

    # Calculate the multiplier for `G_sc`
    multiplier = (1.000110 + (0.034221 * np.cos(B_radians)) +
                  (0.001280 * np.sin(B_radians)) +
                  (0.000719 * np.cos(2 * B_radians)) +
                  (0.000077 * np.sin(2 * B_radians)))
    return G_sc * multiplier

Ejemplo n.º 14
0
def test_no_requirements():
    """Test to ensure that no errors are thrown when a
    numeric value is provided with no `minimum` or
    `maximum`."""
    validate_numeric_value(10, minimum=None, maximum=None)
Ejemplo n.º 15
0
def test_invalid_type():
    """Test to ensure a TypeError is thrown when a
    non-numeric value is provided."""
    with pytest.raises(TypeError):
        assert validate_numeric_value("blah", minimum=0, maximum=10)
Ejemplo n.º 16
0
def test_validate_numeric_value(value):
    """Functional test to ensure validate_numeric_value()
    runs properly for range-valid values."""
    validate_numeric_value(value, minimum=-1e6, maximum=1e6)