Skip to content

API Reference

This page provides detailed documentation for all public functions and classes in Sunwhere.

Main Functions

sites

sites(
    times: ndarray[tuple[int], datetime64] | DatetimeIndex,
    latitude: ndarray[tuple[int]] | Sequence[float] | float,
    longitude: ndarray[tuple[int]]
    | Sequence[float]
    | float,
    algorithm: Literal["psa", "nrel", "iqbal"] = "psa",
    refraction: bool = True,
    engine: Literal["numexpr", "numpy"] = "numexpr",
    site_names: Sequence[str] | None = None,
) -> Sunpos

Compute solar position at arbitrarily dispersed locations over a common time series.

This function calculates solar position (zenith and azimuth angles, declination, etc.) for an arbitrary number of fixed geographic locations throughout a common time grid. All locations share the same time instants.

Parameters:

  • times

    (array-like of datetime-like, shape (K,)) –

    Time instants at which solar position is evaluated. Can be datetime.datetime, pandas.DatetimeIndex, numpy.datetime64, or any type accepted by pandas.to_datetime(). Timezone-naive inputs are assumed to be UTC.

  • latitude

    (float or array-like of float, shape (J,)) –

    Latitudes in degrees where solar position is evaluated. Valid range: [-90, 90]. Must have the same size as longitude.

  • longitude

    (float or array-like of float, shape (J,)) –

    Longitudes in degrees where solar position is evaluated. Valid range: [-180, 180). Must have the same size as latitude.

  • algorithm

    ((psa, nrel, iqbal), default: 'psa' ) –

    Solar position algorithm to use: - 'psa': Plataforma Solar de Almería algorithm [1] with updated coefficients [2]. Fast and accurate (avg error ~0.002°). Default. - 'nrel': NREL's SPA algorithm [3]. Most accurate (±0.0003°) but slower. Valid for years -2000 to 6000. - 'iqbal': Iqbal's algorithm [4]. Lower accuracy, mainly for educational use.

  • refraction

    (bool, default: True ) –

    If True (default), applies atmospheric refraction correction to zenith angle. Typically adds ~0.5° correction near the horizon.

  • engine

    ((numexpr, numpy), default: 'numexpr' ) –

    Computation engine. 'numexpr' (default) is faster for large arrays, 'numpy' may be faster for small arrays.

  • site_names

    (array-like of str, shape (J,), default: None ) –

    Custom names for each site. If provided, must have the same length as latitude and longitude. These names will be used as a coordinate for the 'site' dimension, enabling labeled selection like result.sza.sel(site='Madrid'). If None (default), sites are indexed numerically (0, 1, 2, ...).

Returns:

  • sunpos ( Sunpos ) –

    Object containing solar position data as xarray DataArrays with dimensions:

    • Time-dependent only (K,): dec, eot, ecf
    • Time and location dependent (K, J): sza, saa, elevation, azimuth, zenith, cosz
Notes
  • Latitude and longitude arrays must have the same length.
  • For a single location, use scalar values for latitude and longitude.
  • All times must be unique and monotonically increasing for best performance.
  • The refraction correction uses a simple model suitable for standard atmospheric conditions at sea level.

Examples:

Calculate solar position for three cities over one day:

>>> import pandas as pd
>>> import sunwhere
>>> 
>>> # Define locations: Madrid, New York, Tokyo
>>> cities = ['Madrid', 'New York', 'Tokyo']
>>> lats = [40.4168, 40.7128, 35.6762]
>>> lons = [-3.7038, -74.0060, 139.6503]
>>> 
>>> # Create hourly time series for 2024-06-21 (summer solstice)
>>> times = pd.date_range('2024-06-21', periods=24, freq='h', tz='UTC')
>>> 
>>> # Compute solar position with site names
>>> result = sunwhere.sites(times, lats, lons, site_names=cities)
>>> 
>>> # Access solar zenith angle with labeled coordinates
>>> print(result.sza)  # shape: (24, 3) with site names as coordinate
>>> 
>>> # Select data for a specific city using name
>>> madrid_sza = result.sza.sel(site='Madrid')
>>> 
>>> # Find solar noon (minimum zenith angle) for Madrid
>>> solar_noon_idx = result.sza.sel(site='Madrid').argmin()
>>> print(f"Solar noon in Madrid: {times[solar_noon_idx]}")

Single location example:

>>> # Single location (scalar coordinates)
>>> times = pd.date_range('2024-01-01', periods=365, freq='D', tz='UTC')
>>> result = sunwhere.sites(times, 40.0, -3.0, algorithm='nrel')
>>> 
>>> # Get solar azimuth angle
>>> azimuth = result.saa  # shape: (365, 1)
References
  1. Blanco-Muriel, M. et al., 2001. Computing the solar vector. Solar Energy, 70(5), 431-441. https://doi.org/10.1016/S0038-092X(00)00156-0

  2. Blanco, M. et al., 2020. Updating the PSA sun position algorithm. Solar Energy, 212, 339-341. https://doi.org/10.1016/j.solener.2020.10.084

  3. Reda, I. and Andreas, A., 2003. Solar Position Algorithm for Solar Radiation Applications. NREL Report No. TP-560-34302. https://www.nrel.gov/docs/fy08osti/34302.pdf

  4. Iqbal, M., 1983. An introduction to solar radiation. Academic Press.

See Also

regular_grid : Solar position over a regular lat-lon grid transect : Solar position along a moving path

