Skip to content

Solar Position

PVGIS features API functions for each and every elementary solar positioning angle. To start we launch an interactive i/python session

ipython

Where ?

Define the geographic location and the positioning of our solar surface

>>> latitude = 45.812
>>> longitude = 8.628
>>> surface_orientation = 177
>>> surface_tilt = 35

Native objects for coordinates

We can also use PVGIS' native classes Longitude and Latitude for the coordinates. Actually, we should ! We can import them as every other Python module and create objects

>>> from pvgisprototype import Longitude, Latitude

inspect them

>>> longitude = Longitude()
>>> dir(longitude)

and use them as in the following example

>>> from pvgisprototype import Longitude
>>> Longitude(value=8.628)
>>> longitude = Longitude(value=8.628)

Let's see what is in the longitude variable

>>> print(f"{longitude=}")

longitude=Longitude(data_source=None, equation=None, algorithm=None, unit=None, value=8.628, symbol='Λ', description="The angle between a point on the Earth's surface and the meridian plane, with its value ranging from 0° at the Prime Meridian in Greenwich, England, to 180° east or west.", label='Longitude', title='Longitude', supertitle='Longitude data', shortname='Longitude', name='Longitude', min_radians=-3.141592653589793, max_radians=3.141592653589793, min_degrees=-180, max_degrees=180)

Even better if we define the unit

>>> from pvgisprototype import Longitude
>>> Longitude(value=8.628)
>>> longitude = Longitude(value=8.628, unit='degrees')

Now the longitude value is unit-aware !

>>> print(f"{longitude}")

data_source=None equation=None algorithm=None unit='degrees' value=8.628 symbol='Λ' description="The angle between a point on the Earth's surface and the meridian plane, with its value ranging from 0° at the Prime Meridian in Greenwich, England, to 180° east or west." label='Longitude' title='Longitude' supertitle='Longitude data' shortname='Longitude' name='Longitude' min_radians=-3.141592653589793 max_radians=3.141592653589793 min_degrees=-180 max_degrees=180

We can even do

>>> print(f"{longitude.degrees=}")

longitude.degrees=8.628

or

>>> print(f"{longitude.radians=}")

longitude.radians=0.15058700786207074

When ?

Prepate a series of timestamps as a Pandas DatetimeIndex, using our helper function generate_datetime_series

>>> from pvgisprototype.api.datetime.datetimeindex import generate_datetime_series
>>> timestamps = generate_datetime_series(start_time="2010-01-27 08:00:00", end_time="2010-01-27 18:00:00", frequency="h")

and the default UTC timezone as a ZoneInfo object

>>> from zoneinfo import ZoneInfo
>>> utc_zone = ZoneInfo("UTC")

Let's confirm the generation of the timestamps and the timezone :

>>> print(f'{timestamps=}')
>>> print(f'\n{utc_zone=}')

timestamps=DatetimeIndex(['2010-01-27 08:00:00', '2010-01-27 09:00:00', '2010-01-27 10:00:00', '2010-01-27 11:00:00', '2010-01-27 12:00:00', '2010-01-27 13:00:00', '2010-01-27 14:00:00', '2010-01-27 15:00:00', '2010-01-27 16:00:00', '2010-01-27 17:00:00', '2010-01-27 18:00:00'], dtype='datetime64[ns]', freq='h')

utc_zone=zoneinfo.ZoneInfo(key='UTC')

Solar altitude

>>> from pvgisprototype.api.position.altitude import calculate_solar_altitude_series
>>> calculate_solar_altitude_series

We can see the required arguments to run the command

Error Handling

Let's give it a first try with some reasonable input values for longitude, latitude and timestamps plus timezone

Failure

