Skip to content

This section documents the API.

Solar position

fractional_year

Functions:

Name Description
model_fractional_year

Model fractional year

model_fractional_year

model_fractional_year()

Model fractional year

Source code in pvgis/api/position/fractional_year.py
def model_fractional_year():
    """Model fractional year"""
    pass

solar_time

Functions:

Name Description
calculate_solar_time_series

Calculates the solar time using all models and returns the results in a table.

model_solar_time_series

Calculates the solar time using the requested algorithm.

calculate_solar_time_series

calculate_solar_time_series(
    longitude: Longitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_time_models: List[SolarTimeModel] = [noaa],
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = VERBOSE_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> Dict

Calculates the solar time using all models and returns the results in a table.

Parameters:

Name Type Description Default
longitude Longitude

Observer longitude.

required
timestamps DatetimeIndex

Timestamps for the evaluation grid.

required
timezone ZoneInfo

Local timezone for solar time conversions.

required
solar_time_models list of SolarTimeModel

Models to evaluate (default: NOAA only).

[noaa]
dtype str

Array dtype.

DATA_TYPE_DEFAULT
array_backend str

Array backend identifier.

ARRAY_BACKEND_DEFAULT
verbose int

Verbosity level.

VERBOSE_LEVEL_DEFAULT
log int

Logging level.

VERBOSE_LEVEL_DEFAULT
validate_output bool

Whether to validate model outputs.

VALIDATE_OUTPUT_DEFAULT

Returns:

Type Description
dict

Nested overview keyed by model name with timing algorithm, solar time series, and unit metadata.

Source code in pvgis/api/position/solar_time.py
def calculate_solar_time_series(
    longitude: Longitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_time_models: List[SolarTimeModel] = [SolarTimeModel.noaa],
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = VERBOSE_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> Dict:
    """Calculates the solar time using all models and returns the results in a table.

    Parameters
    ----------
    longitude : Longitude
        Observer longitude.
    timestamps : DatetimeIndex
        Timestamps for the evaluation grid.
    timezone : ZoneInfo
        Local timezone for solar time conversions.
    solar_time_models : list of SolarTimeModel, optional
        Models to evaluate (default: NOAA only).
    dtype : str, optional
        Array dtype.
    array_backend : str, optional
        Array backend identifier.
    verbose : int, optional
        Verbosity level.
    log : int, optional
        Logging level.
    validate_output : bool, optional
        Whether to validate model outputs.

    Returns
    -------
    dict
        Nested overview keyed by model name with timing algorithm, solar time
        series, and unit metadata.
    """
    results = {}
    # solar_time_models = select_models(SolarTimeModel, solar_time_model)  # Using a callback fails!
    for solar_time_model in solar_time_models:
        if (
            solar_time_model != SolarTimeModel.all
        ):  # ignore 'all' in the enumeration
            solar_time_series = model_solar_time_series(
                longitude=longitude,
                timestamps=timestamps,
                timezone=timezone,
                solar_time_model=solar_time_model,
                dtype=dtype,
                array_backend=array_backend,
                verbose=verbose,
                log=log,
                validate_output=validate_output,
            )
            solar_time_model_overview = {
                solar_time_model.name: {
                    TIME_ALGORITHM_NAME: (
                        solar_time_model.value if solar_time_model else NOT_AVAILABLE
                    ),
                    SOLAR_TIME_NAME: (
                        solar_time_series if solar_time_series else NOT_AVAILABLE
                    ),
                    UNITS_COLUMN_NAME: MINUTES,
                }
            }
            results = results | solar_time_model_overview

    return results

model_solar_time_series

model_solar_time_series(
    longitude: Longitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_time_model: SolarTimeModel = noaa,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
)

Calculates the solar time using the requested algorithm.

Parameters:

Name Type Description Default
longitude Longitude

Observer longitude.

required
timestamps DatetimeIndex

UTC (or aligned) timestamps for the series.

required
timezone ZoneInfo

Local timezone for solar time conversions.

required
solar_time_model SolarTimeModel

Algorithm to use (default: NOAA).

noaa
dtype str

Array dtype.

DATA_TYPE_DEFAULT
array_backend str

Array backend identifier.

ARRAY_BACKEND_DEFAULT
verbose int

Verbosity level.

VERBOSE_LEVEL_DEFAULT
log int

Logging level.

LOG_LEVEL_DEFAULT
validate_output bool

Whether to validate model outputs.

VALIDATE_OUTPUT_DEFAULT

Returns:

Type Description
object

Time series object produced by the selected solar_time_model (minutes-based).

Source code in pvgis/api/position/solar_time.py
@log_function_call
@validate_with_pydantic(ModelSolarTimeTimeSeriesInputModel)
def model_solar_time_series(
    longitude: Longitude,
    # latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_time_model: SolarTimeModel = SolarTimeModel.noaa,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
):
    """Calculates the solar time using the requested _algorithm_.

    Parameters
    ----------
    longitude : Longitude
        Observer longitude.
    timestamps : DatetimeIndex
        UTC (or aligned) timestamps for the series.
    timezone : ZoneInfo
        Local timezone for solar time conversions.
    solar_time_model : SolarTimeModel, optional
        Algorithm to use (default: NOAA).
    dtype : str, optional
        Array dtype.
    array_backend : str, optional
        Array backend identifier.
    verbose : int, optional
        Verbosity level.
    log : int, optional
        Logging level.
    validate_output : bool, optional
        Whether to validate model outputs.

    Returns
    -------
    object
        Time series object produced by the selected ``solar_time_model`` (minutes-based).
    """
    solar_time_series = None

    if solar_time_model.value == SolarTimeModel.milne:
        pass

        solar_time_series = calculate_apparent_solar_time_series_milne1921(
            longitude=longitude,
            timestamps=timestamps,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )

    if solar_time_model.value == SolarTimeModel.pvgis:
        # Requires : time_offset_global, hour_offset
        pass

    if solar_time_model.value == SolarTimeModel.noaa:

        solar_time_series = calculate_true_solar_time_series_noaa(
            longitude=longitude,
            timestamps=timestamps,
            timezone=timezone,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )

    if solar_time_model.value == SolarTimeModel.skyfield:
        pass

        # # vvv vvv vvv --------------------------------------- expects degrees!
        # solar_time = calculate_solar_time_skyfield(
        #     longitude=longitude,
        #     latitude=latitude,
        #     timestamp=timestamp,
        #     timezone=timezone,
        #     verbose=verbose,
        # )
        # # ^^^ ^^^ ^^^ --------------------------------------- expects degrees!
    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    return solar_time_series

hour_angle

Functions:

Name Description
calculate_event_hour_angle_series

Calculate the hour angle (ω) at sunrise and sunset

calculate_solar_hour_angle_series

Calculate the hour angle ω'

model_solar_hour_angle_series

calculate_event_hour_angle_series

calculate_event_hour_angle_series(
    latitude: Latitude,
    surface_tilt: float = 0,
    solar_declination: float = 0,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> HourAngleSunrise

Calculate the hour angle (ω) at sunrise and sunset

Parameters:

Name Type Description Default
latitude Latitude

Latitude (Φ) is the angle between the sun's rays and its projection on the horizontal surface measured in radians

required
surface_tilt float

Surface tilt (or slope) (β) is the angle between the inclined surface (slope) and the horizontal plane.

0
solar_declination float

Solar declination (δ) is the angle between the equator and a line drawn from the centre of the Earth to the centre of the sun measured in radians.

0

Returns:

Type Description
Tuple(float, str)

Tuple containg (hour_angle, units). Hour angle (ω) is the angle at any instant through which the earth has to turn to bring the meridian of the observer directly in line with the sun's rays measured in radian.

Notes

Hour angle = acos( -tan * ( Latitude Angle - Tilt Angle ) * tan( Declination Angle ) )

The hour angle (ω) at sunrise and sunset measures the angular distance between the sun at the local solar time and the sun at solar noon.

ω = acos(-tan(Φ-β)*tan(δ))

Source code in pvgis/api/position/hour_angle.py
@validate_with_pydantic(CalculateEventHourAngleInputModel)
def calculate_event_hour_angle_series(
    latitude: Latitude,
    surface_tilt: float = 0,
    solar_declination: float = 0,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> HourAngleSunrise:
    """Calculate the hour angle (ω) at sunrise and sunset

    Parameters
    ----------

    latitude: float
        Latitude (Φ) is the angle between the sun's rays and its projection on the
        horizontal surface measured in radians

    surface_tilt: float
        Surface tilt (or slope) (β) is the angle between the inclined surface
        (slope) and the horizontal plane.

    solar_declination: float
        Solar declination (δ) is the angle between the equator and a line drawn
        from the centre of the Earth to the centre of the sun measured in
        radians.

    Returns
    -------

    Tuple(float, str)
        Tuple containg (hour_angle, units). Hour angle (ω) is the angle at any
        instant through which the earth has to turn to bring the meridian of the
        observer directly in line with the sun's rays measured in radian.

    Notes
    -----
    Hour angle = acos( -tan * ( Latitude Angle - Tilt Angle ) * tan( Declination Angle ) )

    The hour angle (ω) at sunrise and sunset measures the angular distance
    between the sun at the local solar time and the sun at solar noon.

    ω = acos(-tan(Φ-β)*tan(δ))

    """
    hour_angle_sunrise = acos(
        -tan(latitude.radians - surface_tilt.radians) * tan(solar_declination.radians)
    )

    return HourAngleSunrise(
        value=hour_angle_sunrise,
        unit=RADIANS,
    )

calculate_solar_hour_angle_series

calculate_solar_hour_angle_series(
    longitude: Longitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_models: List[SolarPositionModel] = [
        noaa
    ],
    solar_time_model: SolarTimeModel = noaa,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> SolarHourAngle

Calculate the hour angle ω'

ω = (ST / 3600 - 12) * 15 * pi / 180

Parameters:

Name Type Description Default
longitude Longitude

Observer longitude.

required
timestamps DatetimeIndex

Timestamps for the evaluation grid.

required
timezone ZoneInfo

Local timezone for solar time conversions.

required
solar_position_models list of SolarPositionModel

Solar geometry models to include (default: NOAA).

[noaa]
solar_time_model SolarTimeModel

Solar time algorithm feeding hour-angle calculations.

noaa
angle_output_units str

Units used when extracting hour-angle values from internal series.

RADIANS
dtype str

Array dtype.

DATA_TYPE_DEFAULT
array_backend str

Array backend identifier.

ARRAY_BACKEND_DEFAULT
verbose int

Verbosity level.

VERBOSE_LEVEL_DEFAULT
log int

Logging level.

LOG_LEVEL_DEFAULT
validate_output bool

Whether to validate model outputs.

VALIDATE_OUTPUT_DEFAULT
fingerprint bool

If true, add content hashes to the returned overview table.

FINGERPRINT_FLAG_DEFAULT

Returns:

Type Description
dict

Overview keyed by solar_position_model.name with algorithm metadata, timing model, hour-angle values, units, and optional fingerprints.

Notes

The hour angle ω (elsewhere symbolised with h) of a point on the earth’s surface is defined as the angle through which the earth would turn to bring the meridian of the point directly under the sun. The hour angle at local solar noon is zero, with each 360/24 or 15° of longitude equivalent to 1 h, afternoon hours being designated as positive. Expressed symbolically, the hour angle in degrees is:

h = ±0.25 (Number of minutes from local solar noon)

where the plus sign applies to afternoon hours and the minus sign to morning hours.

The hour angle can also be obtained from the apparent solar time (AST); that is, the corrected local solar time:

h = (AST - 12) * 15

At local solar noon, AST = 12 and h = 0°. Therefore, from Eq <<(2.3)<<, the local solar time (LST, the time shown by our clocks at local solar noon) is:

LST = 12 - ET ∓ 4 * (SL - LL)

where:

    ET is the Equation of Time
    SL Standard Longitude
    LL Local Longitude

Example 1

The equation for LST at local solar noon for Nicosia, Cyprus is:

LST = 12 - ET - 13.32 (minutes)

Example 2

Given the ET for March 10 (N = 69) is calculated from Eq (2.1), in which the factor B is obtained from Eq <<(2.2)<< as:

B = 360 / 364 * (N-81) = 360 / 364 * (69- 81) = -11.87

ET = 9.87 * sin(2*B) - 7.53 * cos(B) - 1.5 * sin(B) =
   = 9.87 * sin(-2 * 11.87) - 7.53 * cos(-11.87) - 1.5 * sin(-11.87)
   = -11.04min ∼ -11min

The standard meridian for Athens is 30°E longitude.

The apparent solar time on March 10 at 2:30 pm for the city of Athens, Greece (23°40′E longitude) is

AST = 14:30 - 4 * (30 - 23.66) - 0:11
    = 14:30 - 0:25 - 0:11
    = 13:54 or 1:54 pm

Additional notes:

Nomenclature from [1]_

α [°] solar altitude angle β [°] tilt angle δ [°] solar declination θ [°] solar incidence angle Φ [°] solar zenith angle h [°] hour angle L [°] local latitude N [-] day of the year z [°] solar azimuth angle ZS [°] surface azimuth angle AST Apparent Solar Time LST Local Standard Time ET Equation of Time SL Standard Longitude LL Local Longitude DS Daylight Saving

.. [1] Determination of Optimal Position of Solar Trough Collector. Available from: https://www.researchgate.net/publication/317826540_Determination_of_Optimal_Position_of_Solar_Trough_Collector [accessed Sep 06 2023].

In PVGIS : hour_angle = (solar_time / 3600 - 12) * 15 * 0.0175

which means:
- solar time is expected in seconds
- conversion to radians `* 0.0175` replaced by `pi / 180`

In this function:

Source code in pvgis/api/position/hour_angle.py
def calculate_solar_hour_angle_series(
    longitude: Longitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_models: List[SolarPositionModel] = [SolarPositionModel.noaa],
    solar_time_model: SolarTimeModel = SolarTimeModel.noaa,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> SolarHourAngle:
    """Calculate the hour angle ω'

    ω = (ST / 3600 - 12) * 15 * pi / 180

    Parameters
    ----------
    longitude : Longitude
        Observer longitude.
    timestamps : DatetimeIndex
        Timestamps for the evaluation grid.
    timezone : ZoneInfo
        Local timezone for solar time conversions.
    solar_position_models : list of SolarPositionModel, optional
        Solar geometry models to include (default: NOAA).
    solar_time_model : SolarTimeModel, optional
        Solar time algorithm feeding hour-angle calculations.
    angle_output_units : str, optional
        Units used when extracting hour-angle values from internal series.
    dtype : str, optional
        Array dtype.
    array_backend : str, optional
        Array backend identifier.
    verbose : int, optional
        Verbosity level.
    log : int, optional
        Logging level.
    validate_output : bool, optional
        Whether to validate model outputs.
    fingerprint : bool, optional
        If true, add content hashes to the returned overview table.

    Returns
    -------
    dict
        Overview keyed by ``solar_position_model.name`` with algorithm metadata,
        timing model, hour-angle values, units, and optional fingerprints.

    Notes
    -----

    The hour angle ω (elsewhere symbolised with `h`) of a point on the earth’s
    surface is defined as the angle through which the earth would turn to bring
    the meridian of the point directly under the sun. The hour angle at local
    solar noon is zero, with each 360/24 or 15° of longitude equivalent to 1 h,
    afternoon hours being designated as positive. Expressed symbolically, the
    hour angle in degrees is:

        h = ±0.25 (Number of minutes from local solar noon)

    where the plus sign applies to afternoon hours and the minus sign to
    morning hours.

    The hour angle can also be obtained from the apparent solar time (AST);
    that is, the corrected local solar time:

        h = (AST - 12) * 15

    At local solar noon, AST = 12 and h = 0°. Therefore, from Eq <<(2.3)<<, the
    local solar time (LST, the time shown by our clocks at local solar noon)
    is:

        LST = 12 - ET ∓ 4 * (SL - LL)

        where:

            ET is the Equation of Time
            SL Standard Longitude
            LL Local Longitude

    Example 1

    The equation for LST at local solar noon for Nicosia, Cyprus is:

        LST = 12 - ET - 13.32 (minutes)

    Example 2

    Given the ET for March 10 (N = 69) is calculated from Eq (2.1), in which
    the factor B is obtained from Eq <<(2.2)<< as:

        B = 360 / 364 * (N-81) = 360 / 364 * (69- 81) = -11.87

        ET = 9.87 * sin(2*B) - 7.53 * cos(B) - 1.5 * sin(B) =
           = 9.87 * sin(-2 * 11.87) - 7.53 * cos(-11.87) - 1.5 * sin(-11.87)
           = -11.04min ∼ -11min

    The standard meridian for Athens is 30°E longitude.

    The apparent solar time on March 10 at 2:30 pm for the city of Athens,
    Greece (23°40′E longitude) is

        AST = 14:30 - 4 * (30 - 23.66) - 0:11
            = 14:30 - 0:25 - 0:11
            = 13:54 or 1:54 pm

    Additional notes:

    Nomenclature from [1]_

    α [°] solar altitude angle
    β [°] tilt angle
    δ [°] solar declination
    θ [°] solar incidence angle
    Φ [°] solar zenith angle
    h [°] hour angle
    L [°] local latitude
    N [-] day of the year
    z [°] solar azimuth angle
    ZS [°] surface azimuth angle
    AST Apparent Solar Time
    LST Local Standard Time
    ET Equation of Time
    SL Standard Longitude
    LL Local Longitude
    DS Daylight Saving

    .. [1] Determination of Optimal Position of Solar Trough Collector. Available from: https://www.researchgate.net/publication/317826540_Determination_of_Optimal_Position_of_Solar_Trough_Collector [accessed Sep 06 2023].

    In PVGIS :
        hour_angle = (solar_time / 3600 - 12) * 15 * 0.0175

        which means:
        - solar time is expected in seconds
        - conversion to radians `* 0.0175` replaced by `pi / 180`

    In this function:
    """
    results = {}
    for solar_position_model in solar_position_models:
        # for the time being! ------------------------------------------------
        if solar_position_model != SolarPositionModel.noaa:
            logger.warning(
                f"Solar geometry overview series is not implemented for the requested solar position model: {solar_position_model}!"
            )
        # --------------------------------------------------------------------
        if (
            solar_position_model != SolarPositionModel.all
        ):  # ignore 'all' in the enumeration
            solar_hour_angle_series = model_solar_hour_angle_series(
                longitude=longitude,
                # latitude=latitude,
                timestamps=timestamps,
                timezone=timezone,
                solar_time_model=solar_time_model,
                solar_position_model=solar_position_model,
                dtype=dtype,
                array_backend=array_backend,
                validate_output=validate_output,
                verbose=verbose,
                log=log,
            )
            hour_angle_values = (
                getattr(solar_hour_angle_series, angle_output_units, NOT_AVAILABLE)
                if solar_hour_angle_series
                else NOT_AVAILABLE
            )
            model_entry = {
                POSITION_ALGORITHM_NAME: solar_position_model.value,
                TIME_ALGORITHM_NAME: (
                    solar_hour_angle_series.timing_algorithm
                    if solar_hour_angle_series
                    else NOT_AVAILABLE
                ),
                HOUR_ANGLE_NAME: hour_angle_values,
                UNITS_COLUMN_NAME: angle_output_units,
            }
            if fingerprint and solar_hour_angle_series is not None:
                model_entry[FINGERPRINT_COLUMN_NAME] = generate_hash(hour_angle_values)
            results = results | {solar_position_model.name: model_entry}

    return results

model_solar_hour_angle_series

model_solar_hour_angle_series(
    longitude: Longitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_model: SolarPositionModel = noaa,
    solar_time_model: SolarTimeModel = milne,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarHourAngle
Source code in pvgis/api/position/hour_angle.py
@log_function_call
@custom_cached
# @validate_with_pydantic(CalculateSolarHourAngleTimeSeriesNOAAInput)
def model_solar_hour_angle_series(
    longitude: Longitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_model: SolarPositionModel = SolarPositionModel.noaa,
    solar_time_model: SolarTimeModel = SolarTimeModel.milne,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarHourAngle:
    """ """
    solar_hour_angle_series = None

    if solar_position_model.value == SolarPositionModel.noaa:
        solar_hour_angle_series = calculate_solar_hour_angle_series_noaa(
            longitude=longitude,
            timestamps=timestamps,
            timezone=timezone,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )

    if solar_position_model.value == SolarPositionModel.skyfield:
        pass

    if solar_position_model.value == SolarPositionModel.jenco:
        pass

    if solar_position_model.value == SolarPositionModel.hofierka:
        solar_hour_angle_series = calculate_solar_hour_angle_series_hofierka(
            longitude=longitude,
            timestamps=timestamps,
            timezone=timezone,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_position_model.value == SolarPositionModel.pvlib:
        solar_hour_angle_series = calculate_solar_hour_angle_series_pvlib(
            longitude=longitude,
            timestamps=timestamps,
            # timezone=timezone,
            # dtype=dtype,
            # array_backend=array_backend,
            # verbose=verbose,
            # log=log,
        )

    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    return solar_hour_angle_series

declination

Functions:

Name Description
calculate_solar_declination_series

Calculate the solar declination angle

model_solar_declination_series

calculate_solar_declination_series

calculate_solar_declination_series(
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_declination_models: List[
        SolarDeclinationModel
    ] = [pvgis],
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> Dict

Calculate the solar declination angle

The solar declination is the angle between the rays of the sun and the equator of the earth. It is used to calculate the solar elevation and azimuth angles.

Source code in pvgis/api/position/declination.py
def calculate_solar_declination_series(
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_declination_models: List[SolarDeclinationModel] = [
        SolarDeclinationModel.pvgis
    ],
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> Dict:
    """Calculate the solar declination angle

    The solar declination is the angle between the rays of the sun and the
    equator of the earth. It is used to calculate the solar elevation and
    azimuth angles.
    """
    results = {}
    for solar_declination_model in solar_declination_models:
        if (
            solar_declination_model != SolarDeclinationModel.all
        ):  # ignore 'all' in the enumeration
            solar_declination_series = model_solar_declination_series(
                timestamps=timestamps,
                timezone=timezone,
                solar_declination_model=solar_declination_model,
                perigee_offset=perigee_offset,
                eccentricity_correction_factor=eccentricity_correction_factor,
                array_backend=array_backend,
                dtype=dtype,
                verbose=verbose,
                log=log,
                validate_output=validate_output,
            )
            declination_values = getattr(
                solar_declination_series, angle_output_units, NOT_AVAILABLE
            )
            model_entry = {
                POSITION_ALGORITHM_NAME: solar_declination_model.value,
                DECLINATION_NAME: declination_values,
                UNITS_COLUMN_NAME: angle_output_units,
            }
            if fingerprint:
                model_entry[FINGERPRINT_COLUMN_NAME] = generate_hash(
                    declination_values
                )
            results = results | {solar_declination_model.name: model_entry}

    return results

model_solar_declination_series

model_solar_declination_series(
    timestamps: DatetimeIndex,
    timezone: ZoneInfo = ZoneInfo("UTC"),
    solar_declination_model: SolarDeclinationModel = pvgis,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = VERBOSE_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarDeclination
Source code in pvgis/api/position/declination.py
@log_function_call
@custom_cached
def model_solar_declination_series(
    timestamps: DatetimeIndex,
    timezone: ZoneInfo = ZoneInfo("UTC"),
    solar_declination_model: SolarDeclinationModel = SolarDeclinationModel.pvgis,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = VERBOSE_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarDeclination:
    """ """
    solar_declination_series = None

    if solar_declination_model.value == SolarDeclinationModel.noaa:
        solar_declination_series = calculate_solar_declination_series_noaa(
            timestamps=timestamps,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )
        solar_declination_series = convert_series_to_degrees_if_requested(
            solar_declination_series,
            angle_output_units,
        )

    if solar_declination_model.value == SolarDeclinationModel.pvgis:
        # if solar_position_model.value == SolarPositionModel.hofierka:

        solar_declination_series = calculate_solar_declination_series_hofierka(
            timestamps=timestamps,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_declination_model.value == SolarDeclinationModel.hargreaves:
        pass

    if solar_declination_model.value == SolarDeclinationModel.pvlib:
        solar_declination_series = calculate_solar_declination_series_pvlib(
            timestamps=timestamps,
            # dtype=dtype,
            # array_backend=array_backend,
            # verbose=verbose,
            # log=log,
        )

    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    log_data_fingerprint(
        data=solar_declination_series.value,
        log_level=log,
        hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
    )

    return solar_declination_series

zenith

Functions:

Name Description
calculate_solar_zenith_series

Calculates the solar position using the requested models and returns the

model_solar_zenith_series

Notes

calculate_solar_zenith_series

calculate_solar_zenith_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_models: List[SolarPositionModel] = [
        noaa
    ],
    apply_atmospheric_refraction: bool = True,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> Dict

Calculates the solar position using the requested models and returns the results in a dictionary.

Source code in pvgis/api/position/zenith.py
@log_function_call
def calculate_solar_zenith_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_models: List[SolarPositionModel] = [SolarPositionModel.noaa],
    # solar_time_model: SolarTimeModel = SolarTimeModel.noaa,
    apply_atmospheric_refraction: bool = True,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> Dict:
    """Calculates the solar position using the requested models and returns the
    results in a dictionary.
    """
    results = {}
    for solar_position_model in solar_position_models:
        if (
            solar_position_model != SolarPositionModel.all
        ):  # ignore 'all' in the enumeration
            solar_zenith_series = model_solar_zenith_series(
                longitude=longitude,
                latitude=latitude,
                timestamps=timestamps,
                timezone=timezone,
                solar_position_model=solar_position_model,
                apply_atmospheric_refraction=apply_atmospheric_refraction,
                perigee_offset=perigee_offset,
                eccentricity_correction_factor=eccentricity_correction_factor,
                dtype=dtype,
                array_backend=array_backend,
                verbose=verbose,
                log=log,
                validate_output=validate_output,
            )
            zenith_values = (
                getattr(solar_zenith_series, angle_output_units, NOT_AVAILABLE)
                if solar_zenith_series
                else NOT_AVAILABLE
            )
            model_entry = {
                TIME_ALGORITHM_NAME: (
                    solar_zenith_series.timing_algorithm
                    if solar_zenith_series
                    else NOT_AVAILABLE
                ),
                POSITION_ALGORITHM_NAME: solar_position_model.value,
                ZENITH_NAME: zenith_values,
                UNITS_COLUMN_NAME: None,
            }
            if fingerprint and solar_zenith_series is not None:
                model_entry[FINGERPRINT_COLUMN_NAME] = generate_hash(zenith_values)
            results = results | {solar_position_model.name: model_entry}

    return results

model_solar_zenith_series

model_solar_zenith_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_model: SolarPositionModel = noaa,
    apply_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarZenith
Notes

The solar altitude angle measures from the horizon up towards the zenith (positive, and down towards the nadir (negative)). The altitude is zero all along the great circle between zenith and nadir.

  • All solar calculation functions return floating angular measurements in radians.

pysolar :

From https://pysolar.readthedocs.io:

  • Altitude is reckoned with zero at the horizon. The altitude is positive when the sun is above the horizon.

  • The result is returned with units.

Source code in pvgis/api/position/zenith.py
@log_function_call
@custom_cached
@validate_with_pydantic(ModelSolarAltitudeTimeSeriesInputModel)
def model_solar_zenith_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_model: SolarPositionModel = SolarPositionModel.noaa,
    apply_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarZenith:
    """
    Notes
    -----

    The solar altitude angle measures from the horizon up towards the zenith
    (positive, and down towards the nadir (negative)). The altitude is zero all
    along the great circle between zenith and nadir.

    - All solar calculation functions return floating angular measurements in
      radians.

    pysolar :

    From https://pysolar.readthedocs.io:

    - Altitude is reckoned with zero at the horizon. The altitude is positive
      when the sun is above the horizon.

    - The result is returned with units.
    """
    solar_zenith_series = None
    solar_altitude_series = None

    if solar_position_model.value == SolarPositionModel.noaa:
        solar_zenith_series = calculate_solar_zenith_series_noaa(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )

    if solar_position_model.value == SolarPositionModel.skyfield:
        pass
    # if solar_position_model.value == SolarPositionModel.skyfield:
    #     solar_zenith, solar_azimuth = calculate_solar_zenith_azimuth_skyfield(
    #         longitude=longitude,
    #         latitude=latitude,
    #         timestamp=timestamp,
    #     )

    if solar_position_model.value == SolarPositionModel.suncalc:
        pass
    # if solar_position_model.value == SolarPositionModel.suncalc:
    #     # note : first azimuth, then altitude
    #     solar_azimuth_south_radians_convention, solar_zenith = suncalc.get_position(
    #         date=timestamp,  # this comes first here!
    #         lng=longitude.degrees,
    #         lat=latitude.degrees,
    #     ).values()  # zero points to south
    #     solar_zenith = SolarAltitude(
    #         value=solar_zenith,
    #         unit=RADIANS,
    #         position_algorithm='suncalc',
    #         timing_algorithm='suncalc',
    #     )
    #     if (
    #         not isfinite(solar_zenith.degrees)
    #         or not solar_zenith.min_degrees <= solar_zenith.degrees <= solar_zenith.max_degrees
    #     ):
    #         raise ValueError(
    #             f"The calculated solar altitude angle {solar_zenith.degrees} is out of the expected range\
    #             [{solar_zenith.min_degrees}, {solar_zenith.max_degrees}] degrees"
    #         )

    if solar_position_model.value == SolarPositionModel.pysolar:
        pass
    # if solar_position_model.value == SolarPositionModel.pysolar:

    #     timestamp = attach_timezone(timestamp, timezone)
    #     solar_zenith = pysolar.solar.get_altitude(
    #         latitude_deg=latitude.degrees,  # this comes first
    #         longitude_deg=longitude.degrees,
    #         when=timestamp,
    #     )  # returns degrees by default
    #     # required by output function
    #     solar_zenith = SolarAltitude(
    #         value=solar_zenith,
    #         unit=DEGREES,
    #         position_algorithm='pysolar',
    #         timing_algorithm='pysolar',
    #     )
    #     if (
    #         not isfinite(solar_zenith.degrees)
    #         or not solar_zenith.min_degrees <= solar_zenith.degrees <= solar_zenith.max_degrees
    #     ):
    #         raise ValueError(
    #             f"The calculated solar altitude angle {solar_zenith.degrees} is out of the expected range\
    #             [{solar_zenith.min_degrees}, {solar_zenith.max_degrees}] degrees"
    #         )

    if solar_position_model.value == SolarPositionModel.jenco:
        solar_altitude_series = calculate_solar_altitude_series_jenco(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            perigee_offset=perigee_offset,
            eccentricity_correction_factor=eccentricity_correction_factor,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_position_model.value == SolarPositionModel.hofierka:
        solar_altitude_series = calculate_solar_altitude_series_hofierka(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            perigee_offset=perigee_offset,
            eccentricity_correction_factor=eccentricity_correction_factor,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_position_model.value == SolarPositionModel.pvlib:
        pass

    # if solar_position_model.value  == SolarPositionModel.pvlib:

    #     solar_zenith = calculate_solar_zenith_pvlib(
    #         longitude=longitude,
    #         latitude=latitude,
    #         timestamp=timestamp,
    #         timezone=timezone,
    #         verbose=verbose,
    #     )
    if isinstance(solar_altitude_series, SolarAltitude):
        solar_zenith_series = SolarZenith(
            value=solar_altitude_series.radians - (np.pi / 2),
            unit=RADIANS,
            position_algorithm=solar_altitude_series.position_algorithm,
            timing_algorithm=solar_altitude_series.timing_algorithm,
        )

    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    return solar_zenith_series

altitude

Functions:

Name Description
calculate_solar_altitude_series

Calculates the solar position using the requested models and returns the

model_solar_altitude_series

Notes

calculate_solar_altitude_series

calculate_solar_altitude_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_models: List[SolarPositionModel] = [
        noaa
    ],
    solar_time_model: SolarTimeModel = noaa,
    apply_atmospheric_refraction: bool = True,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> Dict

Calculates the solar position using the requested models and returns the results in a dictionary.

Source code in pvgis/api/position/altitude.py
@log_function_call
def calculate_solar_altitude_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_models: List[SolarPositionModel] = [SolarPositionModel.noaa],
    solar_time_model: SolarTimeModel = SolarTimeModel.noaa,
    apply_atmospheric_refraction: bool = True,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> Dict:
    """Calculates the solar position using the requested models and returns the
    results in a dictionary.
    """
    results = {}
    for solar_position_model in solar_position_models:
        if (
            solar_position_model != SolarPositionModel.all
        ):  # ignore 'all' in the enumeration
            solar_altitude_series = model_solar_altitude_series(
                longitude=longitude,
                latitude=latitude,
                timestamps=timestamps,
                timezone=timezone,
                solar_position_model=solar_position_model,
                apply_atmospheric_refraction=apply_atmospheric_refraction,
                perigee_offset=perigee_offset,
                eccentricity_correction_factor=eccentricity_correction_factor,
                dtype=dtype,
                array_backend=array_backend,
                verbose=verbose,
                log=log,
                validate_output=validate_output,
            )
            altitude_values = (
                getattr(solar_altitude_series, angle_output_units, NOT_AVAILABLE)
                if solar_altitude_series
                else NOT_AVAILABLE
            )
            model_entry = {
                SolarPositionParameter.timing: (
                    solar_altitude_series.timing_algorithm
                    if solar_altitude_series
                    else NOT_AVAILABLE
                ),
                SolarPositionParameter.positioning: solar_position_model.value,
                SolarPositionParameter.altitude: altitude_values,
                UNITS_COLUMN_NAME: None,
            }
            if fingerprint and solar_altitude_series is not None:
                model_entry[FINGERPRINT_COLUMN_NAME] = generate_hash(altitude_values)
            results = results | {solar_position_model.name: model_entry}

    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    return results

model_solar_altitude_series

model_solar_altitude_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex | Timestamp | None,
    timezone: ZoneInfo,
    solar_position_model: SolarPositionModel = noaa,
    apply_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarAltitude
Notes

The solar altitude angle measures from the horizon up towards the zenith (positive, and down towards the nadir (negative)). The altitude is zero all along the great circle between zenith and nadir.

  • All solar calculation functions return floating angular measurements in radians.

pysolar :

From https://pysolar.readthedocs.io:

  • Altitude is reckoned with zero at the horizon. The altitude is positive when the sun is above the horizon.

  • The result is returned with units.

Source code in pvgis/api/position/altitude.py
@log_function_call
@custom_cached
@validate_with_pydantic(ModelSolarAltitudeTimeSeriesInputModel)
def model_solar_altitude_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex | Timestamp | None,
    timezone: ZoneInfo,
    solar_position_model: SolarPositionModel = SolarPositionModel.noaa,
    apply_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarAltitude:
    """
    Notes
    -----

    The solar altitude angle measures from the horizon up towards the zenith
    (positive, and down towards the nadir (negative)). The altitude is zero all
    along the great circle between zenith and nadir.

    - All solar calculation functions return floating angular measurements in
      radians.

    pysolar :

    From https://pysolar.readthedocs.io:

    - Altitude is reckoned with zero at the horizon. The altitude is positive
      when the sun is above the horizon.

    - The result is returned with units.
    """
    logger.debug(
        f"Executing solar positioning modelling function model_solar_altitude_series() for\n{timestamps}",
        alt=f"Executing [underline]solar positioning modelling[/underline] function model_solar_altitude_series() for\n{timestamps}",
    )
    solar_altitude_series = None

    if solar_position_model.value == SolarPositionModel.noaa:
        solar_altitude_series = calculate_solar_altitude_series_noaa(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )

    if solar_position_model.value == SolarPositionModel.skyfield:
        pass
    # if solar_position_model.value == SolarPositionModel.skyfield:
    #     solar_altitude, solar_azimuth = calculate_solar_altitude_azimuth_skyfield(
    #         longitude=longitude,
    #         latitude=latitude,
    #         timestamp=timestamp,
    #     )

    if solar_position_model.value == SolarPositionModel.suncalc:
        pass
    # if solar_position_model.value == SolarPositionModel.suncalc:
    #     # note : first azimuth, then altitude
    #     solar_azimuth_south_radians_convention, solar_altitude = suncalc.get_position(
    #         date=timestamp,  # this comes first here!
    #         lng=longitude.degrees,
    #         lat=latitude.degrees,
    #     ).values()  # zero points to south
    #     solar_altitude = SolarAltitude(
    #         value=solar_altitude,
    #         unit=RADIANS,
    #         position_algorithm='suncalc',
    #         timing_algorithm='suncalc',
    #     )
    #     if (
    #         not isfinite(solar_altitude.degrees)
    #         or not solar_altitude.min_degrees <= solar_altitude.degrees <= solar_altitude.max_degrees
    #     ):
    #         raise ValueError(
    #             f"The calculated solar altitude angle {solar_altitude.degrees} is out of the expected range\
    #             [{solar_altitude.min_degrees}, {solar_altitude.max_degrees}] degrees"
    #         )

    if solar_position_model.value == SolarPositionModel.pysolar:
        pass
    # if solar_position_model.value == SolarPositionModel.pysolar:

    #     timestamp = attach_timezone(timestamp, timezone)
    #     solar_altitude = pysolar.solar.get_altitude(
    #         latitude_deg=latitude.degrees,  # this comes first
    #         longitude_deg=longitude.degrees,
    #         when=timestamp,
    #     )  # returns degrees by default
    #     # required by output function
    #     solar_altitude = SolarAltitude(
    #         value=solar_altitude,
    #         unit=DEGREES,
    #         position_algorithm='pysolar',
    #         timing_algorithm='pysolar',
    #     )
    #     if (
    #         not isfinite(solar_altitude.degrees)
    #         or not solar_altitude.min_degrees <= solar_altitude.degrees <= solar_altitude.max_degrees
    #     ):
    #         raise ValueError(
    #             f"The calculated solar altitude angle {solar_altitude.degrees} is out of the expected range\
    #             [{solar_altitude.min_degrees}, {solar_altitude.max_degrees}] degrees"
    #         )

    if solar_position_model.value == SolarPositionModel.jenco:
        solar_altitude_series = calculate_solar_altitude_series_jenco(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            perigee_offset=perigee_offset,
            eccentricity_correction_factor=eccentricity_correction_factor,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_position_model.value == SolarPositionModel.hofierka:
        solar_altitude_series = calculate_solar_altitude_series_hofierka(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            perigee_offset=perigee_offset,
            eccentricity_correction_factor=eccentricity_correction_factor,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_position_model.value == SolarPositionModel.pvlib:
        solar_altitude_series = calculate_solar_altitude_series_pvlib(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    logger.debug(
        f"Returning solar altitude time series :\n{solar_altitude_series}",
        alt=f"Returning [yellow]solar altitude[/yellow] time series :\n{solar_altitude_series}",
    )
    return solar_altitude_series

azimuth

An overview of conventions and conversions from a North-based system to either East- or South-based systems is:

         ┌─────────────┐  ┌────────────┐  ┌────────────┐
         │     N=0     │  │     N      │  │      N     │
         │      ▲      │  │     ▲      │  │      ▲     │
 Origin  │   W ◄┼► E   │  │  W ◄┼► E=0 │  │   W ◄┼► E  │
         │      ▼      │  │     ▼      │  │      ▼     │
         │      S      │  │     S      │  │      S=0   │
         └─────────────┘  └────────────┘  └────────────┘
         ┌─────────────┐  ┌────────────┐  ┌────────────┐
         │             │  │            │  │            │
         │             │  │            │  │            │

Input South │ 180 │ │ 90 │ │ 0 │ (IS) │ │ │ │ │ │ │ │ │ │ │ │ └─────────────┘ └────────────┘ └────────────┘ ┌─────────────┐ ┌────────────┐ ┌────────────┐ │ │ │ │ │ │ Internal │ │ │ │ │ │ │ = │ │ IS - 90 │ │ IS - 180 │ Conversion │ │ │ │ │ │ │ │ │ │ │ │ └─────────────┘ └────────────┘ └────────────┘

Functions:

Name Description
calculate_solar_azimuth_series

Calculates the solar position using all models and returns the results in a table.

model_solar_azimuth_series

The solar azimuth angle measures horizontally around the horizon from north

calculate_solar_azimuth_series

calculate_solar_azimuth_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_models: List[SolarPositionModel] = [
        noaa
    ],
    solar_time_model: SolarTimeModel = noaa,
    apply_atmospheric_refraction: bool = True,
    refracted_solar_zenith: RefractedSolarZenith
    | None = REFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> Dict

Calculates the solar position using all models and returns the results in a table.

Source code in pvgis/api/position/azimuth.py
def calculate_solar_azimuth_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    solar_position_models: List[SolarPositionModel] = [SolarPositionModel.noaa],
    solar_time_model: SolarTimeModel = SolarTimeModel.noaa,
    apply_atmospheric_refraction: bool = True,
    refracted_solar_zenith: (
        RefractedSolarZenith | None
    ) = REFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,  # radians
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> Dict:
    """
    Calculates the solar position using all models and returns the results in a table.
    """
    results = {}
    for solar_position_model in solar_position_models:
        if (
            solar_position_model != SolarPositionModel.all
        ):  # ignore 'all' in the enumeration
            solar_azimuth_series = model_solar_azimuth_series(
                longitude=longitude,
                latitude=latitude,
                timestamps=timestamps,
                timezone=timezone,
                solar_position_model=solar_position_model,
                apply_atmospheric_refraction=apply_atmospheric_refraction,
                refracted_solar_zenith=refracted_solar_zenith,
                solar_time_model=solar_time_model,
                perigee_offset=perigee_offset,
                eccentricity_correction_factor=eccentricity_correction_factor,
                verbose=verbose,
                validate_output=validate_output,
            )
            azimuth_values = (
                getattr(solar_azimuth_series, angle_output_units, NOT_AVAILABLE)
                if solar_azimuth_series
                else NOT_AVAILABLE
            )
            model_entry = {
                TIME_ALGORITHM_NAME: (
                    solar_azimuth_series.timing_algorithm
                    if solar_azimuth_series
                    else NOT_AVAILABLE
                ),
                POSITION_ALGORITHM_NAME: (
                    solar_azimuth_series.position_algorithm
                    if solar_azimuth_series
                    else NOT_AVAILABLE
                ),
                AZIMUTH_NAME: azimuth_values,
                AZIMUTH_ORIGIN_NAME: (
                    solar_azimuth_series.origin
                    if solar_azimuth_series
                    else NOT_AVAILABLE
                ),
                UNITS_COLUMN_NAME: angle_output_units,
            }
            if fingerprint and solar_azimuth_series is not None:
                model_entry[FINGERPRINT_COLUMN_NAME] = generate_hash(azimuth_values)
            results = results | {solar_position_model.name: model_entry}

    return results

model_solar_azimuth_series

model_solar_azimuth_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex | None,
    timezone: ZoneInfo | None,
    solar_position_model: SolarPositionModel = noaa,
    apply_atmospheric_refraction: bool = True,
    refracted_solar_zenith: RefractedSolarZenith
    | None = REFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    solar_time_model: SolarTimeModel = milne,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = 0,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarAzimuth

The solar azimuth angle measures horizontally around the horizon from north through east, south, and west.

Notes
  • All solar calculation functions return floating angular measurements in radians.

pysolar :

From https://pysolar.readthedocs.io:

  • Azimuth is reckoned with zero corresponding to north. Positive azimuth estimates correspond to estimates east of north; negative estimates, or estimates larger than 180 are west of north. In the northern hemisphere, if we speak in terms of (altitude, azimuth), the sun comes up around (0, 90), reaches (70, 180) around noon, and sets around (0, 270).

  • The result is returned with units.

Source code in pvgis/api/position/azimuth.py
@log_function_call
@custom_cached
@validate_with_pydantic(ModelSolarAzimuthTimeSeriesInputModel)
def model_solar_azimuth_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex | None,
    timezone: ZoneInfo | None,
    solar_position_model: SolarPositionModel = SolarPositionModel.noaa,
    apply_atmospheric_refraction: bool = True,
    refracted_solar_zenith: (
        RefractedSolarZenith | None
    ) = REFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,  # radians
    solar_time_model: SolarTimeModel = SolarTimeModel.milne,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = 0,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> SolarAzimuth:
    """
    The solar azimuth angle measures horizontally around the horizon from north
    through east, south, and west.

    Notes
    -----

    - All solar calculation functions return floating angular measurements in
      radians.

    pysolar :

    From https://pysolar.readthedocs.io:

    - Azimuth is reckoned with zero corresponding to north. Positive azimuth
      estimates correspond to estimates east of north; negative estimates, or
      estimates larger than 180 are west of north. In the northern hemisphere,
      if we speak in terms of (altitude, azimuth), the sun comes up around
      (0, 90), reaches (70, 180) around noon, and sets around (0, 270).

    - The result is returned with units.
    """
    logger.debug(
        f"Executing solar positioning modelling function model_solar_azimuth_series() for\n{timestamps}",
        alt=f"Executing [underline]solar positioning modelling[/underline] function model_solar_azimuth_series() for\n{timestamps}",
    )
    solar_azimuth_series = None

    if solar_position_model.value == SolarPositionModel.noaa:
        solar_azimuth_series = calculate_solar_azimuth_series_noaa(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )

    if solar_position_model.value == SolarPositionModel.jenco:
        solar_azimuth_series = calculate_solar_azimuth_series_jenco(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_position_model.value == SolarPositionModel.skyfield:
        pass
    # if solar_position_model.value == SolarPositionModel.skyfield:

    #     solar_altitude, solar_azimuth = calculate_solar_altitude_azimuth_skyfield(
    #             longitude=longitude,
    #             latitude=latitude,
    #             timestamp=timestamp,
    #             )

    if solar_position_model.value == SolarPositionModel.suncalc:
        pass
    # if solar_position_model.value == SolarPositionModel.suncalc:
    #     # note : first azimuth, then altitude
    #     solar_azimuth_south_radians_convention, solar_altitude = suncalc.get_position(
    #         date=timestamp,  # this comes first here!
    #         lng=longitude.degrees,
    #         lat=latitude.degrees,
    #     ).values()  # zero points to south
    #     solar_azimuth = convert_south_to_north_radians_convention(
    #         solar_azimuth_south_radians_convention
    #     )
    #     solar_azimuth = SolarAzimuth(
    #         value=solar_azimuth,
    #         unit=RADIANS,
    #         position_algorithm='suncalc',
    #         timing_algorithm='suncalc',
    #     )

    if solar_position_model.value == SolarPositionModel.pysolar:
        pass
        # timestamp = attach_timezone(timestamp, timezone)

        # solar_azimuth = pysolar.solar.get_azimuth(
        #     latitude_deg=longitude.degrees,  # this comes first
        #     longitude_deg=latitude.degrees,
        #     when=timestamp,
        # )  # returns degrees by default
        # # required by output function
        # solar_azimuth = SolarAzimuth(
        #     value=solar_azimuth,
        #     unit=DEGREES,
        #     position_algorithm='pysolar',
        #     timing_algorithm='pysolar',
        # )

    if solar_position_model.value == SolarPositionModel.hofierka:
        pass
        # solar_azimuth = calculate_solar_azimuth_pvgis(
        #     longitude=longitude,
        #     latitude=latitude,
        #     timestamp=timestamp,
        #     timezone=timezone,
        #     solar_time_model=solar_time_model,
        # )

    if solar_position_model.value == SolarPositionModel.pvlib:
        solar_azimuth_series = calculate_solar_azimuth_series_pvlib(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    # log_data_fingerprint(
    #         data=solar_azimuth_series.value,
    #         log_level=log,
    #         hash_after_this_verbosity_level=HASH_AFTER_THIS_VERBOSITY_LEVEL,
    # )
    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    logger.debug(
        f"Returning solar azimuth time series :\n{solar_azimuth_series}",
        alt=f"Returning [yellow]solar azimuth[/yellow] time series :\n{solar_azimuth_series}",
    )

    return solar_azimuth_series

overview

Functions:

Name Description
calculate_solar_position_overview_series

Calculate an overview of solar position parameters for a time series.

model_solar_position_overview_series

Model solar position parameters for a position and moment in time.

calculate_solar_position_overview_series

calculate_solar_position_overview_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    event: List[SolarEvent | None] = [None],
    surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
    solar_position_models: List[SolarPositionModel] = [
        noaa
    ],
    sun_horizon_position: List[
        SunHorizonPositionModel
    ] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_model: SolarIncidenceModel = iqbal,
    horizon_profile: DataArray | None = None,
    shading_model: ShadingModel = pvgis,
    complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    apply_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    solar_time_model: SolarTimeModel = noaa,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = VERBOSE_LEVEL_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> Dict

Calculate an overview of solar position parameters for a time series.

Calculate an overview of solar position parameters for a solar surface orientation and tilt at a given geographic position for a time series and for the user-requested solar position models (as in positioning algorithms) and one solar time model (as in solar timing algorithm).

Notes

While it is straightforward to report the solar position parameters for a series of solar position models (positioning algorithms), offering the option for multiple solar time models (timing algorithms), would mean to carefully craft the combinations for each solar time model and solar position models. Not impossible, yet something for expert users that would like to assess different combinations of algorithms to explore and assess solar position parameters.

Source code in pvgis/api/position/overview.py
def calculate_solar_position_overview_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    event: List[SolarEvent | None] = [None],
    surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
    solar_position_models: List[SolarPositionModel] = [SolarPositionModel.noaa],
    sun_horizon_position: List[SunHorizonPositionModel] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_model: SolarIncidenceModel = SolarIncidenceModel.iqbal,
    horizon_profile: DataArray | None = None,
    shading_model: ShadingModel = ShadingModel.pvgis,
    complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    apply_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    solar_time_model: SolarTimeModel = SolarTimeModel.noaa,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = RADIANS,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = VERBOSE_LEVEL_DEFAULT,
    fingerprint: bool = FINGERPRINT_FLAG_DEFAULT,
) -> Dict:
    """Calculate an overview of solar position parameters for a time series.

    Calculate an overview of solar position parameters for a solar surface
    orientation and tilt at a given geographic position for a time series and
    for the user-requested solar position models (as in positioning algorithms)
    and one solar time model (as in solar timing algorithm).

    Notes
    -----
    While it is straightforward to report the solar position parameters for a
    series of solar position models (positioning algorithms), offering the
    option for multiple solar time models (timing algorithms), would mean to
    carefully craft the combinations for each solar time model and solar
    position models. Not impossible, yet something for expert users that would
    like to assess different combinations of algorithms to explore and assess
    solar position parameters.

    """
    results = {}
    for solar_position_model in solar_position_models:
        # for the time being! ------------------------------------------------
        if solar_position_model != SolarPositionModel.noaa:
            logger.warning(
                f"Solar geometry overview series is not implemented for the requested solar position model: {solar_position_model}!"
            )
        # --------------------------------------------------------------------
        if (
            solar_position_model != SolarPositionModel.all
        ):  # ignore 'all' in the enumeration
            (
                solar_declination_series,
                solar_hour_angle_series,
                solar_zenith_series,
                solar_altitude_series,
                solar_azimuth_series,
                surface_orientation,
                surface_tilt,
                solar_incidence_series,
                sun_horizon_position_series,
                surface_in_shade_series,
                solar_event_type_series,
                solar_event_time_series,
            ) = model_solar_position_overview_series(
                longitude=longitude,
                latitude=latitude,
                timestamps=timestamps,
                timezone=timezone,
                event=event,
                surface_orientation=surface_orientation,
                surface_tilt=surface_tilt,
                solar_time_model=solar_time_model,
                solar_position_model=solar_position_model,
                sun_horizon_position=sun_horizon_position,
                horizon_profile=horizon_profile,
                shading_model=shading_model,
                apply_atmospheric_refraction=apply_atmospheric_refraction,
                # solar_incidence_model=solar_incidence_model,
                complementary_incidence_angle=complementary_incidence_angle,
                zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
                perigee_offset=perigee_offset,
                eccentricity_correction_factor=eccentricity_correction_factor,
                dtype=dtype,
                array_backend=array_backend,
                verbose=verbose,
                log=log,
                validate_output=validate_output,
            )
            solar_position_model_overview = {
                solar_position_model.name: {
                    SolarPositionParameter.timing: (
                        solar_hour_angle_series.timing_algorithm
                        if solar_hour_angle_series
                        else NOT_AVAILABLE
                    ),
                    SolarPositionParameter.declination: (
                        getattr(
                            solar_declination_series, angle_output_units, NOT_AVAILABLE
                        )
                        if solar_declination_series
                        else NOT_AVAILABLE
                    ),
                    SolarPositionParameter.hour_angle: (
                        getattr(
                            solar_hour_angle_series, angle_output_units, NOT_AVAILABLE
                        )
                        if solar_hour_angle_series
                        else NOT_AVAILABLE
                    ),
                    SolarPositionParameter.positioning: solar_position_model.value,
                    SolarPositionParameter.zenith: (
                        getattr(solar_zenith_series, angle_output_units, NOT_AVAILABLE)
                        if solar_zenith_series
                        else NOT_AVAILABLE
                    ),
                    SolarPositionParameter.altitude: (
                        getattr(
                            solar_altitude_series, angle_output_units, NOT_AVAILABLE
                        )
                        if solar_altitude_series
                        else NOT_AVAILABLE
                    ),
                    SolarPositionParameter.azimuth: (
                        getattr(solar_azimuth_series, angle_output_units, NOT_AVAILABLE)
                        if solar_azimuth_series
                        else NOT_AVAILABLE
                    ),
                    AZIMUTH_ORIGIN_NAME: (  # Not meant as a column name !
                        solar_azimuth_series.origin
                        if solar_azimuth_series
                        else NOT_AVAILABLE
                    ),
                    SURFACE_ORIENTATION_NAME: (
                        getattr(surface_orientation, angle_output_units, NOT_AVAILABLE)
                        if surface_orientation
                        else None
                    ),
                    SURFACE_TILT_NAME: (
                        getattr(surface_tilt, angle_output_units, NOT_AVAILABLE)
                        if surface_tilt
                        else None
                    ),
                    SolarPositionParameter.incidence: (
                        getattr(
                            solar_incidence_series, angle_output_units, NOT_AVAILABLE
                        )
                        if solar_incidence_series
                        else NOT_AVAILABLE
                    ),
                    INCIDENCE_ALGORITHM_NAME: (
                        solar_incidence_series.incidence_algorithm
                        if solar_incidence_series
                        else NOT_AVAILABLE
                    ),
                    INCIDENCE_DEFINITION: (
                        solar_incidence_series.definition
                        if solar_incidence_series
                        else NOT_AVAILABLE
                    ),
                    SUN_HORIZON_POSITIONS_NAME: (  # Requested positions
                        sun_horizon_position if sun_horizon_position else NOT_AVAILABLE
                    ),
                    SolarPositionParameter.sun_horizon: (  # Series of sun-horizon position
                        sun_horizon_position_series.tolist()
                        if sun_horizon_position_series is not None
                        else NOT_AVAILABLE
                    ),
                    **generate_dictionary_of_surface_in_shade_series(
                        surface_in_shade_series,
                        angle_output_units,
                    ),  # This is the horizon height profile !
                    SOLAR_EVENTS_NAME: (  # Requested solar events
                        event if event else NOT_AVAILABLE
                    ),
                    **(
                        {SolarPositionParameter.event_type: solar_event_type_series}
                        if solar_event_type_series is not None
                        and not (
                            isinstance(solar_event_type_series, (list, numpy.ndarray))
                            and all(x is None for x in solar_event_type_series)
                        )
                        else {}
                    ),
                    **(
                        {SolarPositionParameter.event_time: solar_event_time_series}
                        if solar_event_time_series is not None
                        and not (
                            isinstance(solar_event_time_series, DatetimeIndex)
                            and all(isna(x) for x in solar_event_time_series)
                        )
                        else {}
                    ),
                    UNITS_COLUMN_NAME: angle_output_units,
                    FINGERPRINT_COLUMN_NAME: (
                        generate_hash(solar_incidence_series)
                        if fingerprint
                        else NOT_AVAILABLE
                    ),
                }
            }
            results = results | solar_position_model_overview

    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    return results

model_solar_position_overview_series

model_solar_position_overview_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    event: List[SolarEvent | None] = [None],
    surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
    solar_time_model: SolarTimeModel = milne,
    solar_position_model: SolarPositionModel = noaa,
    sun_horizon_position: List[
        SunHorizonPositionModel
    ] = SUN_HORIZON_POSITION_DEFAULT,
    horizon_profile: DataArray | None = None,
    shading_model: ShadingModel = pvgis,
    apply_atmospheric_refraction: bool = True,
    refracted_solar_zenith: float
    | None = REFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> Tuple

Model solar position parameters for a position and moment in time.

Model essential solar position parameters for a solar surface orientation and tilt at a given geographic position for a time series based on a given solar position model (as in positioning algorithm, see class SolarPositionModel) and solar time model (as in solar timing algorithm, see class SolarTimeModel) :

  • solar declination
  • solar hour angle
  • solar zenith
  • solar altitude
  • solar azimuth
  • solar incidence
  • behind horizon
Notes

The solar altitude angle measures from the horizon up towards the zenith (positive, and down towards the nadir (negative)). The altitude is zero all along the great circle between zenith and nadir.

The solar azimuth angle measures horizontally around the horizon from north through east, south, and west.

In order to avoid confusion, the solar incidence angle series are derived using specific functions for each "algorithm". For example, the NOAA solar positioning set of equations are "bound" to the calculate_solar_incidence_series_iqbal(). However, thinking of more flexibility, for example to facilitate a cross-comparison between different implementations of the Equation of Time and their impact on different solar incidence angle definitions, we can refactor the source code to allow for combinations of different "blocks" of solar timing and positioning algorithms.

The following combinations are currently the default ones to derive the solar incidence angle :

  • SolarPositionModel.noaa + SolarIncidenceModel.iqbal

  • SolarPositionModel.jenco + SolarIncidenceModel.jenco

  • SolarPositionModel.hofierka + SolarIncidenceModel.hofierka

Source code in pvgis/api/position/overview.py
@log_function_call
@validate_with_pydantic(ModelSolarPositionOverviewSeriesInputModel)
def model_solar_position_overview_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    event: List[SolarEvent | None] = [None],
    surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
    solar_time_model: SolarTimeModel = SolarTimeModel.milne,
    solar_position_model: SolarPositionModel = SolarPositionModel.noaa,
    sun_horizon_position: List[SunHorizonPositionModel] = SUN_HORIZON_POSITION_DEFAULT,
    horizon_profile: DataArray | None = None,
    shading_model: ShadingModel = ShadingModel.pvgis,
    apply_atmospheric_refraction: bool = True,
    refracted_solar_zenith: float | None = REFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    # solar_incidence_model: SolarIncidenceModel = SolarIncidenceModel.iqbal,
    complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
) -> Tuple:
    """Model solar position parameters for a position and moment in time.

    Model essential solar position parameters for a solar surface
    orientation and tilt at a given geographic position for a time series based
    on a given solar position model (as in positioning algorithm, see class
    `SolarPositionModel`) and solar time model (as in solar timing algorithm,
    see class `SolarTimeModel`) :

    - solar declination
    - solar hour angle
    - solar zenith
    - solar altitude
    - solar azimuth
    - solar incidence
    - behind horizon

    Notes
    -----
    The solar altitude angle measures from the horizon up towards the zenith
    (positive, and down towards the nadir (negative)). The altitude is zero all
    along the great circle between zenith and nadir.

    The solar azimuth angle measures horizontally around the horizon from north
    through east, south, and west.

    In order to avoid confusion, the solar incidence angle series are derived
    using specific functions for each "algorithm". For example, the NOAA solar
    positioning set of equations are "bound" to the
    `calculate_solar_incidence_series_iqbal()`. However, thinking of more
    flexibility, for example to facilitate a cross-comparison
    between different implementations of the Equation of Time and their impact
    on different solar incidence angle definitions, we can refactor the source
    code to allow for combinations of different "blocks" of solar timing and
    positioning algorithms.

    The following combinations are currently the default ones to derive the
    solar incidence angle :

    - SolarPositionModel.noaa + SolarIncidenceModel.iqbal

    - SolarPositionModel.jenco + SolarIncidenceModel.jenco

    - SolarPositionModel.hofierka + SolarIncidenceModel.hofierka

    """
    solar_declination_series = None  # updated if applicable
    solar_hour_angle_series = None
    solar_zenith_series = None  # updated if applicable
    solar_altitude_series = None
    solar_azimuth_series = None
    solar_incidence_series = None
    sun_horizon_position_series = None
    surface_in_shade_series = model_surface_in_shade_series(
        horizon_profile=horizon_profile,
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        timezone=timezone,
        solar_time_model=solar_time_model,
        solar_position_model=solar_position_model,
        shading_model=shading_model,
        apply_atmospheric_refraction=apply_atmospheric_refraction,
        refracted_solar_zenith=refracted_solar_zenith,
        perigee_offset=perigee_offset,
        eccentricity_correction_factor=eccentricity_correction_factor,
        dtype=dtype,
        array_backend=array_backend,
        verbose=verbose,
        log=log,
        validate_output=validate_output,
    )
    solar_event_series = model_solar_event_time_series(
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        event=event,
        timezone=timezone,
        dtype=dtype,
        array_backend=array_backend,
        verbose=verbose,
        log=log,
    )
    solar_event_type_series = solar_event_series.event
    solar_event_time_series = solar_event_series.value

    if solar_position_model.value == SolarPositionModel.noaa:
        solar_declination_series = calculate_solar_declination_series_noaa(
            timestamps=timestamps,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )
        solar_hour_angle_series = calculate_solar_hour_angle_series_noaa(
            longitude=longitude,
            timestamps=timestamps,
            timezone=timezone,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )
        solar_zenith_series = calculate_solar_zenith_series_noaa(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )
        solar_altitude_series = calculate_solar_altitude_series_noaa(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )
        solar_azimuth_series = calculate_solar_azimuth_series_noaa(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )
        # Iqbal (1983) measures azimuthal angles from South !
        surface_orientation_south_convention = SurfaceOrientation(
            value=convert_north_to_south_radians_convention(
                north_based_angle=surface_orientation
            ),
            unit=RADIANS,
        )
        solar_incidence_series = calculate_solar_incidence_series_iqbal(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            surface_orientation=surface_orientation_south_convention,
            surface_tilt=surface_tilt,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            sun_horizon_position=sun_horizon_position,
            surface_in_shade_series=surface_in_shade_series,
            complementary_incidence_angle=complementary_incidence_angle,
            zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )

    if solar_position_model.value == SolarPositionModel.skyfield:
        pass
        # solar_hour_angle, solar_declination = calculate_solar_hour_angle_declination_skyfield(
        #     longitude=longitude,
        #     latitude=latitude,
        #     timestamp=timestamp,
        #     timezone=timezone,
        # )
        # solar_altitude, solar_azimuth = calculate_solar_altitude_azimuth_skyfield(
        #         longitude=longitude,
        #         latitude=latitude,
        #         timestamp=timestamp,
        #         )
        # solar_zenith = SolarZenith(
        #     value = 90 - solar_altitude.degrees,
        #     unit = DEGREES,
        #     position_algorithm=solar_azimuth.position_algorithm,
        #     timing_algorithm=solar_azimuth.timing_algorithm,
        # )

    if solar_position_model.value == SolarPositionModel.suncalc:
        pass
        # # note : first azimuth, then altitude
        # solar_azimuth_south_radians_convention, solar_altitude = suncalc.get_position(
        #     date=timestamp,  # this comes first here!
        #     lng=longitude.degrees,
        #     lat=latitude.degrees,
        # ).values()  # zero points to south
        # solar_azimuth = convert_south_to_north_radians_convention(
        #     solar_azimuth_south_radians_convention
        # )
        # solar_azimuth = SolarAzimuth(
        #     value=solar_azimuth,
        #     unit=RADIANS,
        #     position_algorithm='suncalc',
        #     timing_algorithm='suncalc',
        # )
        # solar_altitude = SolarAltitude(
        #     value=solar_altitude,
        #     unit=RADIANS,
        #     position_algorithm='suncalc',
        #     timing_algorithm='suncalc',
        # )
        # solar_zenith = SolarZenith(
        #     value = 90 - solar_altitude.degrees,
        #     unit = DEGREES,
        #     position_algorithm='suncalc',
        #     timing_algorithm='suncalc',
        # )

    if solar_position_model.value == SolarPositionModel.pysolar:
        pass
        # timestamp = attach_timezone(timestamp, timezone)

        # solar_altitude = pysolar.solar.get_altitude(
        #     latitude_deg=latitude.degrees,  # this comes first
        #     longitude_deg=longitude.degrees,
        #     when=timestamp,
        # )  # returns degrees by default
        # # required by output function
        # solar_altitude = SolarAltitude(
        #     value=solar_altitude,
        #     unit=DEGREES,
        #     position_algorithm='pysolar',
        #     timing_algorithm='pysolar',
        # )
        # solar_zenith = SolarZenith(
        #     value = 90 - solar_altitude.degrees,
        #     unit = DEGREES,
        #     position_algorithm=solar_altitude.position_algorithm,
        #     timing_algorithm=solar_altitude.timing_algorithm,
        # )
        # solar_azimuth = pysolar.solar.get_azimuth(
        #     latitude_deg=latitude.degrees,  # this comes first
        #     longitude_deg=longitude.degrees,
        #     when=timestamp,
        # )  # returns degrees by default
        # # required by output function
        # solar_azimuth = SolarAzimuth(
        #     value=solar_azimuth,
        #     unit=DEGREES,
        #     position_algorithm='pysolar',
        #     timing_algorithm='pysolar',
        # )

    if solar_position_model.value == SolarPositionModel.jenco:
        solar_declination_series = calculate_solar_declination_series_jenco(
            timestamps=timestamps,
            perigee_offset=perigee_offset,
            eccentricity_correction_factor=eccentricity_correction_factor,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )
        solar_altitude_series = calculate_solar_altitude_series_jenco(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            perigee_offset=perigee_offset,
            eccentricity_correction_factor=eccentricity_correction_factor,
            dtype=dtype,
            array_backend=array_backend,
            verbose=0,
            log=log,
        )
        solar_azimuth_series = calculate_solar_azimuth_series_jenco(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            dtype=dtype,
            array_backend=array_backend,
            verbose=0,
            log=log,
        )  # North = 0
        surface_orientation_south_convention = SurfaceOrientation(
            value=convert_north_to_south_radians_convention(
                north_based_angle=surface_orientation
            ),
            unit=RADIANS,
        )
        # And apparently, defined the complementary surface tilt angle too!
        # from math import pi
        # surface_tilt = SurfaceTilt(
        #         value=(pi/2 - surface_tilt.radians),
        #         unit=RADIANS,
        #         )
        # ---------------------------------------------------------- Update-Me
        solar_incidence_series = calculate_solar_incidence_series_jenco(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            # surface_orientation=surface_orientation,
            # surface_orientation=surface_orientation_east_convention,
            surface_orientation=surface_orientation_south_convention,
            surface_tilt=surface_tilt,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            surface_in_shade_series=surface_in_shade_series,
            complementary_incidence_angle=complementary_incidence_angle,
            zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
            perigee_offset=perigee_offset,
            eccentricity_correction_factor=eccentricity_correction_factor,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_position_model.value == SolarPositionModel.hofierka:
        solar_declination_series = calculate_solar_declination_series_hofierka(
            timestamps=timestamps,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )
        solar_hour_angle_series = calculate_solar_hour_angle_series_hofierka(
            longitude=longitude,
            timestamps=timestamps,
            timezone=timezone,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )
        solar_altitude_series = calculate_solar_altitude_series_hofierka(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            perigee_offset=perigee_offset,
            eccentricity_correction_factor=eccentricity_correction_factor,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )
        solar_azimuth_series = calculate_solar_azimuth_series_hofierka(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            # apply_atmospheric_refraction=apply_atmospheric_refraction,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )  # East = 0 !
        surface_orientation_south_convention = SurfaceOrientation(
            value=convert_north_to_south_radians_convention(
                north_based_angle=surface_orientation
            ),
            unit=RADIANS,
        )
        solar_incidence_series = calculate_solar_incidence_series_hofierka(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            # surface_orientation=surface_orientation,
            surface_orientation=surface_orientation_south_convention,
            surface_tilt=surface_tilt,
            complementary_incidence_angle=complementary_incidence_angle,
            zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_position_model.value == SolarPositionModel.pvlib:

        solar_declination_series = calculate_solar_declination_series_pvlib(
            timestamps=timestamps,
            # dtype=dtype,
            # array_backend=array_backend,
            # verbose=verbose,
            # log=log,
        )
        solar_hour_angle_series = calculate_solar_hour_angle_series_pvlib(
            longitude=longitude,
            timestamps=timestamps,
            # timezone=timezone,
            # dtype=dtype,
            # array_backend=array_backend,
            # verbose=verbose,
            # log=log,
        )
        solar_zenith_series = calculate_solar_zenith_series_pvlib(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )
        solar_altitude_series = calculate_solar_altitude_series_pvlib(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )
        solar_azimuth_series = calculate_solar_azimuth_series_pvlib(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )
        solar_incidence_series = calculate_solar_incidence_series_pvlib(
            longitude=longitude,
            latitude=latitude,
            surface_tilt=surface_tilt,
            surface_orientation=surface_orientation,
            timestamps=timestamps,
            complementary_incidence_angle=complementary_incidence_angle,
            zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_incidence_series.sun_horizon_position is not None:
        sun_horizon_position_series = solar_incidence_series.sun_horizon_position
    else:
        sun_horizon_position = NOT_AVAILABLE

    if shading_model:

        surface_in_shade_series = model_surface_in_shade_series(
            horizon_profile=horizon_profile,
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            solar_time_model=solar_time_model,
            solar_position_model=solar_position_model,
            shading_model=shading_model,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            refracted_solar_zenith=refracted_solar_zenith,
            perigee_offset=perigee_offset,
            eccentricity_correction_factor=eccentricity_correction_factor,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
            validate_output=validate_output,
        )

    position_series = (
        solar_declination_series if solar_declination_series is not None else None,
        solar_hour_angle_series if solar_hour_angle_series is not None else None,
        solar_zenith_series if solar_zenith_series is not None else None,
        solar_altitude_series if solar_altitude_series is not None else None,
        solar_azimuth_series if solar_azimuth_series is not None else None,
        surface_orientation if surface_orientation is not None else None,
        surface_tilt if surface_tilt is not None else None,
        solar_incidence_series if solar_incidence_series is not None else None,
        (
            sun_horizon_position_series
            if sun_horizon_position_series is not None
            else None
        ),
        surface_in_shade_series if surface_in_shade_series is not None else None,
        solar_event_type_series if solar_event_series.event is not None else None,
        solar_event_time_series if solar_event_series.value is not None else None,
    )
    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    return position_series

incidence

API modules to calculate the solar incidence angle between the direction of the sun-to-surface vector and either the direction of the normal-to-surface vector or the direction of the surface-plane vector.

Attention is required im handling the rotational solar azimuth and surface orientation (also referred to as surface azimuth) anngles. The origin of measuring azimuthal angles will obvisouly impact the direction of the calculated angles. See also the API azimuth.py module.

Functions:

Name Description
calculate_solar_incidence_series

Calculates the solar Incidence angle for the selected models and returns the results in a table

model_solar_incidence_series

calculate_solar_incidence_series

calculate_solar_incidence_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
    solar_position_model: SolarPositionModel = noaa,
    sun_horizon_position: List[
        SunHorizonPositionModel
    ] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_models: List[SolarIncidenceModel] = [
        iqbal
    ],
    horizon_profile: DataArray | None = None,
    shading_model: ShadingModel = pvgis,
    complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = ANGLE_OUTPUT_UNITS_DEFAULT,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
) -> Dict

Calculates the solar Incidence angle for the selected models and returns the results in a table

Source code in pvgis/api/position/incidence.py
@log_function_call
def calculate_solar_incidence_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo,
    surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
    # solar_time_model: SolarTimeModel = SolarTimeModel.milne,
    solar_position_model: SolarPositionModel = SolarPositionModel.noaa,  # Only one !
    sun_horizon_position: List[SunHorizonPositionModel] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_models: List[SolarIncidenceModel] = [SolarIncidenceModel.iqbal],
    horizon_profile: DataArray | None = None,
    shading_model: ShadingModel = ShadingModel.pvgis,
    complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    angle_output_units: str = ANGLE_OUTPUT_UNITS_DEFAULT,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
) -> Dict:
    """Calculates the solar Incidence angle for the selected models and returns the results in a table"""
    results = {}
    for solar_incidence_model in solar_incidence_models:
        if (
            solar_incidence_model != SolarIncidenceModel.all
        ):  # ignore 'all' in the enumeration
            solar_incidence_series = model_solar_incidence_series(
                longitude=longitude,
                latitude=latitude,
                timestamps=timestamps,
                timezone=timezone,
                surface_orientation=surface_orientation,
                surface_tilt=surface_tilt,
                solar_position_model=solar_position_model,
                sun_horizon_position=sun_horizon_position,
                # solar_time_model=solar_time_model,
                solar_incidence_model=solar_incidence_model,
                horizon_profile=horizon_profile,
                shading_model=shading_model,
                complementary_incidence_angle=complementary_incidence_angle,
                zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
                perigee_offset=perigee_offset,
                eccentricity_correction_factor=eccentricity_correction_factor,
                dtype=dtype,
                array_backend=array_backend,
                validate_output=validate_output,
                verbose=verbose,
                log=log,
            )
            solar_incidence_model_series = {
                solar_incidence_model.name: {
                    TIME_ALGORITHM_NAME: (
                        solar_incidence_series.timing_algorithm
                        if solar_incidence_series
                        else NOT_AVAILABLE
                    ),
                    POSITION_ALGORITHM_NAME: (
                        solar_incidence_series.position_algorithm
                        if solar_incidence_series
                        else NOT_AVAILABLE
                    ),
                    # ALTITUDE_NAME: getattr(solar_altitude_series, angle_output_units, NOT_AVAILABLE) if solar_altitude_series else NOT_AVAILABLE,
                    # AZIMUTH_NAME: getattr(solar_azimuth_series, angle_output_units, NOT_AVAILABLE) if solar_azimuth_series else NOT_AVAILABLE,
                    AZIMUTH_ORIGIN_NAME: (
                        solar_incidence_series.azimuth_origin
                        if solar_incidence_series
                        else NOT_AVAILABLE
                    ),
                    # SURFACE_ORIENTATION_NAME: getattr(surface_orientation, angle_output_units, NOT_AVAILABLE) if surface_orientation else None,
                    # SURFACE_TILT_NAME: getattr(surface_tilt, angle_output_units, NOT_AVAILABLE) if surface_tilt else None,
                    INCIDENCE_ALGORITHM_NAME: (
                        solar_incidence_series.incidence_algorithm
                        if solar_incidence_series
                        else NOT_AVAILABLE
                    ),
                    INCIDENCE_NAME: (
                        getattr(
                            solar_incidence_series, angle_output_units, NOT_AVAILABLE
                        )
                        if solar_incidence_series
                        else NOT_AVAILABLE
                    ),
                    INCIDENCE_DEFINITION: (
                        solar_incidence_series.definition
                        if solar_incidence_series
                        else NOT_AVAILABLE
                    ),
                    UNITS_COLUMN_NAME: angle_output_units,
                }
            }
        results = results | solar_incidence_model_series

    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    return results

model_solar_incidence_series

model_solar_incidence_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo | None = None,
    surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
    apply_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: float
    | None = REFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,
    solar_time_model: SolarTimeModel = milne,
    solar_position_model: SolarPositionModel = noaa,
    sun_horizon_position: List[
        SunHorizonPositionModel
    ] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_model: SolarIncidenceModel = iqbal,
    horizon_profile: DataArray | None = None,
    shading_model: ShadingModel = pvgis,
    complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
) -> SolarIncidence
Source code in pvgis/api/position/incidence.py
@log_function_call
@custom_cached
@validate_with_pydantic(ModelSolarIncidenceTimeSeriesInputModel)
def model_solar_incidence_series(
    longitude: Longitude,
    latitude: Latitude,
    timestamps: DatetimeIndex,
    timezone: ZoneInfo | None = None,
    surface_orientation: SurfaceOrientation = SURFACE_ORIENTATION_DEFAULT,
    surface_tilt: SurfaceTilt = SURFACE_TILT_DEFAULT,
    apply_atmospheric_refraction: bool = ATMOSPHERIC_REFRACTION_FLAG_DEFAULT,
    refracted_solar_zenith: (
        float | None
    ) = REFRACTED_SOLAR_ZENITH_ANGLE_DEFAULT,  # radians
    solar_time_model: SolarTimeModel = SolarTimeModel.milne,
    solar_position_model: SolarPositionModel = SolarPositionModel.noaa,
    sun_horizon_position: List[SunHorizonPositionModel] = SUN_HORIZON_POSITION_DEFAULT,
    solar_incidence_model: SolarIncidenceModel = SolarIncidenceModel.iqbal,
    horizon_profile: DataArray | None = None,
    shading_model: ShadingModel = ShadingModel.pvgis,
    complementary_incidence_angle: bool = COMPLEMENTARY_INCIDENCE_ANGLE_DEFAULT,
    zero_negative_solar_incidence_angle: bool = ZERO_NEGATIVE_INCIDENCE_ANGLE_DEFAULT,
    perigee_offset: float = PERIGEE_OFFSET,
    eccentricity_correction_factor: float = ECCENTRICITY_CORRECTION_FACTOR,
    dtype: str = DATA_TYPE_DEFAULT,
    array_backend: str = ARRAY_BACKEND_DEFAULT,
    validate_output: bool = VALIDATE_OUTPUT_DEFAULT,
    verbose: int = VERBOSE_LEVEL_DEFAULT,
    log: int = LOG_LEVEL_DEFAULT,
) -> SolarIncidence:
    """ """
    logger.debug(
        f"Executing solar positioning modelling function model_solar_incidence_series() for\n{timestamps}",
        alt=f"Executing [underline]solar positioning modelling[/underline] function model_solar_incidence_series() for\n{timestamps}",
    )
    solar_incidence_series = None
    surface_in_shade_series = model_surface_in_shade_series(
        horizon_profile=horizon_profile,
        longitude=longitude,
        latitude=latitude,
        timestamps=timestamps,
        timezone=timezone,
        solar_time_model=solar_time_model,
        solar_position_model=solar_position_model,
        shading_model=shading_model,
        apply_atmospheric_refraction=apply_atmospheric_refraction,
        refracted_solar_zenith=refracted_solar_zenith,
        perigee_offset=perigee_offset,
        eccentricity_correction_factor=eccentricity_correction_factor,
        dtype=dtype,
        array_backend=array_backend,
        verbose=verbose,
        log=log,
        validate_output=validate_output,
    )

    if solar_incidence_model.value == SolarIncidenceModel.jenco:
        # Update-Me ----------------------------------------------------------
        # Hofierka (2002) measures azimuth angles from East !
        # Convert the user-defined North-based surface orientation angle to East-based
        # surface_orientation_east_convention = SurfaceOrientation(
        #     value=convert_north_to_east_radians_convention(
        #         north_based_angle=surface_orientation
        #     ),
        #     unit=RADIANS,
        # )
        surface_orientation_south_convention = SurfaceOrientation(
            value=convert_north_to_south_radians_convention(
                north_based_angle=surface_orientation
            ),
            unit=RADIANS,
        )
        # And apparently, defined the complementary surface tilt angle too!
        # from math import pi
        # surface_tilt = SurfaceTilt(
        #         value=(pi/2 - surface_tilt.radians),
        #         unit=RADIANS,
        #         )
        # ---------------------------------------------------------- Update-Me
        solar_incidence_series = calculate_solar_incidence_series_jenco(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            # surface_orientation=surface_orientation,
            # surface_orientation=surface_orientation_east_convention,
            surface_orientation=surface_orientation_south_convention,
            surface_tilt=surface_tilt,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            surface_in_shade_series=surface_in_shade_series,
            complementary_incidence_angle=complementary_incidence_angle,
            zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
            perigee_offset=perigee_offset,
            eccentricity_correction_factor=eccentricity_correction_factor,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_incidence_model.value == SolarIncidenceModel.iqbal:
        # Iqbal (1983) measures azimuthal angles from South !
        surface_orientation_south_convention = SurfaceOrientation(
            value=convert_north_to_south_radians_convention(
                north_based_angle=surface_orientation
            ),
            unit=RADIANS,
        )
        solar_incidence_series = calculate_solar_incidence_series_iqbal(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            surface_orientation=surface_orientation_south_convention,
            surface_tilt=surface_tilt,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            sun_horizon_position=sun_horizon_position,
            surface_in_shade_series=surface_in_shade_series,
            complementary_incidence_angle=complementary_incidence_angle,
            zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
            dtype=dtype,
            array_backend=array_backend,
            validate_output=validate_output,
            verbose=verbose,
            log=log,
        )

    if solar_incidence_model.value == SolarIncidenceModel.hofierka:
        surface_orientation_south_convention = SurfaceOrientation(
            value=convert_north_to_south_radians_convention(
                north_based_angle=surface_orientation
            ),
            unit=RADIANS,
        )
        solar_incidence_series = calculate_solar_incidence_series_hofierka(
            longitude=longitude,
            latitude=latitude,
            timestamps=timestamps,
            timezone=timezone,
            # surface_orientation=surface_orientation,
            surface_orientation=surface_orientation_south_convention,
            surface_tilt=surface_tilt,
            apply_atmospheric_refraction=apply_atmospheric_refraction,
            complementary_incidence_angle=complementary_incidence_angle,
            zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )

    if solar_incidence_model.value == SolarIncidenceModel.pvlib:
        solar_incidence_series = calculate_solar_incidence_series_pvlib(
            longitude=longitude,
            latitude=latitude,
            surface_tilt=surface_tilt,
            surface_orientation=surface_orientation,
            timestamps=timestamps,
            complementary_incidence_angle=complementary_incidence_angle,
            zero_negative_solar_incidence_angle=zero_negative_solar_incidence_angle,
            dtype=dtype,
            array_backend=array_backend,
            verbose=verbose,
            log=log,
        )
    if verbose > DEBUG_AFTER_THIS_VERBOSITY_LEVEL:
        debug(locals())

    logger.debug(
        f"Returning solar incidence time series :\n{solar_incidence_series}",
        alt=f"Returning [yellow]solar incidence[/yellow] time series :\n{solar_incidence_series}",
    )
    return solar_incidence_series

models

Functions:

Name Description
select_models

Select models from an enum list.

validate_model

Check that one and only one model from an Enum class is selected

select_models

select_models(
    enum_type: Type[Enum], models: List[str]
) -> List[Enum]

Select models from an enum list.

Source code in pvgis/api/position/models.py
def select_models(enum_type: Type[Enum], models: List[str]) -> List[Enum]:
    """Select models from an enum list."""
    if enum_type.all in models:
        return [model for model in enum_type if model != enum_type.all]

    return [enum_type(model) for model in models]

validate_model

validate_model(
    enum_type: Type[Enum], model: List[Enum]
) -> Enum

Check that one and only one model from an Enum class is selected

Source code in pvgis/api/position/models.py
def validate_model(enum_type: Type[Enum], model: List[Enum]) -> Enum:
    """Check that one and only one model from an Enum class is selected"""
    if model == enum_type.all:  # or len(model) > 1: will not work! -- ReviewMe
        raise typer.BadParameter(
            "You can select only one model for [code]solar_time_model[/code]. Multiple or all are not a meaningful option."
        )

    return model