Source code in src/sunwhere/usecases.py
def sites(
    times: np.ndarray[tuple[int], np.datetime64] | pd.DatetimeIndex,
    latitude: np.ndarray[tuple[int]] | Sequence[float] | float,
    longitude: np.ndarray[tuple[int]] | Sequence[float] | float,
    algorithm: Literal['psa', 'nrel', 'iqbal'] = 'psa',
    refraction: bool = True,
    engine: Literal['numexpr', 'numpy'] = 'numexpr',
    site_names: Sequence[str] | None = None,
) -> Sunpos:
    """
    Compute solar position at arbitrarily dispersed locations over a common time series.

    This function calculates solar position (zenith and azimuth angles, declination,
    etc.) for an arbitrary number of fixed geographic locations throughout a common
    time grid. All locations share the same time instants.

    Parameters
    ----------
    times : array-like of datetime-like, shape (K,)
        Time instants at which solar position is evaluated. Can be datetime.datetime,
        pandas.DatetimeIndex, numpy.datetime64, or any type accepted by
        pandas.to_datetime(). Timezone-naive inputs are assumed to be UTC.
    latitude : float or array-like of float, shape (J,)
        Latitudes in degrees where solar position is evaluated.
        Valid range: [-90, 90]. Must have the same size as longitude.
    longitude : float or array-like of float, shape (J,)
        Longitudes in degrees where solar position is evaluated.
        Valid range: [-180, 180). Must have the same size as latitude.
    algorithm : {'psa', 'nrel', 'iqbal'}, optional
        Solar position algorithm to use:
        - 'psa': Plataforma Solar de Almería algorithm [1] with updated
          coefficients [2]. Fast and accurate (avg error ~0.002°). Default.
        - 'nrel': NREL's SPA algorithm [3]. Most accurate (±0.0003°) but slower.
          Valid for years -2000 to 6000.
        - 'iqbal': Iqbal's algorithm [4]. Lower accuracy, mainly for educational use.
    refraction : bool, optional
        If True (default), applies atmospheric refraction correction to zenith angle.
        Typically adds ~0.5° correction near the horizon.
    engine : {'numexpr', 'numpy'}, optional
        Computation engine. 'numexpr' (default) is faster for large arrays,
        'numpy' may be faster for small arrays.
    site_names : array-like of str, shape (J,), optional
        Custom names for each site. If provided, must have the same length as
        latitude and longitude. These names will be used as a coordinate for the
        'site' dimension, enabling labeled selection like ``result.sza.sel(site='Madrid')``.
        If None (default), sites are indexed numerically (0, 1, 2, ...).

    Returns
    -------
    sunpos : Sunpos
        Object containing solar position data as xarray DataArrays with dimensions:

        - Time-dependent only (K,): dec, eot, ecf
        - Time and location dependent (K, J): sza, saa, elevation, azimuth, zenith, cosz

    Notes
    -----
    - Latitude and longitude arrays must have the same length.
    - For a single location, use scalar values for latitude and longitude.
    - All times must be unique and monotonically increasing for best performance.
    - The refraction correction uses a simple model suitable for standard atmospheric
      conditions at sea level.

    Examples
    --------
    Calculate solar position for three cities over one day:

    >>> import pandas as pd
    >>> import sunwhere
    >>> 
    >>> # Define locations: Madrid, New York, Tokyo
    >>> cities = ['Madrid', 'New York', 'Tokyo']
    >>> lats = [40.4168, 40.7128, 35.6762]
    >>> lons = [-3.7038, -74.0060, 139.6503]
    >>> 
    >>> # Create hourly time series for 2024-06-21 (summer solstice)
    >>> times = pd.date_range('2024-06-21', periods=24, freq='h', tz='UTC')
    >>> 
    >>> # Compute solar position with site names
    >>> result = sunwhere.sites(times, lats, lons, site_names=cities)
    >>> 
    >>> # Access solar zenith angle with labeled coordinates
    >>> print(result.sza)  # shape: (24, 3) with site names as coordinate
    >>> 
    >>> # Select data for a specific city using name
    >>> madrid_sza = result.sza.sel(site='Madrid')
    >>> 
    >>> # Find solar noon (minimum zenith angle) for Madrid
    >>> solar_noon_idx = result.sza.sel(site='Madrid').argmin()
    >>> print(f"Solar noon in Madrid: {times[solar_noon_idx]}")

    Single location example:

    >>> # Single location (scalar coordinates)
    >>> times = pd.date_range('2024-01-01', periods=365, freq='D', tz='UTC')
    >>> result = sunwhere.sites(times, 40.0, -3.0, algorithm='nrel')
    >>> 
    >>> # Get solar azimuth angle
    >>> azimuth = result.saa  # shape: (365, 1)

    References
    ----------
    1. Blanco-Muriel, M. et al., 2001. Computing the solar vector.
       Solar Energy, 70(5), 431-441.
       https://doi.org/10.1016/S0038-092X(00)00156-0

    2. Blanco, M. et al., 2020. Updating the PSA sun position algorithm.
       Solar Energy, 212, 339-341.
       https://doi.org/10.1016/j.solener.2020.10.084

    3. Reda, I. and Andreas, A., 2003. Solar Position Algorithm for Solar
       Radiation Applications. NREL Report No. TP-560-34302.
       https://www.nrel.gov/docs/fy08osti/34302.pdf

    4. Iqbal, M., 1983. An introduction to solar radiation. Academic Press.

    See Also
    --------
    regular_grid : Solar position over a regular lat-lon grid
    transect : Solar position along a moving path
    """
    # I use ndmin=1 to allow scalar inputs
    #   the argument `utc` of pd.to_datetime "localizes" timezone-naive
    #   inputs as UTC, while timezone-aware inputs are "converted" to UTC
    times_utc = np.array(
        pd.to_datetime(times, utc=True).tz_localize(None),  # naive datetime
        ndmin=1, dtype='datetime64[ns]')
    sites_lats = np.array(latitude, ndmin=1, dtype=np.float64)
    sites_lons = np.array(longitude, ndmin=1, dtype=np.float64)

    check_dimensions(times_utc.ndim, sites_lats.ndim, sites_lons.ndim, (1, 1, 1))

    if sites_lats.shape != sites_lons.shape:
        raise ValueError(
            'shape mismatch: expected equal shape for latitude and '
            f'longitude, but got {sites_lats.shape} for latitude and '
            f'{sites_lons.shape} for longitude')

    # Validate site_names if provided
    if site_names is not None:
        site_names = np.array(site_names, ndmin=1, dtype=str)
        if site_names.ndim != 1:
            raise ValueError(
                f'expected 1-dim array for site_names, but got {site_names.ndim}-dim array')
        if site_names.shape[0] != sites_lats.shape[0]:
            raise ValueError(
                f'shape mismatch: site_names must have the same length as latitude and longitude, '
                f'but got {site_names.shape[0]} for site_names and {sites_lats.shape[0]} for latitude/longitude')

    # NOTE: ndim=1: two dimensional space: (n_times, n_locations)
    # NOTE: times_utc is a naive datetime64[s]

    solpos = evaluate(times_utc, sites_lats, sites_lons, algorithm,
                      ndim=1, refraction=refraction, engine=engine)

    return Sunpos(
        times=times,
        latitude=sites_lats,
        longitude=sites_lons,
        algorithm=algorithm,
        engine=engine,
        refraction=refraction,
        usecase='sites',
        site_names=site_names,
        ecf=solpos.get('ecf'),
        eot=solpos.get('eot'),
        declination=solpos.get('declination'),
        zenith=solpos.get('zenith'),
        azimuth=solpos.get('azimuth')
    )

regular_grid

regular_grid(
    times: ndarray[tuple[int], datetime64] | DatetimeIndex,
    latitude: ndarray[tuple[int]] | Sequence[float] | float,
    longitude: ndarray[tuple[int]]
    | Sequence[float]
    | float,
    algorithm: Literal["psa", "nrel", "iqbal"] = "psa",
    refraction: bool = True,
    engine: Literal["numexpr", "numpy"] = "numexpr",
) -> Sunpos

Compute solar position over a regular latitude-longitude grid.

This function calculates solar position for a regular 2D grid of latitudes and longitudes throughout a common time series. The output has shape (time, latitude, longitude), suitable for gridded climate data or maps.

Parameters:

  • times

    (array-like of datetime-like, shape (K,)) –

    Time instants at which solar position is evaluated. Can be datetime.datetime, pandas.DatetimeIndex, numpy.datetime64, or any type accepted by pandas.to_datetime(). Timezone-naive inputs are assumed to be UTC.

  • latitude

    (float or array-like of float, shape (J,)) –

    1-D array of latitudes in degrees defining the grid rows. Valid range: [-90, 90]. Can be a scalar for a single latitude.

  • longitude

    (float or array-like of float, shape (I,)) –

    1-D array of longitudes in degrees defining the grid columns. Valid range: [-180, 180). Can be a scalar for a single longitude.

  • algorithm

    ((psa, nrel, iqbal), default: 'psa' ) –

    Solar position algorithm to use:

    • 'psa': Plataforma Solar de Almería algorithm [2] with updated coefficients [3]. Fast and accurate (avg error ~0.002°). Default.
    • 'nrel': NREL's SPA algorithm [1]. Most accurate (±0.0003°) but slower. Valid for years -2000 to 6000.
    • 'iqbal': Iqbal's algorithm [4]. Lower accuracy, mainly for educational use.
  • refraction

    (bool, default: True ) –

    If True (default), applies atmospheric refraction correction to zenith angle. Typically adds ~0.5° correction near the horizon.

  • engine

    ((numexpr, numpy), default: 'numexpr' ) –

    Computation engine. 'numexpr' (default) is significantly faster for large grids (>1000 points), 'numpy' may be faster for small grids.

Returns:

  • sunpos ( Sunpos ) –

    Object containing solar position data as xarray DataArrays with dimensions:

    • Time-dependent only (K,): dec, eot, ecf
    • Time and space dependent (K, J, I): sza, saa, elevation, azimuth, zenith, cosz
Notes
  • This function is optimized for regular grids where latitudes and longitudes form a rectangular mesh.
  • For irregular or scattered points, use sites() instead.
  • The grid is created using NumPy broadcasting: all combinations of lat×lon are computed for each time instant.
  • Memory usage scales as K × J × I × 8 bytes per output variable.

Examples:

Create a global grid and compute solar zenith angle:

>>> import numpy as np
>>> import pandas as pd
>>> import sunwhere
>>> 
>>> # Define a 5° resolution global grid
>>> lats = np.arange(-90, 91, 5.0)
>>> lons = np.arange(-180, 180, 5.0)
>>> 
>>> # One day at noon UTC
>>> times = pd.date_range('2024-06-21 12:00', periods=1, freq='h', tz='UTC')
>>> 
>>> # Compute solar position
>>> result = sunwhere.regular_grid(times, lats, lons)
>>> 
>>> # Solar zenith angle has shape (1, 37, 72)
>>> print(result.sza.shape)
(1, 37, 72)
>>> 
>>> # Create a map of solar zenith angle
>>> import matplotlib.pyplot as plt
>>> result.sza[0].plot(x='lon', y='lat')
>>> plt.title('Solar Zenith Angle at Summer Solstice Noon UTC')
>>> plt.show()

High-resolution regional grid:

>>> # European domain
>>> lats = np.linspace(35, 70, 141)  # 0.25° resolution
>>> lons = np.linspace(-10, 40, 201)
>>> 
>>> # Full year, daily resolution
>>> times = pd.date_range('2024-01-01', '2024-12-31', freq='D', tz='UTC')
>>> 
>>> # Use NREL algorithm for highest accuracy
>>> result = sunwhere.regular_grid(times, lats, lons, algorithm='nrel')
>>> 
>>> # Compute average zenith angle over the year
>>> annual_mean_sza = result.sza.mean(dim='time')
References
  1. Reda, I. and Andreas, A., 2003. Solar Position Algorithm for Solar Radiation Applications. NREL Report No. TP-560-34302. https://www.nrel.gov/docs/fy08osti/34302.pdf
  2. Blanco-Muriel, M. et al., 2001. Computing the solar vector. Solar Energy, 70(5), 431-441. https://doi.org/10.1016/S0038-092X(00)00156-0
  3. Blanco, M. et al., 2020. Updating the PSA sun position algorithm. Solar Energy, 212, 339-341. https://doi.org/10.1016/j.solener.2020.10.084
  4. Iqbal, M., 1983. An introduction to solar radiation. Academic Press.