Traceback (most recent call last):
  File "/builds/NikosAlexandris/pvgis-prototype/.venv/lib/python3.11/site-packages/markdown_exec/_internal/formatters/python.py", line 71, in _run_python
    exec_python(code, code_block_id, exec_globals)
  File "/builds/NikosAlexandris/pvgis-prototype/.venv/lib/python3.11/site-packages/markdown_exec/_internal/formatters/_exec_python.py", line 8, in exec_python
    exec(compiled, exec_globals)  # noqa: S102
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "<code block: session solar-altitude; n2>", line 1, in <module>
    calculate_solar_altitude_series(8, 45, '2001-01-01 10:00:00', 'UTC')
  File "/builds/NikosAlexandris/pvgis-prototype/.venv/lib/python3.11/site-packages/pvgisprototype/log.py", line 460, in wrapper
    return function(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/builds/NikosAlexandris/pvgis-prototype/.venv/lib/python3.11/site-packages/pvgisprototype/api/position/altitude.py", line 225, in calculate_solar_altitude_series
    solar_altitude_series = model_solar_altitude_series(
                            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/builds/NikosAlexandris/pvgis-prototype/.venv/lib/python3.11/site-packages/pvgisprototype/log.py", line 460, in wrapper
    return function(*args, **kwargs)
           ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/builds/NikosAlexandris/pvgis-prototype/.venv/lib/python3.11/site-packages/pvgisprototype/core/caching.py", line 215, in wrapper
    result = func(*args, **kwargs)
             ^^^^^^^^^^^^^^^^^^^^^
  File "/builds/NikosAlexandris/pvgis-prototype/.venv/lib/python3.11/site-packages/pvgisprototype/validation/functions.py", line 93, in wrapper
    validated_input = input_model(**input_data)
                      ^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/builds/NikosAlexandris/pvgis-prototype/.venv/lib/python3.11/site-packages/pydantic/main.py", line 250, in __init__
    validated_self = self.__pydantic_validator__.validate_python(data, self_instance=self)
                     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
pydantic_core._pydantic_core.ValidationError: 6 validation errors for ModelSolarAltitudeTimeSeriesInputModel
timestamps.is-instance[DatetimeIndex]
  Input should be an instance of DatetimeIndex [type=is_instance_of, input_value='2001-01-01 10:00:00', input_type=str]
    For further information visit https://errors.pydantic.dev/2.12/v/is_instance_of
timestamps.is-instance[Timestamp]
  Input should be an instance of Timestamp [type=is_instance_of, input_value='2001-01-01 10:00:00', input_type=str]
    For further information visit https://errors.pydantic.dev/2.12/v/is_instance_of
latitude.constrained-float
  Input should be less than or equal to 1.5707963267948966 [type=less_than_equal, input_value=45, input_type=int]
    For further information visit https://errors.pydantic.dev/2.12/v/less_than_equal
latitude.Latitude
  Input should be a valid dictionary or instance of Latitude [type=model_type, input_value=45, input_type=int]
    For further information visit https://errors.pydantic.dev/2.12/v/model_type
longitude.constrained-float
  Input should be less than or equal to 3.141592653589793 [type=less_than_equal, input_value=8, input_type=int]
    For further information visit https://errors.pydantic.dev/2.12/v/less_than_equal
longitude.Longitude
  Input should be a valid dictionary or instance of Longitude [type=model_type, input_value=8, input_type=int]
    For further information visit https://errors.pydantic.dev/2.12/v/model_type

PVGIS' API is indeed idiomatic and our reasonable inputs won't work ! However, the input arguments are validated via Pydantic and thus we receive informative error messages.

Speaking PVGIS' API language

Let's import the required modules

>>> from pandas import Timestamp
>>> from zoneinfo import ZoneInfo
>>> from pvgisprototype.api.utilities.conversions import convert_float_to_radians_if_requested

Setting the input parameters

>>> latitude=convert_float_to_radians_if_requested(8, 'radians')
>>> longitude=convert_float_to_radians_if_requested(45, 'radians')
>>> timestamps=Timestamp('2001-01-01 10:00:00+00:00')
>>> timezone=ZoneInfo('UTC')

And re-run the calculation

>>> solar_altitude = calculate_solar_altitude_series(longitude=longitude, latitude=latitude, timestamps=timestamps, timezone=timezone)
>>> print(f"Solar altitude from PVGIS' API : {solar_altitude}")
Solar altitude from PVGIS' API : {'noaa': OrderedDict([('Core', OrderedDict([('Name', 'Solar Altitude'), ('Title', 'Solar Altitude'), ('Description', 'Solar altitude data for a location and period in time'), ('Symbol', '⦩'), ('Altitude ⦩', array([0.97663325], dtype=float32)), ('refracted_value', array([], dtype=float64)), ('Unit', 'radians')])), ('Context', {}), ('Metadata', {}), ('Out-of-range', {}), ('Sources', {}), ('References', {}), ('Fingerprint', {})])}

Solar Azimuth

We can calculate solar azimuth angles for a specific geographic location and over a time series with the API function calculate_solar_azimuth_series().

>>> from pvgisprototype.api.position.azimuth import calculate_solar_azimuth_series

Help

What exactly can we do with it ? We can use the Python built-in function help to find out

>>> help(calculate_solar_azimuth_series)

Or indeed, in an interactive ipython session, it suffices to use a ? right after the function name and without space

>>> calculate_solar_azimuth_series?

More Imports

>>> from pvgisprototype import Longitude, Latitude
>>> from pvgisprototype.api.datetime.datetimeindex import generate_datetime_series
>>> timestamps = generate_datetime_series(start_time='2010-01-27', end_time='2010-01-28')
>>> from zoneinfo import ZoneInfo

Calculate solar azimuth time series for the 27th January 2010

>>> longitude = Longitude(value=8.628, unit='degrees')
>>> latitude = Latitude(value=45.812, unit='degrees')
>>> calculate_solar_azimuth_series(
... longitude=longitude.radians,
... latitude=latitude.radians,
... timestamps=timestamps,
... timezone=ZoneInfo("UTC"),
... adjust_for_atmospheric_refraction=True
... )

The above function returns a Python dictionary with top-level key/s being the model or algorithm used for the calculation. Of course we can feed the result to a new variable and print or re-use it for further processing

>>> solar_azimuth_series = calculate_solar_azimuth_series(
... longitude=longitude.radians,
... latitude=latitude.radians,
... timestamps=timestamps,
... timezone=ZoneInfo("UTC"),
... adjust_for_atmospheric_refraction=True
... )
>>> print(f"{solar_azimuth_series=}")

solar_azimuth_series={'noaa': OrderedDict([('Core', OrderedDict([('Name', 'Solar Azimuth'), ('Title', 'Solar Azimuth'), ('Description', 'Solar azimuth angle data for a location and period in time'), ('Symbol', '\U000f19a5'), ('Azimuth \U000f19a5', array([0.20324755, 0.68162394, 1.0347457 , 1.2956891 , 1.5057368 , 1.6915469 , 1.8700204 , 2.0612502 , 2.250216 , 2.466558 , 2.7086406 , 2.9751859 , 3.2346225 , 3.50884 , 3.757659 , 3.9807858 , 4.1786404 , 4.3685203 , 4.547683 , 4.73073 , 4.933111 , 5.178298 , 5.503539 , 5.949113 , 0.19983101], dtype=float32)), ('Azimuth Origin \U000f19a5', 'North'), ('Unit', 'radians')])), ('Context', {}), ('Metadata', {}), ('Out-of-range', {}), ('Sources', {}), ('References', {}), ('Fingerprint', {})])}