See Also

sites : Solar position at multiple fixed locations transect : Solar position along a moving path

Source code in src/sunwhere/usecases.py
def regular_grid(
    times: np.ndarray[tuple[int], np.datetime64] | pd.DatetimeIndex,
    latitude: np.ndarray[tuple[int]] | Sequence[float] | float,
    longitude: np.ndarray[tuple[int]] | Sequence[float] | float,
    algorithm: Literal['psa', 'nrel', 'iqbal'] = 'psa',
    refraction: bool = True,
    engine: Literal['numexpr', 'numpy'] = 'numexpr',
) -> Sunpos:
    """
    Compute solar position over a regular latitude-longitude grid.

    This function calculates solar position for a regular 2D grid of latitudes
    and longitudes throughout a common time series. The output has shape
    (time, latitude, longitude), suitable for gridded climate data or maps.

    Parameters
    ----------
    times : array-like of datetime-like, shape (K,)
        Time instants at which solar position is evaluated. Can be datetime.datetime,
        pandas.DatetimeIndex, numpy.datetime64, or any type accepted by
        pandas.to_datetime(). Timezone-naive inputs are assumed to be UTC.
    latitude : float or array-like of float, shape (J,)
        1-D array of latitudes in degrees defining the grid rows.
        Valid range: [-90, 90]. Can be a scalar for a single latitude.
    longitude : float or array-like of float, shape (I,)
        1-D array of longitudes in degrees defining the grid columns.
        Valid range: [-180, 180). Can be a scalar for a single longitude.
    algorithm : {'psa', 'nrel', 'iqbal'}, optional
        Solar position algorithm to use:

        - 'psa': Plataforma Solar de Almería algorithm [2] with updated
          coefficients [3]. Fast and accurate (avg error ~0.002°). Default.
        - 'nrel': NREL's SPA algorithm [1]. Most accurate (±0.0003°) but slower.
          Valid for years -2000 to 6000.
        - 'iqbal': Iqbal's algorithm [4]. Lower accuracy, mainly for educational use.
    refraction : bool, optional
        If True (default), applies atmospheric refraction correction to zenith angle.
        Typically adds ~0.5° correction near the horizon.
    engine : {'numexpr', 'numpy'}, optional
        Computation engine. 'numexpr' (default) is significantly faster for large
        grids (>1000 points), 'numpy' may be faster for small grids.

    Returns
    -------
    sunpos : Sunpos
        Object containing solar position data as xarray DataArrays with dimensions:

        - Time-dependent only (K,): dec, eot, ecf
        - Time and space dependent (K, J, I): sza, saa, elevation, azimuth, zenith, cosz

    Notes
    -----
    - This function is optimized for regular grids where latitudes and longitudes
      form a rectangular mesh.
    - For irregular or scattered points, use `sites()` instead.
    - The grid is created using NumPy broadcasting: all combinations of lat×lon
      are computed for each time instant.
    - Memory usage scales as K × J × I × 8 bytes per output variable.

    Examples
    --------
    Create a global grid and compute solar zenith angle:

    >>> import numpy as np
    >>> import pandas as pd
    >>> import sunwhere
    >>> 
    >>> # Define a 5° resolution global grid
    >>> lats = np.arange(-90, 91, 5.0)
    >>> lons = np.arange(-180, 180, 5.0)
    >>> 
    >>> # One day at noon UTC
    >>> times = pd.date_range('2024-06-21 12:00', periods=1, freq='h', tz='UTC')
    >>> 
    >>> # Compute solar position
    >>> result = sunwhere.regular_grid(times, lats, lons)
    >>> 
    >>> # Solar zenith angle has shape (1, 37, 72)
    >>> print(result.sza.shape)
    (1, 37, 72)
    >>> 
    >>> # Create a map of solar zenith angle
    >>> import matplotlib.pyplot as plt
    >>> result.sza[0].plot(x='lon', y='lat')
    >>> plt.title('Solar Zenith Angle at Summer Solstice Noon UTC')
    >>> plt.show()

    High-resolution regional grid:

    >>> # European domain
    >>> lats = np.linspace(35, 70, 141)  # 0.25° resolution
    >>> lons = np.linspace(-10, 40, 201)
    >>> 
    >>> # Full year, daily resolution
    >>> times = pd.date_range('2024-01-01', '2024-12-31', freq='D', tz='UTC')
    >>> 
    >>> # Use NREL algorithm for highest accuracy
    >>> result = sunwhere.regular_grid(times, lats, lons, algorithm='nrel')
    >>> 
    >>> # Compute average zenith angle over the year
    >>> annual_mean_sza = result.sza.mean(dim='time')

    References
    ----------
    1. Reda, I. and Andreas, A., 2003. Solar Position Algorithm for Solar
       Radiation Applications. NREL Report No. TP-560-34302.
       https://www.nrel.gov/docs/fy08osti/34302.pdf
    2. Blanco-Muriel, M. et al., 2001. Computing the solar vector.
       Solar Energy, 70(5), 431-441.
       https://doi.org/10.1016/S0038-092X(00)00156-0
    3. Blanco, M. et al., 2020. Updating the PSA sun position algorithm.
       Solar Energy, 212, 339-341.
       https://doi.org/10.1016/j.solener.2020.10.084
    4. Iqbal, M., 1983. An introduction to solar radiation. Academic Press.

    See Also
    --------
    sites : Solar position at multiple fixed locations
    transect : Solar position along a moving path
    """
    # I use ndmin=1 to allow scalar input times
    #   the argument `utc` of pd.to_datetime "localizes" timezone-naive
    #   inputs as UTC, while timezone-aware inputs are "converted" to UTC
    times_utc = np.array(
        pd.to_datetime(times, utc=True).tz_localize(None),  # naive datetime
        ndmin=1, dtype='datetime64[s]')
    grid_lats = np.array(latitude, ndmin=1, dtype=np.float64)
    grid_lons = np.array(longitude, ndmin=1, dtype=np.float64)

    check_dimensions(times_utc.ndim, grid_lats.ndim, grid_lons.ndim, (1, 1, 1))

    # ndim=2: three dimensional space: (n_times, n_lats, n_lons)

    solpos = evaluate(times, grid_lats, grid_lons, algorithm,
                      ndim=2, refraction=refraction, engine=engine)

    return Sunpos(
        times=times,
        latitude=grid_lats,
        longitude=grid_lons,
        algorithm=algorithm,
        engine=engine,
        refraction=refraction,
        usecase='regular_grid',
        ecf=solpos.get('ecf'),
        eot=solpos.get('eot'),
        declination=solpos.get('declination'),
        zenith=solpos.get('zenith'),
        azimuth=solpos.get('azimuth')
    )

transect

transect(
    times: ndarray[tuple[int], datetime64] | DatetimeIndex,
    latitude: ndarray[tuple[int]] | Sequence[float] | float,
    longitude: ndarray[tuple[int]]
    | Sequence[float]
    | float,
    algorithm: Literal["psa", "nrel", "iqbal"] = "psa",
    refraction: bool = True,
    engine: Literal["numexpr", "numpy"] = "numexpr",
) -> Sunpos

Compute solar position along a moving path (transect).

This function calculates solar position for a trajectory where each time instant corresponds to a different geographic location. The output has shape (time,), suitable for moving observers like satellites, aircraft, or ships.

Parameters:

  • times

    (array-like of datetime-like, shape (K,)) –

    Time instants at which solar position is evaluated. Can be datetime.datetime, pandas.DatetimeIndex, numpy.datetime64, or any type accepted by pandas.to_datetime(). Timezone-naive inputs are assumed to be UTC.

  • latitude

    (float or array-like of float, shape (K,)) –

    Latitude in degrees for each time instant. Valid range: [-90, 90]. If scalar, the same latitude is used for all times (meridional transect).

  • longitude

    (float or array-like of float, shape (K,)) –

    Longitude in degrees for each time instant. Valid range: [-180, 180). If scalar, the same longitude is used for all times (zonal transect).

  • algorithm

    ((psa, nrel, iqbal), default: 'psa' ) –

    Solar position algorithm to use: - 'psa': Plataforma Solar de Almería algorithm [2] with updated coefficients [3]. Fast and accurate (avg error ~0.002°). Default. - 'nrel': NREL's SPA algorithm [1]. Most accurate (±0.0003°) but slower. Valid for years -2000 to 6000. - 'iqbal': Iqbal's algorithm [4]. Lower accuracy, mainly for educational use.

  • refraction

    (bool, default: True ) –

    If True (default), applies atmospheric refraction correction to zenith angle. Typically adds ~0.5° correction near the horizon.

  • engine

    ((numexpr, numpy), default: 'numexpr' ) –

    Computation engine. 'numexpr' (default) is typically faster for long transects (>100 points), 'numpy' may be faster for short transects.

Returns:

  • sunpos ( Sunpos ) –

    Object containing solar position data as xarray DataArrays with dimension (K,) for all variables: dec, eot, ecf, sza, saa, elevation, azimuth, zenith, cosz.

Notes
  • This function is designed for scenarios where position changes with time, such as satellite ground tracks, flight paths, or ship routes.
  • If latitude and longitude are both scalars, all times share the same location (equivalent to a single site with varying times).
  • For multiple fixed locations, use sites() instead for better performance.
  • The time array and position arrays must have the same length if both are arrays.

Examples:

Satellite ground track over one orbit:

>>> import numpy as np
>>> import pandas as pd
>>> import sunwhere
>>> 
>>> # ISS-like orbit: ~90 minute period
>>> times = pd.date_range('2024-01-15 00:00', periods=100, freq='54s', tz='UTC')
>>> 
>>> # Simplified sinusoidal ground track (not physically accurate)
>>> lats = 51.6 * np.sin(2 * np.pi * np.arange(100) / 100)
>>> lons = np.linspace(-180, 180, 100, endpoint=False)
>>> 
>>> # Compute solar position along the track
>>> result = sunwhere.transect(times, lats, lons)
>>> 
>>> # Check when satellite is in sunlight (elevation > 0)
>>> in_sunlight = result.elevation > 0
>>> print(f"Sunlit for {in_sunlight.sum().item()} of {len(times)} points")

Aircraft flight path from New York to Tokyo:

>>> # Great circle approximation (10-hour flight)
>>> times = pd.date_range('2024-03-20 10:00', periods=121, freq='5min', tz='UTC')
>>> 
>>> # Linear interpolation (simplified - not actual great circle)
>>> lats = np.linspace(40.7, 35.7, 121)  # NYC to Tokyo
>>> lons = np.linspace(-74.0, 139.7, 121)
>>> 
>>> # Compute solar position
>>> result = sunwhere.transect(times, lats, lons, algorithm='nrel')
>>> 
>>> # Find solar elevation throughout flight
>>> import matplotlib.pyplot as plt
>>> result.elevation.plot()
>>> plt.axhline(0, color='k', linestyle='--', label='Horizon')
>>> plt.ylabel('Solar Elevation (degrees)')
>>> plt.title('Sun Position During NYC-Tokyo Flight')
>>> plt.legend()
>>> plt.show()

Meridional transect at fixed longitude:

>>> # Moving north along Prime Meridian
>>> times = pd.date_range('2024-06-21 12:00', periods=181, freq='h', tz='UTC')
>>> lats = np.linspace(-90, 90, 181)  # South Pole to North Pole
>>> lon = 0.0  # Prime Meridian (scalar)
>>> 
>>> result = sunwhere.transect(times, lats, lon)
>>> # Solar zenith angle variation from pole to pole
>>> result.sza.plot()
References
  1. Reda, I. and Andreas, A., 2003. Solar Position Algorithm for Solar Radiation Applications. NREL Report No. TP-560-34302. https://www.nrel.gov/docs/fy08osti/34302.pdf
  2. Blanco-Muriel, M. et al., 2001. Computing the solar vector. Solar Energy, 70(5), 431-441. https://doi.org/10.1016/S0038-092X(00)00156-0
  3. Blanco, M. et al., 2020. Updating the PSA sun position algorithm. Solar Energy, 212, 339-341. https://doi.org/10.1016/j.solener.2020.10.084
  4. Iqbal, M., 1983. An introduction to solar radiation. Academic Press.
See Also

sites : Solar position at multiple fixed locations regular_grid : Solar position over a latitude-longitude grid

Source code in src/sunwhere/usecases.py
def transect(
    times: np.ndarray[tuple[int], np.datetime64] | pd.DatetimeIndex,
    latitude: np.ndarray[tuple[int]] | Sequence[float] | float,
    longitude: np.ndarray[tuple[int]] | Sequence[float] | float,
    algorithm: Literal['psa', 'nrel', 'iqbal'] = 'psa',
    refraction: bool = True,
    engine: Literal['numexpr', 'numpy'] = 'numexpr',
) -> Sunpos:
    """
    Compute solar position along a moving path (transect).

    This function calculates solar position for a trajectory where each time
    instant corresponds to a different geographic location. The output has shape
    (time,), suitable for moving observers like satellites, aircraft, or ships.

    Parameters
    ----------
    times : array-like of datetime-like, shape (K,)
        Time instants at which solar position is evaluated. Can be datetime.datetime,
        pandas.DatetimeIndex, numpy.datetime64, or any type accepted by
        pandas.to_datetime(). Timezone-naive inputs are assumed to be UTC.
    latitude : float or array-like of float, shape (K,)
        Latitude in degrees for each time instant. Valid range: [-90, 90].
        If scalar, the same latitude is used for all times (meridional transect).
    longitude : float or array-like of float, shape (K,)
        Longitude in degrees for each time instant. Valid range: [-180, 180).
        If scalar, the same longitude is used for all times (zonal transect).
    algorithm : {'psa', 'nrel', 'iqbal'}, optional
        Solar position algorithm to use:
        - 'psa': Plataforma Solar de Almería algorithm [2] with updated
          coefficients [3]. Fast and accurate (avg error ~0.002°). Default.
        - 'nrel': NREL's SPA algorithm [1]. Most accurate (±0.0003°) but slower.
          Valid for years -2000 to 6000.
        - 'iqbal': Iqbal's algorithm [4]. Lower accuracy, mainly for educational use.
    refraction : bool, optional
        If True (default), applies atmospheric refraction correction to zenith angle.
        Typically adds ~0.5° correction near the horizon.
    engine : {'numexpr', 'numpy'}, optional
        Computation engine. 'numexpr' (default) is typically faster for long
        transects (>100 points), 'numpy' may be faster for short transects.

    Returns
    -------
    sunpos : Sunpos
        Object containing solar position data as xarray DataArrays with dimension
        (K,) for all variables: dec, eot, ecf, sza, saa, elevation, azimuth, 
        zenith, cosz.

    Notes
    -----
    - This function is designed for scenarios where position changes with time,
      such as satellite ground tracks, flight paths, or ship routes.
    - If latitude and longitude are both scalars, all times share the same location
      (equivalent to a single site with varying times).
    - For multiple fixed locations, use `sites()` instead for better performance.
    - The time array and position arrays must have the same length if both are arrays.

    Examples
    --------
    Satellite ground track over one orbit:

    >>> import numpy as np
    >>> import pandas as pd
    >>> import sunwhere
    >>> 
    >>> # ISS-like orbit: ~90 minute period
    >>> times = pd.date_range('2024-01-15 00:00', periods=100, freq='54s', tz='UTC')
    >>> 
    >>> # Simplified sinusoidal ground track (not physically accurate)
    >>> lats = 51.6 * np.sin(2 * np.pi * np.arange(100) / 100)
    >>> lons = np.linspace(-180, 180, 100, endpoint=False)
    >>> 
    >>> # Compute solar position along the track
    >>> result = sunwhere.transect(times, lats, lons)
    >>> 
    >>> # Check when satellite is in sunlight (elevation > 0)
    >>> in_sunlight = result.elevation > 0
    >>> print(f"Sunlit for {in_sunlight.sum().item()} of {len(times)} points")

    Aircraft flight path from New York to Tokyo:

    >>> # Great circle approximation (10-hour flight)
    >>> times = pd.date_range('2024-03-20 10:00', periods=121, freq='5min', tz='UTC')
    >>> 
    >>> # Linear interpolation (simplified - not actual great circle)
    >>> lats = np.linspace(40.7, 35.7, 121)  # NYC to Tokyo
    >>> lons = np.linspace(-74.0, 139.7, 121)
    >>> 
    >>> # Compute solar position
    >>> result = sunwhere.transect(times, lats, lons, algorithm='nrel')
    >>> 
    >>> # Find solar elevation throughout flight
    >>> import matplotlib.pyplot as plt
    >>> result.elevation.plot()
    >>> plt.axhline(0, color='k', linestyle='--', label='Horizon')
    >>> plt.ylabel('Solar Elevation (degrees)')
    >>> plt.title('Sun Position During NYC-Tokyo Flight')
    >>> plt.legend()
    >>> plt.show()

    Meridional transect at fixed longitude:

    >>> # Moving north along Prime Meridian
    >>> times = pd.date_range('2024-06-21 12:00', periods=181, freq='h', tz='UTC')
    >>> lats = np.linspace(-90, 90, 181)  # South Pole to North Pole
    >>> lon = 0.0  # Prime Meridian (scalar)
    >>> 
    >>> result = sunwhere.transect(times, lats, lon)
    >>> # Solar zenith angle variation from pole to pole
    >>> result.sza.plot()

    References
    ----------
    1. Reda, I. and Andreas, A., 2003. Solar Position Algorithm for Solar
       Radiation Applications. NREL Report No. TP-560-34302.
       https://www.nrel.gov/docs/fy08osti/34302.pdf
    2. Blanco-Muriel, M. et al., 2001. Computing the solar vector.
       Solar Energy, 70(5), 431-441.
       https://doi.org/10.1016/S0038-092X(00)00156-0
    3. Blanco, M. et al., 2020. Updating the PSA sun position algorithm.
       Solar Energy, 212, 339-341.
       https://doi.org/10.1016/j.solener.2020.10.084
    4. Iqbal, M., 1983. An introduction to solar radiation. Academic Press.

    See Also
    --------
    sites : Solar position at multiple fixed locations
    regular_grid : Solar position over a latitude-longitude grid
    """
    # I use ndmin=1 to allow scalar inputs
    #   the argument `utc` of pd.to_datetime "localizes" timezone-naive
    #   inputs as UTC, while timezone-aware inputs are "converted" to UTC
    times_utc = np.array(
        pd.to_datetime(times, utc=True).tz_localize(None),
        ndmin=1, dtype='datetime64[s]')
    transect_lats = np.array(latitude, ndmin=1, dtype=np.float64)
    transect_lons = np.array(longitude, ndmin=1, dtype=np.float64)

    check_dimensions(times_utc.ndim, transect_lats.ndim, transect_lons.ndim, (1, 1, 1))

    if not (transect_lats.shape == transect_lons.shape == times_utc.shape):
        raise ValueError(
            'shape mismatch: expected equal shape for times, latitude and '
            f'longitude, but got {times_utc.shape} for times, {transect_lats.shape} '
            f'for latitude and {transect_lons.shape} for longitude')

    # ndim=0: one dimensional space: (n_times,) == (n_lats,) == (n_lons,)

    solpos = evaluate(times, transect_lats, transect_lons, algorithm,
                      ndim=0, refraction=refraction, engine=engine)

    return Sunpos(
        times=times,
        latitude=transect_lats,
        longitude=transect_lons,
        algorithm=algorithm,
        engine=engine,
        refraction=refraction,
        usecase='transect',
        ecf=solpos.get('ecf'),
        eot=solpos.get('eot'),
        declination=solpos.get('declination'),
        zenith=solpos.get('zenith'),
        azimuth=solpos.get('azimuth')
    )

Output object

Sunpos

Container class for sunwhere outputs.

Provides access to the solar geometry parameters, generally as xarray DataArrays.

Parameters:

  • times

    (array-like of datetime-like) –

    Times where solar geometry is evaluated. Can be numpy datetime64 or any type convertible to pandas DatetimeIndex.

  • latitude

    (array-like of float) –

    Latitude of the locations where solar geometry is evaluated. Must be in the range [-90, 90].

  • longitude

    (array-like of float) –

    Longitude of the locations where solar geometry is evaluated. Must be in the range [-180, 180).

  • algorithm

    (str) –

    Solar position algorithm: 'nrel', 'psa', or 'iqbal'.

  • engine

    (str) –

    Code implementation: 'numpy' or 'numexpr'.

  • refraction

    (bool) –

    Whether atmospheric refraction is considered.

  • usecase

    (str) –

    Use case: 'sites', 'regular_grid', or 'transect'.

  • ecf

    (array-like of float, shape (n_times,)) –

    Sun-earth distance (eccentricity) correction factor.

  • eot

    (array-like of float, shape (n_times,)) –

    Equation of time, in minutes.

  • declination

    (array-like of float, shape (n_times,)) –

    Solar declination, in degrees.

  • zenith

    (ndarray) –

    Solar zenith angle, in degrees. Shape varies by use case: 1-D for transect, 2-D for sites, 3-D for regular_grid.

  • azimuth

    (ndarray) –

    Solar azimuth angle, in degrees. Shape varies by use case: 1-D for transect, 2-D for sites, 3-D for regular_grid.

  • site_names

    (array-like of str, default: None ) –

    Names for each site (only used for 'sites' usecase).

Raises:

  • ValueError

    If the inputs are not of the proper type or shape.

Methods:

  • airmass

    Atmosphere relative optical air mass.

  • daylight_length

    Daylight length, in hours.

  • eth

    Extraterrestrial horizontal solar irradiance.

  • incidence

    Cosine of the solar incidence angle.

  • sunrise

    Sunrise angle or time.

  • sunset

    Sunset angle or time.

Attributes:

  • algorithm

    Solar position algorithm.

  • azimuth

    Solar azimuth angle.

  • cosz

    Cosine of solar zenith angle.

  • dec

    Solar declination.

  • declination

    Solar declination.

  • ecf

    Sun-earth orbit's eccentricity correction factor.

  • elevation

    Solar elevation angle.

  • engine

    Calculation engine.

  • eot

    Equation of time.

  • has_refraction

    Whether solar zenith angle consideres atmospheric refraction.

  • latitude

    Latitudes where solar geometry is evaluated, degrees.

  • local_standard_time

    Local standard time.

  • longitude

    Longitudes.

  • saa

    Solar azimuth angle.

  • sza

    Solar zenith angle.

  • times

    Times where solar geometry is evaluated.

  • times_utc

    Universal coordinated times where solar geometry is evaluated.

  • timezone

    Timezone.

  • true_solar_time

    True solar time.

  • usecase

    Usage case.

  • zenith

    Solar zenith angle.

Source code in src/sunwhere/_base.py
def __init__(self, times, latitude, longitude,
             algorithm, engine, refraction, usecase,
             ecf, eot, declination, zenith, azimuth, site_names=None):
    """Creates a Sunpos instance.

    Provides access to the solar geometry parameters, generally as
    xarray DataArrays.

    Parameters
    ----------
    times : array-like of datetime-like
        Times where solar geometry is evaluated. Can be numpy datetime64 or
        any type convertible to pandas DatetimeIndex.
    latitude : array-like of float
        Latitude of the locations where solar geometry is evaluated.
        Must be in the range [-90, 90].
    longitude : array-like of float
        Longitude of the locations where solar geometry is evaluated.
        Must be in the range [-180, 180).
    algorithm : str
        Solar position algorithm: 'nrel', 'psa', or 'iqbal'.
    engine : str
        Code implementation: 'numpy' or 'numexpr'.
    refraction : bool
        Whether atmospheric refraction is considered.
    usecase : str
        Use case: 'sites', 'regular_grid', or 'transect'.
    ecf : array-like of float, shape (n_times,)
        Sun-earth distance (eccentricity) correction factor.
    eot : array-like of float, shape (n_times,)
        Equation of time, in minutes.
    declination : array-like of float, shape (n_times,)
        Solar declination, in degrees.
    zenith : ndarray
        Solar zenith angle, in degrees. Shape varies by use case:
        1-D for transect, 2-D for sites, 3-D for regular_grid.
    azimuth : ndarray
        Solar azimuth angle, in degrees. Shape varies by use case:
        1-D for transect, 2-D for sites, 3-D for regular_grid.
    site_names : array-like of str, optional
        Names for each site (only used for 'sites' usecase).

    Raises
    ------
    ValueError
        If the inputs are not of the proper type or shape.
    """

    # I need that self._times is timezone-aware and ndmin==1. To ensure
    # ndmin==1, I must convert to a numpy datetime64, but numpy complains
    # when dealing with timezone-aware times. Hence, first I use
    # pd.to_datetime to retain the timezone of the input times (if
    # timezone-aware) or assume UTC if the input times are naive. Then, I
    # convert to a numpy array of datetime64[ns] with ndmin=1, but de-localizing
    # the times to prevent numpy complains. Afterwards, I convert back to
    # pandas using again pd.to_datetime and localize
    _times = pd.to_datetime(times)
    self._timezone = UTC if _times.tzinfo is None else _times.tzinfo
    naive_np_times = np.array(_times.tz_localize(None), dtype='datetime64[ns]').reshape(-1)
    self._times = pd.to_datetime(naive_np_times).tz_localize(self._timezone, ambiguous='infer')

    self._times_utc = pd.to_datetime(self._times).tz_convert('UTC')

    self._latitude = np.array(latitude, ndmin=1)
    self._longitude = np.array(longitude, ndmin=1)

    if self._latitude.ndim != self._longitude.ndim != 1:
        raise ValueError('expected 1-dim latitude and longitude, but '
                         f'got {self._latitude.ndim}-dim latitude and '
                         f'{self._longitude.ndim}-dim longitude')

    if usecase not in ('sites', 'regular_grid', 'transect'):
        raise ValueError(f'unknown use case `{usecase}`')
    self._usecase = usecase
    self._site_names = site_names

    self._ecf = np.array(ecf)
    self._eot = np.array(eot)
    self._declination = np.array(declination)
    self._zenith = np.array(zenith)
    self._azimuth = np.array(azimuth)

    n_times = len(self._times)
    n_lats = self._latitude.size
    n_lons = self._longitude.size

    if self._usecase == 'sites':
        # sanity check
        if n_lats != n_lons:
            raise ValueError(
                f'internal error: for sites usecase, expected n_lats == n_lons, '
                f'but got {n_lats} != {n_lons}')
        if not (self._ecf.ndim == self._eot.ndim == self._declination.ndim == 1):
            raise ValueError(
                f'internal error: expected 1-dim arrays for ecf, eot, declination, '
                f'but got {self._ecf.ndim}, {self._eot.ndim}, {self._declination.ndim}')
        if not (self._ecf.shape == self._eot.shape == self._declination.shape == (n_times,)):
            raise ValueError(
                f'internal error: expected shape ({n_times},) for ecf, eot, declination, '
                f'but got {self._ecf.shape}, {self._eot.shape}, {self._declination.shape}')
        if not (self._zenith.ndim == self._azimuth.ndim == 2):
            raise ValueError(
                f'internal error: expected 2-dim arrays for zenith and azimuth, '
                f'but got {self._zenith.ndim}, {self._azimuth.ndim}')
        if not (self._zenith.shape == self._azimuth.shape == (n_times, n_lats)):
            raise ValueError(
                f'internal error: expected shape ({n_times}, {n_lats}) for zenith and azimuth, '
                f'but got {self._zenith.shape}, {self._azimuth.shape}')
        # xarray coordinates
        coords = {
            'time': np.array(self._times.tz_localize(None), dtype='datetime64[ns]'),
            'site': self._site_names if self._site_names is not None else range(n_lats),
            'lat': ('site', self._latitude),
            'lon': ('site', self._longitude)}
        # xarray DataArrays
        kwargs = {'coords': {k: coords[k] for k in ['site']}, 'dims': ['site']}
        self._latitude = xr.DataArray(self._latitude, name='lat', **kwargs)
        self._longitude = xr.DataArray(self._longitude, name='lon', **kwargs)
        kwargs = {'coords': {k: coords[k] for k in ['time']}, 'dims': ['time']}
        self._ecf = xr.DataArray(self._ecf, name='ecf', **kwargs)
        self._eot = xr.DataArray(self._eot, name='eot', **kwargs)
        self._declination = xr.DataArray(self._declination, name='declination', **kwargs)
        kwargs = {'coords': coords, 'dims': ['time', 'site']}
        self._zenith = xr.DataArray(self._zenith, name='zenith', **kwargs)
        self._azimuth = xr.DataArray(self._azimuth, name='azimuth', **kwargs)

    if self._usecase == 'regular_grid':
        # sanity check
        if not (self._ecf.ndim == self._eot.ndim == self._declination.ndim == 1):
            raise ValueError(
                f'internal error: expected 1-dim arrays for ecf, eot, declination, '
                f'but got {self._ecf.ndim}, {self._eot.ndim}, {self._declination.ndim}')
        if not (self._ecf.shape == self._eot.shape == self._declination.shape == (n_times,)):
            raise ValueError(
                f'internal error: expected shape ({n_times},) for ecf, eot, declination, '
                f'but got {self._ecf.shape}, {self._eot.shape}, {self._declination.shape}')
        if not (self._zenith.ndim == self._azimuth.ndim == 3):
            raise ValueError(
                f'internal error: expected 3-dim arrays for zenith and azimuth, '
                f'but got {self._zenith.ndim}, {self._azimuth.ndim}')
        if not (self._zenith.shape == self._azimuth.shape == (n_times, n_lats, n_lons)):
            raise ValueError(
                f'internal error: expected shape ({n_times}, {n_lats}, {n_lons}) for zenith and azimuth, '
                f'but got {self._zenith.shape}, {self._azimuth.shape}')
        # xarray coordinates
        coords = {
            'time': np.array(self._times.tz_localize(None), dtype='datetime64[ns]'),
            'lat': self._latitude, 'lon': self._longitude}
        # xarray DataArrays
        kwargs = {'coords': {k: coords[k] for k in ['lat']}, 'dims': ['lat']}
        self._latitude = xr.DataArray(self._latitude, name='latitude', **kwargs)
        kwargs = {'coords': {k: coords[k] for k in ['lon']}, 'dims': ['lon']}
        self._longitude = xr.DataArray(self._longitude, name='longitude', **kwargs)
        kwargs = {'coords': {k: coords[k] for k in ['time']}, 'dims': ['time']}
        self._ecf = xr.DataArray(self._ecf, name='ecf', **kwargs)
        self._eot = xr.DataArray(self._eot, name='eot', **kwargs)
        self._declination = xr.DataArray(self._declination, name='declination', **kwargs)
        kwargs = {'coords': coords, 'dims': ['time', 'lat', 'lon']}
        self._zenith = xr.DataArray(self._zenith, name='zenith', **kwargs)
        self._azimuth = xr.DataArray(self._azimuth, name='azimuth', **kwargs)

    if self._usecase == 'transect':
        # sanity check
        if not (n_times == n_lats == n_lons):
            raise ValueError(
                f'internal error: for transect usecase, expected n_times == n_lats == n_lons, '
                f'but got {n_times}, {n_lats}, {n_lons}')
        if not (self._ecf.ndim == self._eot.ndim == self._declination.ndim == 1):
            raise ValueError(
                f'internal error: expected 1-dim arrays for ecf, eot, declination, '
                f'but got {self._ecf.ndim}, {self._eot.ndim}, {self._declination.ndim}')
        if not (self._ecf.shape == self._eot.shape == self._declination.shape == (n_times,)):
            raise ValueError(
                f'internal error: expected shape ({n_times},) for ecf, eot, declination, '
                f'but got {self._ecf.shape}, {self._eot.shape}, {self._declination.shape}')
        if not (self._zenith.ndim == self._azimuth.ndim == 1):
            raise ValueError(
                f'internal error: expected 1-dim arrays for zenith and azimuth, '
                f'but got {self._zenith.ndim}, {self._azimuth.ndim}')
        if not (self._zenith.shape == self._azimuth.shape == (n_times,)):
            raise ValueError(
                f'internal error: expected shape ({n_times},) for zenith and azimuth, '
                f'but got {self._zenith.shape}, {self._azimuth.shape}')
        # xarray coordinates
        coords = {
            'time': np.array(self._times.tz_localize(None), dtype='datetime64[ns]'),
            'lat': ('time', self._latitude),
            'lon': ('time', self._longitude)}
        # xarray DataArrays
        kwargs = {'coords': coords, 'dims': ['time']}
        self._latitude = xr.DataArray(self._latitude, name='lat', **kwargs)
        self._longitude = xr.DataArray(self._longitude, name='lon', **kwargs)
        self._ecf = xr.DataArray(self._ecf, name='ecf', **kwargs)
        self._eot = xr.DataArray(self._eot, name='eot', **kwargs)
        self._declination = xr.DataArray(self._declination, name='declination', **kwargs)
        self._zenith = xr.DataArray(self._zenith, name='zenith', **kwargs)
        self._azimuth = xr.DataArray(self._azimuth, name='azimuth', **kwargs)

    self._algorithm = algorithm
    self._engine = engine
    self._refraction = refraction

algorithm property

algorithm

Solar position algorithm.

Returns:

  • str ( solar position algorithm. ) –

azimuth property

azimuth

Solar azimuth angle.

Returns:

  • DataArray

    Solar azimuth angle, in degrees [-180°, 180°], zero south.

cosz property

cosz

Cosine of solar zenith angle.

Returns:

  • DataArray

    Cosine of solar zenith angle.

dec property

dec

Solar declination.

Returns:

  • DataArray

    Solar declination, in degrees. Alias for declination.

declination property

declination

Solar declination.

Returns:

  • xarray.DataArray:

    Solar declination, in degrees.

ecf property

ecf

Sun-earth orbit's eccentricity correction factor.

Returns:

  • xarray.DataArray:

    Sun-earth distance correction factor.

elevation property

elevation

Solar elevation angle.

Returns:

  • DataArray

    Solar elevation angle, in degrees [-90, 90].

engine property

engine

Calculation engine.

Returns:

  • str ( calculation engine. ) –

eot property

eot

Equation of time.

Returns:

  • xarray.DataArray:

    Equation of time, in minutes.

has_refraction property

has_refraction

Whether solar zenith angle consideres atmospheric refraction.

Returns:

  • bool ( True if refraction is considered, False otherwise. ) –

latitude property

latitude

Latitudes where solar geometry is evaluated, degrees.

Returns:

  • xarray.DataArray:

    Latitudes where solar geometry is evaluated, degrees.

local_standard_time property

local_standard_time

Local standard time.

Returns:

  • np.ndarray[tuple[int], np.datetime64]:

    Local standard time.

longitude property

longitude

Longitudes.

Returns:

  • xarray.DataArray:

    Longitudes where solar geometry is evaluated, degrees.

saa property

saa

Solar azimuth angle.

Returns:

  • DataArray

    Solar azimuth angle, in degrees [-180°, 180°], zero south. Alias for azimuth.

sza property

sza

Solar zenith angle.

Returns:

  • DataArray

    Solar zenith angle, in degrees [0, 180]. Alias for zenith.

times property

times

Times where solar geometry is evaluated.

Returns:

  • np.ndarray[tuple[int], np.datetime64]:

    Times where solar geometry is evaluated.

times_utc property

times_utc

Universal coordinated times where solar geometry is evaluated.

Returns:

  • np.ndarray[tuple[int], np.datetime64]:

    Universal coordinated times.

timezone property

timezone

Timezone.

Returns:

  • str ( time zone. ) –

true_solar_time property

true_solar_time

True solar time.

Returns:

  • xarray.DataArray:

    True solar time, known also to as local apparent time.

usecase property

usecase

Usage case.

Returns:

  • str ( usage case. ) –

zenith property

zenith

Solar zenith angle.

Returns:

  • DataArray

    Solar zenith angle, in degrees [0, 180].

airmass

airmass(parameterization='gueymard_2003')

Atmosphere relative optical air mass.

Calculates the relative optical air mass of the atmosphere according to various parameterizations.

Parameters:

  • parameterization
    (str, default: 'gueymard_2003' ) –

    Parameterization to use: 'kasten_1965', 'kasten_and_young_1989', 'gueymard_2001', or 'gueymard_2003'. Default is 'gueymard_2003'.

Returns:

Raises:

  • ValueError

    If the parameterization is unknown.

References
  1. Table V in Kasten, F. (1965) A new table and approximation formula for the relative optical air mass. CRREL Tech. Report 136

  2. Kasten, F. and Young, A.T. (1989) Revised optical air mass tables and approximation formula. Applied Optics. Vol. 28(22), pp. 4735-4738. https://doi.org/10.1364/AO.28.004735

  3. Table A.1 in Gueymard, C.A. (2001) Parameterized transmittance model for direct bean and circumsolar spectral irradiance. Sol Energy, Vol. 71(5), pp. 325-346. https://doi.org/10.1016/S0038-092X(01)00054-8

  4. Eq. B.8 in Gueymard, C.A. (2003) Direct solar transmittance and irradiance predictions with broadband models. Part I - Detailed theoretical performance assessment. Sol Energy, Vol. 74, pp. 355-379. https://doi.org/10.1016/S0038-092X(03)00195-6

Source code in src/sunwhere/_base.py
def airmass(self, parameterization='gueymard_2003'):
    """Atmosphere relative optical air mass.

    Calculates the relative optical air mass of the atmosphere according
    to various parameterizations.

    Parameters
    ----------
    parameterization : str, optional
        Parameterization to use: 'kasten_1965', 'kasten_and_young_1989',
        'gueymard_2001', or 'gueymard_2003'. Default is 'gueymard_2003'.

    Returns
    -------
    xarray.DataArray
        Relative optical air mass.

    Raises
    ------
    ValueError
        If the parameterization is unknown.

    References
    ----------
    1. Table V in Kasten, F. (1965) A new table and approximation
       formula for the relative optical air mass. CRREL Tech. Report 136

    2. Kasten, F. and Young, A.T. (1989) Revised optical air mass
       tables and approximation formula. Applied Optics. Vol. 28(22),
       pp. 4735-4738. https://doi.org/10.1364/AO.28.004735

    3. Table A.1 in Gueymard, C.A. (2001) Parameterized transmittance
       model for direct bean and circumsolar spectral irradiance. Sol
       Energy, Vol. 71(5), pp. 325-346. https://doi.org/10.1016/S0038-092X(01)00054-8

    4. Eq. B.8 in Gueymard, C.A. (2003) Direct solar transmittance
       and irradiance predictions with broadband models. Part I - Detailed
       theoretical performance assessment. Sol Energy, Vol. 74, pp. 355-379.
       https://doi.org/10.1016/S0038-092X(03)00195-6
    """

    if parameterization == 'kasten_1965':
        Da = np.maximum(1e-4, 93.885 - self.sza)
        rec_am = self.cosz + 0.15*Da**(-1.253)

    elif parameterization == 'kasten_and_young_1989':
        Da = np.maximum(1e-4, 96.07995 - self.sza)
        rec_am = self.cosz + 0.50572*Da**(-1.6364)

    elif parameterization == 'gueymard_2001':
        Da = np.maximum(1e-4, 96.4836 - self.sza)
        rec_am = self.cosz + 0.45665*(self.sza**0.07)*(Da**(-1.697))

    elif parameterization == 'gueymard_2003':
        Da = np.maximum(1e-4, 96.741 - self.sza)
        rec_am = self.cosz + 0.48353*(self.sza**0.095846)*(Da**(-1.754))

    else:
        raise ValueError(
            f'unknown airmass parameterization {parameterization}')

    am = 1./rec_am.where(self.sza <= 90., other=np.nan)
    return am.clip(1., np.inf).rename('airmass').assign_attrs(
        units='-',
        description='relative optical air mass'
    )

daylight_length

daylight_length()

Daylight length, in hours.

Returns:

References
  1. Eq. (1.5.5) in Iqbal, M., An Introduction to Solar Radiation, Academic Press, 1983.
Source code in src/sunwhere/_base.py
def daylight_length(self):
    """Daylight length, in hours.

    Returns
    -------
    xarray.DataArray
        Daylight length in hours.

    References
    ----------
    1. Eq. (1.5.5) in Iqbal, M., An Introduction to Solar Radiation,
       Academic Press, 1983.
    """
    return (2./15.)*self.sunrise(units='deg').rename('daylight_length').assign_attrs(
        units='hour',
        description='daylight length'
    )

eth

eth(ISC=1361.1, am_correction='none')

Extraterrestrial horizontal solar irradiance.

Calculates the extraterrestrial horizontal solar irradiance, in W m⁻², with optional corrections to account for solar eclipse obscuration and for the effect of high optical airmass if eth is used to evaluate clearness index.

Parameters:

  • ISC
    (float, default: 1361.1 ) –

    Solar constant, in W m⁻². Default is 1361.1.

  • am_correction
    (str, default: 'none' ) –

    Whether to use airmass correction. Useful if eth is intended to evaluate the clearness index. Allowed values are 'none', 'perez_1990', 'skarveith_1998', and 'gonzalez_and_calbo_1999'. Default is 'none'.

Returns:

  • DataArray

    Extraterrestrial horizontal solar irradiance.

Raises:

References
  1. Perez et al. (1990) Making full use of the clearness index for parameterizing hourly insolation conditions. Sol Energy, Vol. 45(2), pp 111-114. https://doi.org/10.1016/0038-092X(90)90036-C

  2. Skartveit et al. (1998) An hourly diffuse fraction model with correction for variability and surface albedo. Sol Energy, Vol. 63(3), pp. 173-183. https://doi.org/10.1016/S0038-092X(98)00067-X

  3. González and Calbó (1999) Influence of the global radiation variability on the hourly diffuse fraction correlations. Sol Energy, Vol. 65(2), pp. 119-131. https://doi.org/10.1016/S0038-092X(98)00121-2

Source code in src/sunwhere/_base.py
def eth(self, ISC=1361.1, am_correction='none'):
    """Extraterrestrial horizontal solar irradiance.

    Calculates the extraterrestrial horizontal solar irradiance, in W m⁻²,
    with optional corrections to account for solar eclipse obscuration
    and for the effect of high optical airmass if eth is used to evaluate
    clearness index.

    Parameters
    ----------
    ISC : float, optional
        Solar constant, in W m⁻². Default is 1361.1.
    am_correction : str, optional
        Whether to use airmass correction. Useful if eth is intended to
        evaluate the clearness index. Allowed values are 'none',
        'perez_1990', 'skarveith_1998', and 'gonzalez_and_calbo_1999'.
        Default is 'none'.

    Returns
    -------
    xarray.DataArray
        Extraterrestrial horizontal solar irradiance.

    Raises
    ------
    ValueError
        If am_correction is unknown.

    References
    ----------
    1. Perez et al. (1990) Making full use of the clearness index for
       parameterizing hourly insolation conditions. Sol Energy, Vol. 45(2),
       pp 111-114. https://doi.org/10.1016/0038-092X(90)90036-C

    2. Skartveit et al. (1998) An hourly diffuse fraction model with
       correction for variability and surface albedo. Sol Energy, Vol. 63(3),
       pp. 173-183. https://doi.org/10.1016/S0038-092X(98)00067-X

    3. González and Calbó (1999) Influence of the global radiation
       variability on the hourly diffuse fraction correlations. Sol Energy,
       Vol. 65(2), pp. 119-131. https://doi.org/10.1016/S0038-092X(98)00121-2
    """

    eth = ISC*self.ecf*np.maximum(0., self.cosz)

    if am_correction == 'none':
        corr = 1.
    elif am_correction == 'perez_1990':
        am = self.airmass('kasten_and_young_1989')
        corr = 0.1 + 1.031*np.exp(-1.4 / (0.9 + (9.4 / am)))
    elif am_correction == 'skarveith_1998':
        corr = 0.83 - 0.56*np.exp(-0.06*(90.-self.sza))
    elif am_correction == 'gonzalez_and_calbo_1999':
        am = self.airmass('gueymard_2003')
        dCDA = 0.124 - 0.0285*np.log(am)
        corr = 0.229 + 0.957*np.exp(-1.74*am*dCDA)
    else:
        raise ValueError(f'unknown am_correction {am_correction}')

    return (eth*corr).rename('eth').assign_attrs(
        units='W m-2',
        description='extraterrestrial horizontal solar irradiance')

incidence

incidence(sfc_slope, sfc_azimuth)

Cosine of the solar incidence angle.

Calculates the cosine of the solar incidence angle.

Parameters:

  • sfc_slope
    (float) –

    Surface slope, in degrees.

  • sfc_azimuth
    (float) –

    Surface orientation, in degrees (east positive).

Returns:

  • DataArray

    Cosine of the angle of incidence.

References
  1. Eq. (1.6.5a) in Iqbal, M., An Introduction to Solar Radiation, Academic Press, 1983.
Source code in src/sunwhere/_base.py
def incidence(self, sfc_slope, sfc_azimuth):
    """Cosine of the solar incidence angle.

    Calculates the cosine of the solar incidence angle.

    Parameters
    ----------
    sfc_slope : float
        Surface slope, in degrees.
    sfc_azimuth : float
        Surface orientation, in degrees (east positive).

    Returns
    -------
    xarray.DataArray
        Cosine of the angle of incidence.

    References
    ----------
    1. Eq. (1.6.5a) in Iqbal, M., An Introduction to Solar Radiation,
       Academic Press, 1983.
    """
    # hour angle...
    tst = self.true_solar_time.to_numpy()
    hour = self.true_solar_time.copy(
        data=tst - tst.astype('datetime64[D]')
    ).astype('float64') * 1e-9 / 3600
    ha = hour.copy(data=15*(12-hour))

    fcirc = lambda a: (np.cos(np.radians(a)), np.sin(np.radians(a)))  # noqa: E731

    cosbeta, sinbeta = fcirc(sfc_slope)
    cosgamma, singamma = fcirc(sfc_azimuth)
    coslat, sinlat = fcirc(self.latitude)
    cosdec, sindec = fcirc(self.dec)
    coshour, sinhour = fcirc(ha)
    coss = ((sinlat*cosbeta - coslat*sinbeta*cosgamma)*sindec +
            (coslat*cosbeta + sinlat*sinbeta*cosgamma)*cosdec*coshour +
            cosdec*sinhour*sinbeta*singamma)
    return coss.rename('incidence').assign_attrs(
        units='-', description='cosine of the angle of incidence')

sunrise

sunrise(units='deg')

Sunrise angle or time.

Calculates the sunrise angle or sunrise time.

Parameters:

  • units
    (str, default: 'deg' ) –

    Output units: 'deg' for sunrise angle in degrees, 'rad' for radians, 'tst' for true solar time sunrise, 'utc' for UTC, and 'local' for local time sunrise. Default is 'deg'.

Returns:

Raises:

References
  1. Eq. (1.5.4) in Iqbal, M., An Introduction to Solar Radiation, Academic Press, 1983.
Source code in src/sunwhere/_base.py
def sunrise(self, units='deg'):
    """Sunrise angle or time.

    Calculates the sunrise angle or sunrise time.

    Parameters
    ----------
    units : str, optional
        Output units: 'deg' for sunrise angle in degrees, 'rad' for radians,
        'tst' for true solar time sunrise, 'utc' for UTC, and 'local' for
        local time sunrise. Default is 'deg'.

    Returns
    -------
    xarray.DataArray
        Sunrise angle or time.

    Raises
    ------
    ValueError
        If units are unknown.

    References
    ----------
    1. Eq. (1.5.4) in Iqbal, M., An Introduction to Solar Radiation,
       Academic Press, 1983.
    """
    if units not in ('rad', 'deg', 'tst', 'utc', 'local'):
        raise ValueError(
            f'invalid units {units!r}, expected one of: '
            f"'rad', 'deg', 'tst', 'utc', 'local'")

    tanlat = np.tan(np.radians(self.latitude))
    tandec = np.tan(np.radians(self.declination))
    tantan = -tandec*tanlat

    domain = (tantan >= -1) & (tantan <= 1)
    wsr = np.arccos(tantan.where(domain, other=np.nan))  # radians

    if units == 'deg':
        wsr = np.degrees(wsr)

    if units in ('tst', 'utc', 'local'):
        wsr = 12. - (np.degrees(wsr)/15.)
        delta_ns = wsr.copy(
            data=(wsr*3600*1e9).astype('timedelta64[ns]'))
        tst_d = self.true_solar_time.copy(
            # if I convert directly from self.true_solar_time (which is
            # a DataArray) to `datetime64[D]` xarray complains. It raises
            # a UserWarning because I am downgrading the resolution from
            # ns to D. Hence, I use `to_numpy()` to convert to a numpy
            # array, then cast to `datetime64[D]`, then to `datetime64[ns]`
            # and the I create the DataArray tst_d. All this is only to
            # prevent that annoying UserWarning !!
            data=(self.true_solar_time.to_numpy()
                  .astype('datetime64[D]')
                  .astype('datetime64[ns]'))
        )
        wsr = delta_ns + tst_d

        if units in ('utc', 'local'):
            delta_hr = self.eot + 4*self.longitude
            delta_ns = delta_hr.copy(
                data=(delta_hr*60*1e9).astype('timedelta64[ns]'))
            wsr = wsr - delta_ns

            if units == 'local':
                utcoffset = xr.DataArray(
                    self._times.tz_localize(None) - self._times_utc.tz_localize(None),
                    coords={'time': wsr.coords['time']}, dims=('time',))
                wsr = wsr + utcoffset

    return wsr.rename('wsr').assign_attrs(
        units={'deg': 'degrees', 'rad': 'radians', 'tst': 'TST',
               'utc': 'UTC', 'local': self.timezone}.get(units, units),
        description='sunrise'
    )

sunset

sunset(units='deg')

Sunset angle or time.

Calculates the sunset angle or sunset time.

Parameters:

  • units
    (str, default: 'deg' ) –

    Output units: 'deg' for sunset angle in degrees, 'rad' for radians, 'tst' for true solar time sunset, 'utc' for UTC, and 'local' for local time sunset. Default is 'deg'.

Returns:

Raises:

References
  1. Eq. (1.5.4) in Iqbal, M., An Introduction to Solar Radiation, Academic Press, 1983.
Source code in src/sunwhere/_base.py
def sunset(self, units='deg'):
    """Sunset angle or time.

    Calculates the sunset angle or sunset time.

    Parameters
    ----------
    units : str, optional
        Output units: 'deg' for sunset angle in degrees, 'rad' for radians,
        'tst' for true solar time sunset, 'utc' for UTC, and 'local' for
        local time sunset. Default is 'deg'.

    Returns
    -------
    xarray.DataArray
        Sunset angle or time.

    Raises
    ------
    ValueError
        If units are unknown.

    References
    ----------
    1. Eq. (1.5.4) in Iqbal, M., An Introduction to Solar Radiation,
       Academic Press, 1983.
    """
    if units not in ('rad', 'deg', 'tst', 'utc', 'local'):
        raise ValueError(
            f'invalid units {units!r}, expected one of: '
            f"'rad', 'deg', 'tst', 'utc', 'local'")

    if units in ('tst', 'utc', 'local'):
        wsr = self.sunrise(units)
        dl_hr = self.daylight_length()
        wss = wsr + dl_hr.copy(data=(dl_hr*3600*1e9).astype('timedelta64[ns]'))

    if units == 'deg':
        wss = (12. - wss)*15

    if units == 'rad':
        wss = np.radians((12. - wss)*15)

    return wss.rename('wss').assign_attrs(
        units={'deg': 'degrees', 'rad': 'radians', 'tst': 'TST',
               'utc': 'UTC', 'local': self.timezone}.get(units, units),
        description='sunset'
    )