Skip to content

API Reference

Full documentation for all public classes and functions in sparta-solar, generated from the source docstrings.


Atmospheric data sources

MERRA-2 daily reanalysis

MERRA2DailyAtmosphere

MERRA2DailyAtmosphere()

Bases: BaseAtmosphere

MERRA-2 daily atmospheric data accessor.

Provides methods to load and interpolate MERRA-2 daily atmospheric data for specific locations or regular grids. Data is automatically cached locally and loaded from Zarr archives organized by year.

The class inherits from BaseAtmosphere and provides two main factory methods: - at_sites(): Extract data at specific point locations - on_regular_grid(): Extract data on a regular lat/lon mesh

Attributes:

  • database_path (Path) –

    Path to local MERRA-2 data storage directory.

Notes

Available variables include pressure, albedo, pwater, ozone, beta, alpha and ssa.

Validates that the database path exists (if specified) and initializes the internal atmosphere dataset to None.

Raises:

Methods:

  • __init_subclass__

    Automatically sets the database path for subclasses.

  • at_sites

    Load atmospheric data at specific geographic locations.

  • compute

    Compute clear-sky solar radiation using a radiative transfer model.

  • on_regular_grid

    Load atmospheric data on a regular lat/lon grid.

Source code in src/spartasolar/atmoslib/_base.py
def __init__(self):
    """Initialize the atmosphere instance.

    Validates that the database path exists (if specified) and initializes
    the internal atmosphere dataset to None.

    Raises
    ------
    AttributeError
        If database_path is specified but does not exist
    """
    if self.database_path is not None and not self.database_path.exists():
        raise AttributeError(f"missing path `{self.database_path}`")

    self._atmosphere: xr.DataArray = None

__init_subclass__

__init_subclass__(database_path: str, **kwargs)

Automatically sets the database path for subclasses.

Parameters:

  • database_path
    (str or None) –

    The directory path where the specific atmosphere data is stored. Pass None for sources that do not use a file database (e.g. CustomAtmosphere or API-based retrievers).

Source code in src/spartasolar/atmoslib/_base.py
def __init_subclass__(cls, database_path: str, **kwargs):
    """Automatically sets the database path for subclasses.

    Parameters
    ----------
    database_path : str or None
        The directory path where the specific atmosphere data is stored.
        Pass ``None`` for sources that do not use a file database
        (e.g. ``CustomAtmosphere`` or API-based retrievers).
    """
    super().__init_subclass__(**kwargs)
    cls.database_path = None if database_path is None else Path(database_path)

at_sites classmethod

Load atmospheric data at specific geographic locations.

Extracts and interpolates MERRA-2 data for one or more point locations. Performs bilinear spatial interpolation and quadratic temporal interpolation.

Parameters:

Returns:

Raises:

  • ValueError

    If latitude and longitude have different lengths or invalid values.

Examples:

>>> import pandas as pd
>>> from spartasolar.atmoslib import MERRA2DailyAtmosphere

>>> # Single location
>>> times = pd.date_range("2020-01-01", periods=5, freq="D")
>>> atmos = MERRA2DailyAtmosphere.at_sites(
...     times=times,
...     latitude=40.4168,
...     longitude=-3.7038,
...     site_names="Madrid"
... )

>>> # Multiple locations
>>> lats = [40.4168, 41.3851, 36.7213]  # Madrid, Barcelona, Málaga
>>> lons = [-3.7038, 2.1734, -4.4214]
>>> names = ["Madrid", "Barcelona", "Málaga"]
>>> atmos = MERRA2DailyAtmosphere.at_sites(
...     times=times,
...     latitude=lats,
...     longitude=lons,
...     site_names=names
... )

>>> # Access data
>>> pressure = atmos.dataset["pressure"]
>>> print(pressure.dims)
('time', 'site')
Source code in src/spartasolar/atmoslib/merra2_daily.py
@classmethod
def at_sites(
    cls,
    times: np.ndarray[tuple[int], np.dtype[np.datetime64]] | pd.DatetimeIndex,
    latitude: Sequence[float] | float,
    longitude: Sequence[float] | float,
    site_names: Sequence[str] | None = None,
) -> Self:
    """Load atmospheric data at specific geographic locations.

    Extracts and interpolates MERRA-2 data for one or more point locations.
    Performs bilinear spatial interpolation and quadratic temporal interpolation.

    Parameters
    ----------
    times : np.ndarray or pd.DatetimeIndex
        Time points for data extraction.
    latitude : float or Sequence[float]
        Latitude coordinate(s) in decimal degrees.
    longitude : float or Sequence[float]
        Longitude coordinate(s) in decimal degrees.
    site_names : Sequence[str] or None, default None
        Optional names for the sites.

    Returns
    -------
    MERRA2DailyAtmosphere
        Instance containing interpolated atmospheric data.

    Raises
    ------
    ValueError
        If latitude and longitude have different lengths or invalid values.

    Examples
    --------
        >>> import pandas as pd
        >>> from spartasolar.atmoslib import MERRA2DailyAtmosphere

        >>> # Single location
        >>> times = pd.date_range("2020-01-01", periods=5, freq="D")
        >>> atmos = MERRA2DailyAtmosphere.at_sites(
        ...     times=times,
        ...     latitude=40.4168,
        ...     longitude=-3.7038,
        ...     site_names="Madrid"
        ... )

        >>> # Multiple locations
        >>> lats = [40.4168, 41.3851, 36.7213]  # Madrid, Barcelona, Málaga
        >>> lons = [-3.7038, 2.1734, -4.4214]
        >>> names = ["Madrid", "Barcelona", "Málaga"]
        >>> atmos = MERRA2DailyAtmosphere.at_sites(
        ...     times=times,
        ...     latitude=lats,
        ...     longitude=lons,
        ...     site_names=names
        ... )

        >>> # Access data
        >>> pressure = atmos.dataset["pressure"]
        >>> print(pressure.dims)
        ('time', 'site')
    """

    latitude = [latitude] if isinstance(latitude, (float, int)) else latitude
    latitude = np.asarray([validate_type(lat, Latitude) for lat in latitude], dtype=float).reshape(-1)
    longitude = [longitude] if isinstance(longitude, (float, int)) else longitude
    longitude = np.asarray([validate_type(lon, Longitude) for lon in longitude], dtype=float).reshape(-1)

    if len(latitude) != len(longitude):
        raise ValueError('latitude and longitude must have the same length')

    # load the dataset. Check for local availability. If not available, download.
    dataset = cls._load_dataset(times)

    # lat-lon interpolation
    output_lat = xr.DataArray(latitude, dims="site", name="lat")
    output_lon = xr.DataArray(longitude, dims="site", name="lon")
    # # if interpolation was `nearest` it is faster to use .sel than .interp
    # output_dataset = dataset.sel(lat=output_lat, lon=output_lon, method='nearest')
    # output_dataset = dataset.interp(lat=output_lat, lon=output_lon, method='nearest')
    output_dataset = dataset.interp(lat=output_lat, lon=output_lon, method='linear')

    # time interpolation
    if "time" in output_dataset.coords:
        output_dataset = (
            output_dataset
            .assign_coords(
                time=pd.to_datetime(output_dataset.time.values).tz_localize(None)  # convert to tz-naive for interpolation
            )
            .interp(
                time=ensure_tz_aware_datetime_index(times, utc=True).tz_localize(None),  # convert to tz-naive for interpolation
                method='quadratic'
            )
        )

    global_attrs = {
        "title": "Daily Clear-sky Atmospheric Dataset for SPARTA",
        "references": "doi:10.5067/KLICLTZ8EM9D, doi:10.5067/Q9QMY5PBNV1T, doi:10.5067/VJAFPLI1CSIV",
    }

    obj = cls()
    obj._atmosphere = build_atmosphere_of_sites(
        times=times,
        latitude=latitude,
        longitude=longitude,
        constituents=output_dataset.data_vars,
        site_names=site_names,
        global_attrs=global_attrs)
    return obj

compute

compute(
    model: Model = "SPARTA",
    include_atmosphere: bool = False,
    model_kwargs: dict | None = None,
) -> Dataset

Compute clear-sky solar radiation using a radiative transfer model.

This method integrates solar position calculations with atmospheric constituent data to compute clear-sky irradiance components (GHI, DNI, DHI, etc.) using the specified radiative transfer model.

Parameters:

  • model
    (Model, default: "SPARTA" ) –

    Name of the clear-sky model to use. Options: "SPARTA", "Bird"

  • include_atmosphere
    (bool, default: False ) –

    If True, include atmospheric constituents in the output dataset. If False, only radiation components are returned.

  • model_kwargs
    (dict, default: None ) –

    Additional keyword arguments to pass to the model function

Returns:

  • Dataset

    CF-compliant dataset containing computed irradiance components: - ghi: Global Horizontal Irradiance (W/m²) - dni: Direct Normal Irradiance (W/m²) - dhi or dif: Diffuse Horizontal Irradiance (W/m²) - csi: Circumsolar Irradiance (W/m², SPARTA only)

Examples:

>>> import pandas as pd
>>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
>>>
>>> times = pd.date_range("2020-06-15", periods=24, freq="h")
>>> atm = MERRA2DailyAtmosphere.at_sites(
...     times=times,
...     latitude=36.72,
...     longitude=-4.42
... )
>>> result = atm.compute(model="SPARTA")
>>> print(result.ghi.values)

Use different model with custom parameters:

>>> result = atm.compute(
...     model="Bird",
...     model_kwargs={"scheme": "transmittance_parameterization"}
... )
Notes

The method automatically: - Calculates solar position (zenith angle, Earth-Sun distance) - Converts atmospheric units to model requirements - Handles both gridded and site-based data structures

See Also

spartasolar.modlib.sparta : SPARTA model implementation spartasolar.modlib.bird : Bird clear-sky model

Source code in src/spartasolar/atmoslib/_base.py
def compute(
    self,
    model: Model = "SPARTA",
    include_atmosphere: bool = False,  # TODO: currently ignored
    model_kwargs: dict | None = None,
) -> xr.Dataset:
    """Compute clear-sky solar radiation using a radiative transfer model.

    This method integrates solar position calculations with atmospheric
    constituent data to compute clear-sky irradiance components (GHI, DNI,
    DHI, etc.) using the specified radiative transfer model.

    Parameters
    ----------
    model : Model, default "SPARTA"
        Name of the clear-sky model to use. Options: "SPARTA", "Bird"
    include_atmosphere : bool, default False
        If True, include atmospheric constituents in the output dataset.
        If False, only radiation components are returned.
    model_kwargs : dict, optional
        Additional keyword arguments to pass to the model function

    Returns
    -------
    xr.Dataset
        CF-compliant dataset containing computed irradiance components:
        - ghi: Global Horizontal Irradiance (W/m²)
        - dni: Direct Normal Irradiance (W/m²)
        - dhi or dif: Diffuse Horizontal Irradiance (W/m²)
        - csi: Circumsolar Irradiance (W/m², SPARTA only)

    Examples
    --------
    >>> import pandas as pd
    >>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
    >>>
    >>> times = pd.date_range("2020-06-15", periods=24, freq="h")
    >>> atm = MERRA2DailyAtmosphere.at_sites(
    ...     times=times,
    ...     latitude=36.72,
    ...     longitude=-4.42
    ... )
    >>> result = atm.compute(model="SPARTA")
    >>> print(result.ghi.values)

    Use different model with custom parameters:

    >>> result = atm.compute(
    ...     model="Bird",
    ...     model_kwargs={"scheme": "transmittance_parameterization"}
    ... )

    Notes
    -----
    The method automatically:
    - Calculates solar position (zenith angle, Earth-Sun distance)
    - Converts atmospheric units to model requirements
    - Handles both gridded and site-based data structures

    See Also
    --------
    spartasolar.modlib.sparta : SPARTA model implementation
    spartasolar.modlib.bird : Bird clear-sky model
    """

    model = validate_type(model, Model)
    model_func = getattr(modlib, model)
    model_vars = inspect.getfullargspec(model_func).args

    is_regular_grid = "site" not in self.dataset.dims

    # compute solar geometry...
    sw_eval = sunwhere.regular_grid if is_regular_grid else sunwhere.sites
    solpos = sw_eval(
        # self.dataset.time.values,  # WARNING: this returns a utc tz-naive numpy array of dtype datetime64[ns] !!!
        self.dataset.indexes["time"],  # NOTE: with `indexes` we get a pandas DatetimeIndex with tz info preserved!
        latitude=self.dataset.lat.values,
        longitude=self.dataset.lon.values,
        algorithm=config.get_option("sunwhere.algorithm", default="psa"),
        refraction=config.get_option("sunwhere.refraction", default=True),
        engine=config.get_option("sunwhere.engine", default="numexpr")
    )

    new_coord_names = {"location": "site", "latitude": "lat", "longitude": "lon"}
    cosz = solpos.cosz.rename({old: new for old, new in new_coord_names.items() if old in solpos.cosz.dims})

    if is_regular_grid:
        n_lats = self.dataset.sizes["lat"]
        n_lons = self.dataset.sizes["lon"]
        ecf = solpos.ecf.expand_dims(dim={"lat": n_lats, "lon": n_lons}, axis=(1, 2))
    else:
        n_locs = self.dataset.sizes["site"]
        ecf = solpos.ecf.expand_dims(dim={"site": n_locs}, axis=1)

    kwargs = {"cosz": cosz, "ecf": ecf}

    # atmosphere...
    def get_with_proper_units(var):
        if var not in self.dataset.data_vars:
            raise ValueError(f"variable `{var}` required by the model `{model}` is not available in this atmosphere")
        if var == "pressure":
            return self.dataset[var] * 1e-2  # Pa to hPa
        if var == "pwater":
            return pwater_in_kg_m2_to_cm(self.dataset[var])
        if var == "ozone":
            return ozone_in_kg_m2_to_cm(self.dataset[var])
        return self.dataset[var]
    variables = set(model_vars).intersection(self.dataset.data_vars)  # variables required by this model
    kwargs = kwargs | {var: get_with_proper_units(var) for var in variables}

    # and call the clearsky model...
    result = model_func(**(kwargs | (model_kwargs or {})))

    # encapsulate the result in a CF-compliant xarray Dataset
    if is_regular_grid:
        return build_atmosphere_on_regular_grid(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            global_attrs=get_global_attrs(feature_type="grid")
        )
    else:
        site_values = self.dataset.coords.get("site").values
        n_sites = self.dataset.sizes["site"]
        site_names = validate_site_names(site_values, n_sites)
        return build_atmosphere_of_sites(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            site_names=site_names,
            global_attrs=get_global_attrs(feature_type="timeSeries")
        )

on_regular_grid classmethod

Load atmospheric data on a regular lat/lon grid.

Extracts and interpolates MERRA-2 data onto a user-specified regular grid. The output has dimensions (time, lat, lon). NaN values in albedo are filled with zeros.

Parameters:

Returns:

Raises:

  • ValueError

    If coordinates are out of valid range.

Examples:

>>> import pandas as pd
>>> import numpy as np
>>> from spartasolar.atmoslib import MERRA2DailyAtmosphere

>>> # Define a regular grid over Iberian Peninsula
>>> times = pd.date_range("2020-06-01", periods=10, freq="D")
>>> lats = np.linspace(36, 44, 9)  # 9 latitude points
>>> lons = np.linspace(-10, 4, 15)  # 15 longitude points

>>> atmos = MERRA2DailyAtmosphere.on_regular_grid(
...     times=times,
...     latitude=lats,
...     longitude=lons
... )

>>> # Access gridded data
>>> albedo = atmos.dataset["albedo"]
>>> print(albedo.dims)
('time', 'lat', 'lon')
>>> print(albedo.shape)
(10, 9, 15)
Source code in src/spartasolar/atmoslib/merra2_daily.py
@classmethod
def on_regular_grid(
    cls,
    times: np.ndarray[tuple[int], np.dtype[np.datetime64]] | pd.DatetimeIndex,
    latitude: Sequence[float] | float,
    longitude: Sequence[float] | float,
) -> Self:
    """Load atmospheric data on a regular lat/lon grid.

    Extracts and interpolates MERRA-2 data onto a user-specified regular grid.
    The output has dimensions (time, lat, lon). NaN values in albedo are filled
    with zeros.

    Parameters
    ----------
    times : np.ndarray or pd.DatetimeIndex
        Time points for data extraction.
    latitude : float or Sequence[float]
        Latitude coordinates for the grid in decimal degrees.
    longitude : float or Sequence[float]
        Longitude coordinates for the grid in decimal degrees.

    Returns
    -------
    MERRA2DailyAtmosphere
        Instance containing gridded atmospheric data.

    Raises
    ------
    ValueError
        If coordinates are out of valid range.

    Examples
    --------
        >>> import pandas as pd
        >>> import numpy as np
        >>> from spartasolar.atmoslib import MERRA2DailyAtmosphere

        >>> # Define a regular grid over Iberian Peninsula
        >>> times = pd.date_range("2020-06-01", periods=10, freq="D")
        >>> lats = np.linspace(36, 44, 9)  # 9 latitude points
        >>> lons = np.linspace(-10, 4, 15)  # 15 longitude points

        >>> atmos = MERRA2DailyAtmosphere.on_regular_grid(
        ...     times=times,
        ...     latitude=lats,
        ...     longitude=lons
        ... )

        >>> # Access gridded data
        >>> albedo = atmos.dataset["albedo"]
        >>> print(albedo.dims)
        ('time', 'lat', 'lon')
        >>> print(albedo.shape)
        (10, 9, 15)
    """

    latitude = [latitude] if isinstance(latitude, (float, int)) else latitude
    latitude = np.asarray([validate_type(lat, Latitude) for lat in latitude], dtype=float).reshape(-1)
    longitude = [longitude] if isinstance(longitude, (float, int)) else longitude
    longitude = np.asarray([validate_type(lon, Longitude) for lon in longitude], dtype=float).reshape(-1)

    logger.info(f"loading MERRA-2 daily data for {len(times)} time points, "
                f"{len(latitude)} latitudes, {len(longitude)} longitudes...")

    # load the dataset. Check for local availability. If not available, download.
    dataset = cls._load_dataset(times)
    dataset["albedo"] = dataset["albedo"].fillna(0.)

    # lat-lon interpolation
    output_dataset = dataset.interp(lat=latitude, lon=longitude, method='linear')

    # time interpolation
    if "time" in output_dataset.coords:
        output_dataset = (
            output_dataset
            .assign_coords(
                time=pd.to_datetime(output_dataset.time.values).tz_localize(None)  # convert to tz-naive for interpolation
            )
            .interp(
                time=ensure_tz_aware_datetime_index(times, utc=True).tz_localize(None),  # convert to tz-naive for interpolation
                method='quadratic'
            )
        )

    global_attrs = {
        "title": "Daily Clear-sky Atmospheric Dataset for SPARTA",
        "references": "doi:10.5067/KLICLTZ8EM9D, doi:10.5067/Q9QMY5PBNV1T, doi:10.5067/VJAFPLI1CSIV",
    }

    obj = cls()
    obj._atmosphere = build_atmosphere_on_regular_grid(
        times=times,
        latitude=latitude,
        longitude=longitude,
        constituents=output_dataset.data_vars,
        global_attrs=global_attrs)
    return obj

MERRA-2 long-term averages

MERRA2LTAAtmosphere

MERRA2LTAAtmosphere()

Bases: BaseAtmosphere

MERRA-2 long-term monthly average atmospheric database.

Provides climatological monthly averages (1999-2018) of atmospheric constituents from NASA MERRA-2 reanalysis. Data is interpolated spatially and temporally to match requested coordinates and times.

See module documentation for examples.

Validates that the database path exists (if specified) and initializes the internal atmosphere dataset to None.

Raises:

Methods:

  • __init_subclass__

    Automatically sets the database path for subclasses.

  • at_sites

    Retrieve monthly climatology at specific sites.

  • compute

    Compute clear-sky solar radiation using a radiative transfer model.

  • on_regular_grid

    Retrieve monthly climatology on a regular spatial grid.

Source code in src/spartasolar/atmoslib/_base.py
def __init__(self):
    """Initialize the atmosphere instance.

    Validates that the database path exists (if specified) and initializes
    the internal atmosphere dataset to None.

    Raises
    ------
    AttributeError
        If database_path is specified but does not exist
    """
    if self.database_path is not None and not self.database_path.exists():
        raise AttributeError(f"missing path `{self.database_path}`")

    self._atmosphere: xr.DataArray = None

__init_subclass__

__init_subclass__(database_path: str, **kwargs)

Automatically sets the database path for subclasses.

Parameters:

  • database_path
    (str or None) –

    The directory path where the specific atmosphere data is stored. Pass None for sources that do not use a file database (e.g. CustomAtmosphere or API-based retrievers).

Source code in src/spartasolar/atmoslib/_base.py
def __init_subclass__(cls, database_path: str, **kwargs):
    """Automatically sets the database path for subclasses.

    Parameters
    ----------
    database_path : str or None
        The directory path where the specific atmosphere data is stored.
        Pass ``None`` for sources that do not use a file database
        (e.g. ``CustomAtmosphere`` or API-based retrievers).
    """
    super().__init_subclass__(**kwargs)
    cls.database_path = None if database_path is None else Path(database_path)

at_sites classmethod

Retrieve monthly climatology at specific sites.

Parameters:

  • times
    (ndarray or DatetimeIndex) –

    Time stamps for climatology retrieval. Monthly climatology is repeated for each year and interpolated to exact times.

  • latitude
    (Sequence[float]) –

    Latitude(s) in degrees North [-90, 90]

  • longitude
    (Sequence[float]) –

    Longitude(s) in degrees East [-180, 180]

  • site_names
    (Sequence[str], default: None ) –

    Names for each site

Returns:

Examples:

>>> times = pd.date_range("2023-01-01", "2023-12-31", freq="D")
>>> atm = MERRA2LTAAtmosphere.at_sites(
...     times=times,
...     latitude=36.72,
...     longitude=-4.42,
...     site_names="Málaga"
... )
Source code in src/spartasolar/atmoslib/merra2_lta.py
@classmethod
def at_sites(
    cls,
    times: np.ndarray[tuple[int], np.dtype[np.datetime64]] | pd.DatetimeIndex,
    latitude: Sequence[float] | float,
    longitude: Sequence[float] | float,
    site_names: Sequence[str] | None = None,
) -> Self:
    """Retrieve monthly climatology at specific sites.

    Parameters
    ----------
    times : np.ndarray or pd.DatetimeIndex
        Time stamps for climatology retrieval. Monthly climatology is
        repeated for each year and interpolated to exact times.
    latitude : Sequence[float]
        Latitude(s) in degrees North [-90, 90]
    longitude : Sequence[float]
        Longitude(s) in degrees East [-180, 180]
    site_names : Sequence[str], optional
        Names for each site

    Returns
    -------
    MERRA2LTAAtmosphere
        Instance with interpolated climatological data

    Examples
    --------
    >>> times = pd.date_range("2023-01-01", "2023-12-31", freq="D")
    >>> atm = MERRA2LTAAtmosphere.at_sites(
    ...     times=times,
    ...     latitude=36.72,
    ...     longitude=-4.42,
    ...     site_names="Málaga"
    ... )
    """

    latitude = np.asarray(latitude, dtype=float).reshape(-1)
    latitude = [validate_type(lat, Latitude) for lat in latitude]
    longitude = np.asarray(longitude, dtype=float).reshape(-1)
    longitude = [validate_type(lon, Longitude) for lon in longitude]

    if len(latitude) != len(longitude):
        raise ValueError('latitude and longitude must have the same length')

    # load the dataset. Check for local availability. If not available, download.
    dataset = cls._load_dataset(times)

    # lat-lon interpolation
    output_lat = xr.DataArray(latitude, dims="site", name="lat")
    output_lon = xr.DataArray(longitude, dims="site", name="lon")
    output_dataset = dataset.interp(lat=output_lat, lon=output_lon, method='linear')

    # time interpolation
    if "time" in output_dataset.coords:
        output_dataset = (
            output_dataset
            .assign_coords(
                time=pd.to_datetime(output_dataset.time.values).tz_localize(None)  # convert to tz-naive for interpolation
            )
            .interp(
                time=ensure_tz_aware_datetime_index(times, utc=True).tz_localize(None),  # convert to tz-naive for interpolation
                method='quadratic'
            )
        )

    global_attrs = {
        "title": "Long Term Average Clear-sky Atmospheric Dataset for SPARTA",
        "references": "doi:10.5067/KLICLTZ8EM9D, doi:10.5067/Q9QMY5PBNV1T, doi:10.5067/VJAFPLI1CSIV",
    }

    obj = cls()
    obj._atmosphere = build_atmosphere_of_sites(
        times=times,
        latitude=latitude,
        longitude=longitude,
        constituents=output_dataset.data_vars,
        site_names=site_names,
        global_attrs=global_attrs)
    return obj

compute

compute(
    model: Model = "SPARTA",
    include_atmosphere: bool = False,
    model_kwargs: dict | None = None,
) -> Dataset

Compute clear-sky solar radiation using a radiative transfer model.

This method integrates solar position calculations with atmospheric constituent data to compute clear-sky irradiance components (GHI, DNI, DHI, etc.) using the specified radiative transfer model.

Parameters:

  • model
    (Model, default: "SPARTA" ) –

    Name of the clear-sky model to use. Options: "SPARTA", "Bird"

  • include_atmosphere
    (bool, default: False ) –

    If True, include atmospheric constituents in the output dataset. If False, only radiation components are returned.

  • model_kwargs
    (dict, default: None ) –

    Additional keyword arguments to pass to the model function

Returns:

  • Dataset

    CF-compliant dataset containing computed irradiance components: - ghi: Global Horizontal Irradiance (W/m²) - dni: Direct Normal Irradiance (W/m²) - dhi or dif: Diffuse Horizontal Irradiance (W/m²) - csi: Circumsolar Irradiance (W/m², SPARTA only)

Examples:

>>> import pandas as pd
>>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
>>>
>>> times = pd.date_range("2020-06-15", periods=24, freq="h")
>>> atm = MERRA2DailyAtmosphere.at_sites(
...     times=times,
...     latitude=36.72,
...     longitude=-4.42
... )
>>> result = atm.compute(model="SPARTA")
>>> print(result.ghi.values)

Use different model with custom parameters:

>>> result = atm.compute(
...     model="Bird",
...     model_kwargs={"scheme": "transmittance_parameterization"}
... )
Notes

The method automatically: - Calculates solar position (zenith angle, Earth-Sun distance) - Converts atmospheric units to model requirements - Handles both gridded and site-based data structures

See Also

spartasolar.modlib.sparta : SPARTA model implementation spartasolar.modlib.bird : Bird clear-sky model

Source code in src/spartasolar/atmoslib/_base.py
def compute(
    self,
    model: Model = "SPARTA",
    include_atmosphere: bool = False,  # TODO: currently ignored
    model_kwargs: dict | None = None,
) -> xr.Dataset:
    """Compute clear-sky solar radiation using a radiative transfer model.

    This method integrates solar position calculations with atmospheric
    constituent data to compute clear-sky irradiance components (GHI, DNI,
    DHI, etc.) using the specified radiative transfer model.

    Parameters
    ----------
    model : Model, default "SPARTA"
        Name of the clear-sky model to use. Options: "SPARTA", "Bird"
    include_atmosphere : bool, default False
        If True, include atmospheric constituents in the output dataset.
        If False, only radiation components are returned.
    model_kwargs : dict, optional
        Additional keyword arguments to pass to the model function

    Returns
    -------
    xr.Dataset
        CF-compliant dataset containing computed irradiance components:
        - ghi: Global Horizontal Irradiance (W/m²)
        - dni: Direct Normal Irradiance (W/m²)
        - dhi or dif: Diffuse Horizontal Irradiance (W/m²)
        - csi: Circumsolar Irradiance (W/m², SPARTA only)

    Examples
    --------
    >>> import pandas as pd
    >>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
    >>>
    >>> times = pd.date_range("2020-06-15", periods=24, freq="h")
    >>> atm = MERRA2DailyAtmosphere.at_sites(
    ...     times=times,
    ...     latitude=36.72,
    ...     longitude=-4.42
    ... )
    >>> result = atm.compute(model="SPARTA")
    >>> print(result.ghi.values)

    Use different model with custom parameters:

    >>> result = atm.compute(
    ...     model="Bird",
    ...     model_kwargs={"scheme": "transmittance_parameterization"}
    ... )

    Notes
    -----
    The method automatically:
    - Calculates solar position (zenith angle, Earth-Sun distance)
    - Converts atmospheric units to model requirements
    - Handles both gridded and site-based data structures

    See Also
    --------
    spartasolar.modlib.sparta : SPARTA model implementation
    spartasolar.modlib.bird : Bird clear-sky model
    """

    model = validate_type(model, Model)
    model_func = getattr(modlib, model)
    model_vars = inspect.getfullargspec(model_func).args

    is_regular_grid = "site" not in self.dataset.dims

    # compute solar geometry...
    sw_eval = sunwhere.regular_grid if is_regular_grid else sunwhere.sites
    solpos = sw_eval(
        # self.dataset.time.values,  # WARNING: this returns a utc tz-naive numpy array of dtype datetime64[ns] !!!
        self.dataset.indexes["time"],  # NOTE: with `indexes` we get a pandas DatetimeIndex with tz info preserved!
        latitude=self.dataset.lat.values,
        longitude=self.dataset.lon.values,
        algorithm=config.get_option("sunwhere.algorithm", default="psa"),
        refraction=config.get_option("sunwhere.refraction", default=True),
        engine=config.get_option("sunwhere.engine", default="numexpr")
    )

    new_coord_names = {"location": "site", "latitude": "lat", "longitude": "lon"}
    cosz = solpos.cosz.rename({old: new for old, new in new_coord_names.items() if old in solpos.cosz.dims})

    if is_regular_grid:
        n_lats = self.dataset.sizes["lat"]
        n_lons = self.dataset.sizes["lon"]
        ecf = solpos.ecf.expand_dims(dim={"lat": n_lats, "lon": n_lons}, axis=(1, 2))
    else:
        n_locs = self.dataset.sizes["site"]
        ecf = solpos.ecf.expand_dims(dim={"site": n_locs}, axis=1)

    kwargs = {"cosz": cosz, "ecf": ecf}

    # atmosphere...
    def get_with_proper_units(var):
        if var not in self.dataset.data_vars:
            raise ValueError(f"variable `{var}` required by the model `{model}` is not available in this atmosphere")
        if var == "pressure":
            return self.dataset[var] * 1e-2  # Pa to hPa
        if var == "pwater":
            return pwater_in_kg_m2_to_cm(self.dataset[var])
        if var == "ozone":
            return ozone_in_kg_m2_to_cm(self.dataset[var])
        return self.dataset[var]
    variables = set(model_vars).intersection(self.dataset.data_vars)  # variables required by this model
    kwargs = kwargs | {var: get_with_proper_units(var) for var in variables}

    # and call the clearsky model...
    result = model_func(**(kwargs | (model_kwargs or {})))

    # encapsulate the result in a CF-compliant xarray Dataset
    if is_regular_grid:
        return build_atmosphere_on_regular_grid(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            global_attrs=get_global_attrs(feature_type="grid")
        )
    else:
        site_values = self.dataset.coords.get("site").values
        n_sites = self.dataset.sizes["site"]
        site_names = validate_site_names(site_values, n_sites)
        return build_atmosphere_of_sites(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            site_names=site_names,
            global_attrs=get_global_attrs(feature_type="timeSeries")
        )

on_regular_grid classmethod

Retrieve monthly climatology on a regular spatial grid.

Parameters:

Returns:

Examples:

>>> import numpy as np
>>> lats = np.linspace(36.0, 41.0, 20)
>>> lons = np.linspace(-5.0, -3.0, 20)
>>> times = pd.date_range("2023-01-15", periods=12, freq="MS") + pd.Timedelta(14.5, "D")
>>> atm = MERRA2LTAAtmosphere.on_regular_grid(
...     times=times,
...     latitude=lats,
...     longitude=lons
... )
Source code in src/spartasolar/atmoslib/merra2_lta.py
@classmethod
def on_regular_grid(
    cls,
    times: np.ndarray[tuple[int], np.dtype[np.datetime64]] | pd.DatetimeIndex,
    latitude: Sequence[float],
    longitude: Sequence[float],
) -> Self:
    """Retrieve monthly climatology on a regular spatial grid.

    Parameters
    ----------
    times : np.ndarray or pd.DatetimeIndex
        Time stamps for climatology retrieval
    latitude : Sequence[float]
        Latitude grid coordinates in degrees North
    longitude : Sequence[float]
        Longitude grid coordinates in degrees East

    Returns
    -------
    MERRA2LTAAtmosphere
        Instance with gridded climatological data

    Examples
    --------
    >>> import numpy as np
    >>> lats = np.linspace(36.0, 41.0, 20)
    >>> lons = np.linspace(-5.0, -3.0, 20)
    >>> times = pd.date_range("2023-01-15", periods=12, freq="MS") + pd.Timedelta(14.5, "D")
    >>> atm = MERRA2LTAAtmosphere.on_regular_grid(
    ...     times=times,
    ...     latitude=lats,
    ...     longitude=lons
    ... )
    """

    latitude = np.asarray(latitude, dtype=float).reshape(-1)
    latitude = [validate_type(lat, Latitude) for lat in latitude]
    longitude = np.asarray(longitude, dtype=float).reshape(-1)
    longitude = [validate_type(lon, Longitude) for lon in longitude]

    # load the dataset. Check for local availability. If not available, download.
    dataset = cls._load_dataset(times)

    # lat-lon interpolation
    output_dataset = dataset.interp(lat=latitude, lon=longitude, method='linear')

    # time interpolation
    if "time" in output_dataset.coords:
        output_dataset = (
            output_dataset
            .assign_coords(
                time=pd.to_datetime(output_dataset.time.values).tz_localize(None)  # convert to tz-naive for interpolation
            )
            .interp(
                time=ensure_tz_aware_datetime_index(times, utc=True).tz_localize(None),  # convert to tz-naive for interpolation
                method='quadratic'
            )
        )

    global_attrs = {
        "title": "Long Term Average Clear-sky Atmospheric Dataset for SPARTA",
        "references": "doi:10.5067/KLICLTZ8EM9D, doi:10.5067/Q9QMY5PBNV1T, doi:10.5067/VJAFPLI1CSIV",
    }

    obj = cls()
    obj._atmosphere = build_atmosphere_on_regular_grid(
        times=times,
        latitude=latitude,
        longitude=longitude,
        constituents=output_dataset.data_vars,
        global_attrs=global_attrs)
    return obj

MERRA-2 via Google Earth Engine

MERRA2GEEAtmosphere

MERRA2GEEAtmosphere()

Bases: BaseAtmosphere

MERRA-2 atmospheric database via Google Earth Engine.

Provides access to NASA MERRA-2 reanalysis via GEE API. Automatically corrects for GEE's latitude grid offset and time stamp convention.

Requires GEE authentication and active project configuration.

See module documentation for setup instructions and examples.

Validates that the database path exists (if specified) and initializes the internal atmosphere dataset to None.

Raises:

Methods:

  • __init_subclass__

    Automatically sets the database path for subclasses.

  • at_site

    Retrieve MERRA-2 data from GEE for a specific site.

  • compute

    Compute clear-sky solar radiation using a radiative transfer model.

Source code in src/spartasolar/atmoslib/_base.py
def __init__(self):
    """Initialize the atmosphere instance.

    Validates that the database path exists (if specified) and initializes
    the internal atmosphere dataset to None.

    Raises
    ------
    AttributeError
        If database_path is specified but does not exist
    """
    if self.database_path is not None and not self.database_path.exists():
        raise AttributeError(f"missing path `{self.database_path}`")

    self._atmosphere: xr.DataArray = None

__init_subclass__

__init_subclass__(database_path: str, **kwargs)

Automatically sets the database path for subclasses.

Parameters:

  • database_path
    (str or None) –

    The directory path where the specific atmosphere data is stored. Pass None for sources that do not use a file database (e.g. CustomAtmosphere or API-based retrievers).

Source code in src/spartasolar/atmoslib/_base.py
def __init_subclass__(cls, database_path: str, **kwargs):
    """Automatically sets the database path for subclasses.

    Parameters
    ----------
    database_path : str or None
        The directory path where the specific atmosphere data is stored.
        Pass ``None`` for sources that do not use a file database
        (e.g. ``CustomAtmosphere`` or API-based retrievers).
    """
    super().__init_subclass__(**kwargs)
    cls.database_path = None if database_path is None else Path(database_path)

at_site classmethod

Retrieve MERRA-2 data from GEE for a specific site.

Downloads data from GEE if not cached, applies spatial/temporal corrections, then interpolates to requested times.

Parameters:

  • times
    (pd.DatetimeIndex or np.ndarray of datetime64) –

    Time stamps for data retrieval (UTC)

  • latitude
    (float) –

    Latitude in degrees North [-90, 90]

  • longitude
    (float) –

    Longitude in degrees East [-180, 180]

  • site_name
    (str, default: None ) –

    Name identifier for the site

Returns:

Examples:

>>> import pandas as pd
>>> from spartasolar import config
>>> config.set_option('merra2_gee.project', 'my-gee-project')
>>>
>>> times = pd.date_range("2020-06-15", periods=24, freq="h")
>>> atm = MERRA2GEEAtmosphere.at_site(
...     times=times,
...     latitude=36.72,
...     longitude=-4.42,
...     site_name="Málaga"
... )
>>> result = atm.compute(model="SPARTA")
Notes
  • Requires merra2_gee.project configuration
  • Automatically corrects GEE's 0.25° latitude offset
  • Adjusts time stamps from hour-start to hour-center
  • Data is cached locally to minimize API calls
Source code in src/spartasolar/atmoslib/merra2_geeapi.py
@classmethod
def at_site(
    cls,
    times: pd.DatetimeIndex | np.ndarray[tuple[int], np.dtype[np.datetime64]],
    latitude: float,
    longitude: float,
    site_name: str | None = None,
) -> Self:
    """Retrieve MERRA-2 data from GEE for a specific site.

    Downloads data from GEE if not cached, applies spatial/temporal
    corrections, then interpolates to requested times.

    Parameters
    ----------
    times : pd.DatetimeIndex or np.ndarray of datetime64
        Time stamps for data retrieval (UTC)
    latitude : float
        Latitude in degrees North [-90, 90]
    longitude : float
        Longitude in degrees East [-180, 180]
    site_name : str, optional
        Name identifier for the site

    Returns
    -------
    MERRA2GEEAtmosphere
        Instance with interpolated atmospheric data

    Examples
    --------
    >>> import pandas as pd
    >>> from spartasolar import config
    >>> config.set_option('merra2_gee.project', 'my-gee-project')
    >>>
    >>> times = pd.date_range("2020-06-15", periods=24, freq="h")
    >>> atm = MERRA2GEEAtmosphere.at_site(
    ...     times=times,
    ...     latitude=36.72,
    ...     longitude=-4.42,
    ...     site_name="Málaga"
    ... )
    >>> result = atm.compute(model="SPARTA")

    Notes
    -----
    - Requires `merra2_gee.project` configuration
    - Automatically corrects GEE's 0.25° latitude offset
    - Adjusts time stamps from hour-start to hour-center
    - Data is cached locally to minimize API calls
    """

    latitude = validate_type(latitude, Latitude)
    longitude = validate_type(longitude, Longitude)

    def fetch_and_distill_and_archive(year: int, path: Path) -> None:
        logger.info(f"fetching GEE data for year={int(year)} and path={path.as_posix()}")
        if not (gee_project := get_option("merra2_gee.project")):
            raise ValueError(
                'missing Google cloud\'s project. Add `project = "<your_gee_project>"` '
                f"in the `merra2_gee` table in `{get_config_path()}` and reload spartasolar "
                "or use `spartasolar.config.set_option('merra2_gee.project', <your_project>)`"
            )
        logger.info(f"using GEE project <green>{gee_project}</green>")
        data = fetch_merra2_data_from_gee_api(
            latitude=latitude,
            longitude=longitude,
            start_date=f"{year}-01-01",
            end_date=f"{year}-12-31",
            project=gee_project,
        )

        data = cls._distill_crude_data(data, latitude, longitude)
        logger.debug(f"{data.head()=}")
        data.to_parquet(path)
        logger.success(f"data downloaded and archived in <blue>{path.name}</blue>")

    # load data from one year before and one year after the requested times_utc, but
    # clipping the years on 2004 and the current year
    paths = []
    times_utc = ensure_tz_aware_datetime_index(times, utc=True)
    years = cls._infer_years_from_times(times_utc)
    for year in sorted(years):
        if not (path := cls._get_filename(year, latitude, longitude)).exists():
            fetch_and_distill_and_archive(year, path)
        paths.append(path)
    logger.debug([path.as_posix() for path in paths])
    data = pd.read_parquet(paths)  # WARNING: times in data are UTC, tz-naive !!!

    # interpolate to times_utc
    x = times_utc.to_numpy(dtype="datetime64[s]").astype(float)
    xi = data.times_utc.to_numpy(dtype="datetime64[s]").astype(float)
    data_i = data.drop(columns=["times_utc", "albedo"])
    y = interp1d(xi, data_i.values, kind="quadratic", axis=0)(x)
    y_dict = {key: value for key, value in zip(data_i.columns, y.T)}
    y_dict["albedo"] = interp1d(xi, data.albedo.values, kind="linear", axis=0)(x)
    data_interp = pd.DataFrame({"times": times_utc} | y_dict)

    global_attrs = {
        "title:": "HourlyEarth Engine Data Catalog dataset for SPARTA",
        "source": "NASA/GMAO MERRA-2 reanalysis via Google Earth Engine API",
        "references": "doi:10.5067/KLICLTZ8EM9D, doi:10.5067/Q9QMY5PBNV1T, doi:10.5067/VJAFPLI1CSIV",
    }

    obj = cls()
    obj._atmosphere = build_atmosphere_of_sites(
        times=times,  # WARNING: return the input times, even if they are not tz-aware!
        latitude=latitude,
        longitude=longitude,
        constituents=data_interp.drop(columns=["times"]),
        site_names=site_name,
        global_attrs=global_attrs,
    )
    return obj

compute

compute(
    model: Model = "SPARTA",
    include_atmosphere: bool = False,
    model_kwargs: dict | None = None,
) -> Dataset

Compute clear-sky solar radiation using a radiative transfer model.

This method integrates solar position calculations with atmospheric constituent data to compute clear-sky irradiance components (GHI, DNI, DHI, etc.) using the specified radiative transfer model.

Parameters:

  • model
    (Model, default: "SPARTA" ) –

    Name of the clear-sky model to use. Options: "SPARTA", "Bird"

  • include_atmosphere
    (bool, default: False ) –

    If True, include atmospheric constituents in the output dataset. If False, only radiation components are returned.

  • model_kwargs
    (dict, default: None ) –

    Additional keyword arguments to pass to the model function

Returns:

  • Dataset

    CF-compliant dataset containing computed irradiance components: - ghi: Global Horizontal Irradiance (W/m²) - dni: Direct Normal Irradiance (W/m²) - dhi or dif: Diffuse Horizontal Irradiance (W/m²) - csi: Circumsolar Irradiance (W/m², SPARTA only)

Examples:

>>> import pandas as pd
>>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
>>>
>>> times = pd.date_range("2020-06-15", periods=24, freq="h")
>>> atm = MERRA2DailyAtmosphere.at_sites(
...     times=times,
...     latitude=36.72,
...     longitude=-4.42
... )
>>> result = atm.compute(model="SPARTA")
>>> print(result.ghi.values)

Use different model with custom parameters:

>>> result = atm.compute(
...     model="Bird",
...     model_kwargs={"scheme": "transmittance_parameterization"}
... )
Notes

The method automatically: - Calculates solar position (zenith angle, Earth-Sun distance) - Converts atmospheric units to model requirements - Handles both gridded and site-based data structures

See Also

spartasolar.modlib.sparta : SPARTA model implementation spartasolar.modlib.bird : Bird clear-sky model

Source code in src/spartasolar/atmoslib/_base.py
def compute(
    self,
    model: Model = "SPARTA",
    include_atmosphere: bool = False,  # TODO: currently ignored
    model_kwargs: dict | None = None,
) -> xr.Dataset:
    """Compute clear-sky solar radiation using a radiative transfer model.

    This method integrates solar position calculations with atmospheric
    constituent data to compute clear-sky irradiance components (GHI, DNI,
    DHI, etc.) using the specified radiative transfer model.

    Parameters
    ----------
    model : Model, default "SPARTA"
        Name of the clear-sky model to use. Options: "SPARTA", "Bird"
    include_atmosphere : bool, default False
        If True, include atmospheric constituents in the output dataset.
        If False, only radiation components are returned.
    model_kwargs : dict, optional
        Additional keyword arguments to pass to the model function

    Returns
    -------
    xr.Dataset
        CF-compliant dataset containing computed irradiance components:
        - ghi: Global Horizontal Irradiance (W/m²)
        - dni: Direct Normal Irradiance (W/m²)
        - dhi or dif: Diffuse Horizontal Irradiance (W/m²)
        - csi: Circumsolar Irradiance (W/m², SPARTA only)

    Examples
    --------
    >>> import pandas as pd
    >>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
    >>>
    >>> times = pd.date_range("2020-06-15", periods=24, freq="h")
    >>> atm = MERRA2DailyAtmosphere.at_sites(
    ...     times=times,
    ...     latitude=36.72,
    ...     longitude=-4.42
    ... )
    >>> result = atm.compute(model="SPARTA")
    >>> print(result.ghi.values)

    Use different model with custom parameters:

    >>> result = atm.compute(
    ...     model="Bird",
    ...     model_kwargs={"scheme": "transmittance_parameterization"}
    ... )

    Notes
    -----
    The method automatically:
    - Calculates solar position (zenith angle, Earth-Sun distance)
    - Converts atmospheric units to model requirements
    - Handles both gridded and site-based data structures

    See Also
    --------
    spartasolar.modlib.sparta : SPARTA model implementation
    spartasolar.modlib.bird : Bird clear-sky model
    """

    model = validate_type(model, Model)
    model_func = getattr(modlib, model)
    model_vars = inspect.getfullargspec(model_func).args

    is_regular_grid = "site" not in self.dataset.dims

    # compute solar geometry...
    sw_eval = sunwhere.regular_grid if is_regular_grid else sunwhere.sites
    solpos = sw_eval(
        # self.dataset.time.values,  # WARNING: this returns a utc tz-naive numpy array of dtype datetime64[ns] !!!
        self.dataset.indexes["time"],  # NOTE: with `indexes` we get a pandas DatetimeIndex with tz info preserved!
        latitude=self.dataset.lat.values,
        longitude=self.dataset.lon.values,
        algorithm=config.get_option("sunwhere.algorithm", default="psa"),
        refraction=config.get_option("sunwhere.refraction", default=True),
        engine=config.get_option("sunwhere.engine", default="numexpr")
    )

    new_coord_names = {"location": "site", "latitude": "lat", "longitude": "lon"}
    cosz = solpos.cosz.rename({old: new for old, new in new_coord_names.items() if old in solpos.cosz.dims})

    if is_regular_grid:
        n_lats = self.dataset.sizes["lat"]
        n_lons = self.dataset.sizes["lon"]
        ecf = solpos.ecf.expand_dims(dim={"lat": n_lats, "lon": n_lons}, axis=(1, 2))
    else:
        n_locs = self.dataset.sizes["site"]
        ecf = solpos.ecf.expand_dims(dim={"site": n_locs}, axis=1)

    kwargs = {"cosz": cosz, "ecf": ecf}

    # atmosphere...
    def get_with_proper_units(var):
        if var not in self.dataset.data_vars:
            raise ValueError(f"variable `{var}` required by the model `{model}` is not available in this atmosphere")
        if var == "pressure":
            return self.dataset[var] * 1e-2  # Pa to hPa
        if var == "pwater":
            return pwater_in_kg_m2_to_cm(self.dataset[var])
        if var == "ozone":
            return ozone_in_kg_m2_to_cm(self.dataset[var])
        return self.dataset[var]
    variables = set(model_vars).intersection(self.dataset.data_vars)  # variables required by this model
    kwargs = kwargs | {var: get_with_proper_units(var) for var in variables}

    # and call the clearsky model...
    result = model_func(**(kwargs | (model_kwargs or {})))

    # encapsulate the result in a CF-compliant xarray Dataset
    if is_regular_grid:
        return build_atmosphere_on_regular_grid(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            global_attrs=get_global_attrs(feature_type="grid")
        )
    else:
        site_values = self.dataset.coords.get("site").values
        n_sites = self.dataset.sizes["site"]
        site_names = validate_site_names(site_values, n_sites)
        return build_atmosphere_of_sites(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            site_names=site_names,
            global_attrs=get_global_attrs(feature_type="timeSeries")
        )

Copernicus CRS via SODA API

CRSSODAAtmosphere

CRSSODAAtmosphere()

Bases: BaseAtmosphere

CRS SODA (Copernicus Radiation Service) atmospheric database.

Provides access to atmospheric constituent data from the SODA McClear service via WPS API. Data is cached locally after first retrieval.

Requires user registration at https://www.soda-pro.com/ and email configuration via crs_soda.user_email config option.

See module documentation for examples.

Validates that the database path exists (if specified) and initializes the internal atmosphere dataset to None.

Raises:

Methods:

  • __init_subclass__

    Automatically sets the database path for subclasses.

  • at_site

    Retrieve CRS SODA atmospheric data for a specific site.

  • compute

    Compute clear-sky solar radiation using a radiative transfer model.

Source code in src/spartasolar/atmoslib/_base.py
def __init__(self):
    """Initialize the atmosphere instance.

    Validates that the database path exists (if specified) and initializes
    the internal atmosphere dataset to None.

    Raises
    ------
    AttributeError
        If database_path is specified but does not exist
    """
    if self.database_path is not None and not self.database_path.exists():
        raise AttributeError(f"missing path `{self.database_path}`")

    self._atmosphere: xr.DataArray = None

__init_subclass__

__init_subclass__(database_path: str, **kwargs)

Automatically sets the database path for subclasses.

Parameters:

  • database_path
    (str or None) –

    The directory path where the specific atmosphere data is stored. Pass None for sources that do not use a file database (e.g. CustomAtmosphere or API-based retrievers).

Source code in src/spartasolar/atmoslib/_base.py
def __init_subclass__(cls, database_path: str, **kwargs):
    """Automatically sets the database path for subclasses.

    Parameters
    ----------
    database_path : str or None
        The directory path where the specific atmosphere data is stored.
        Pass ``None`` for sources that do not use a file database
        (e.g. ``CustomAtmosphere`` or API-based retrievers).
    """
    super().__init_subclass__(**kwargs)
    cls.database_path = None if database_path is None else Path(database_path)

at_site classmethod

Retrieve CRS SODA atmospheric data for a specific site.

Downloads data from SODA API if not cached, then interpolates to requested times. Data is automatically resampled to hourly resolution.

Parameters:

  • times
    (DatetimeIndex) –

    Time stamps for data retrieval

  • latitude
    (float) –

    Latitude in degrees North [-90, 90]

  • longitude
    (float) –

    Longitude in degrees East [-180, 180]

  • site_name
    (str, default: None ) –

    Name identifier for the site

Returns:

Examples:

>>> import pandas as pd
>>> from spartasolar import config
>>> config.set_option('crs_soda.user_email', 'user@example.com')
>>>
>>> times = pd.date_range("2020-01-01", "2020-01-31", freq="h")
>>> atm = CRSSODAAtmosphere.at_site(
...     times=times,
...     latitude=36.72,
...     longitude=-4.42,
...     site_name="Málaga"
... )
>>> result = atm.compute(model="SPARTA")
Notes
  • Requires crs_soda.user_email configuration
  • Data is cached locally to minimize API calls
  • Temporal interpolation uses quadratic splines
Source code in src/spartasolar/atmoslib/crs_sodaapi.py
@classmethod
def at_site(
    cls,
    times: pd.DatetimeIndex | np.ndarray[tuple[int], np.dtype[np.datetime64]],
    latitude: float,
    longitude: float,
    site_name: str | None = None,
) -> Self:
    """Retrieve CRS SODA atmospheric data for a specific site.

    Downloads data from SODA API if not cached, then interpolates to
    requested times. Data is automatically resampled to hourly resolution.

    Parameters
    ----------
    times : pd.DatetimeIndex
        Time stamps for data retrieval
    latitude : float
        Latitude in degrees North [-90, 90]
    longitude : float
        Longitude in degrees East [-180, 180]
    site_name : str, optional
        Name identifier for the site

    Returns
    -------
    CRSSODAAtmosphere
        Instance with interpolated atmospheric data

    Examples
    --------
    >>> import pandas as pd
    >>> from spartasolar import config
    >>> config.set_option('crs_soda.user_email', 'user@example.com')
    >>>
    >>> times = pd.date_range("2020-01-01", "2020-01-31", freq="h")
    >>> atm = CRSSODAAtmosphere.at_site(
    ...     times=times,
    ...     latitude=36.72,
    ...     longitude=-4.42,
    ...     site_name="Málaga"
    ... )
    >>> result = atm.compute(model="SPARTA")

    Notes
    -----
    - Requires `crs_soda.user_email` configuration
    - Data is cached locally to minimize API calls
    - Temporal interpolation uses quadratic splines
    """

    save_csv: bool = False  # for debugging and reproducibility. The csv files are saved in the same directory as the parquet files, with the same name but .csv extension instead of .parquet

    latitude = validate_type(latitude, Latitude)
    longitude = validate_type(longitude, Longitude)

    def fetch_and_distill_and_archive(year: int, path: Path) -> None:
        if (user_email := get_option("crs_soda.user_email")) is None:
            raise ValueError("missing soda user. Add `user_email = \"<your_email_for_crs_soda>\"` in "
                             f"the `crs_soda` table in `{get_config_path()}` and reload spartasolar or use "
                             "`spartasolar.config.set_option(\'crs_soda.user_email\', <your_email_for_crs_soda>)`")
        data, metadata = fetch_crs_data_from_soda_api(
            latitude=latitude,
            longitude=longitude,
            date_begin=f"{year}-01-01",
            date_end=f"{year}-12-31",
            user_email=user_email,
            time_step="PT01M",
            stream="mcclear",
            version=cls.CRS_VERSION,
            timeout=30,
            to_csv=path.with_suffix(".csv") if save_csv else None)  # save the raw response as csv for debugging and reproducibility

        data = cls._distill_crude_data(data, metadata)
        logger.debug(f"{data.head()=}")
        data.to_parquet(path)
        logger.success(f"data downloaded and archived: <blue>{path.name}</blue>")

    # load data from one year before and one year after the requested times_utc, but
    # clipping the years on 2004 and the current year
    paths = []
    times_utc = ensure_tz_aware_datetime_index(times, utc=True)
    years = cls._infer_years_from_times(times_utc)
    for year in sorted(years):
        if not (path := cls._get_filename(year, latitude, longitude)).exists():
            fetch_and_distill_and_archive(year, path)
        paths.append(path)
    logger.debug([path.as_posix() for path in paths])
    data = pd.read_parquet(paths)  # WARNING: times in data are UTC !!!

    # interpolate to times_utc
    x = times_utc.to_numpy(dtype="datetime64[s]").astype(float)
    xi = data.times_utc.to_numpy(dtype="datetime64[s]").astype(float)
    data_i = data.drop(columns=["times_utc", "albedo"])
    y = interp1d(xi, data_i.values, kind="quadratic", axis=0)(x)
    y_dict = {key: value for key, value in zip(data_i.columns, y.T)}
    y_dict["albedo"] = interp1d(xi, data.albedo.values, kind="linear", axis=0)(x)
    data_interp = pd.DataFrame({"times": times_utc} | y_dict)

    global_attrs = {
        "title:": "Hourly CRS SODA McClear dataset for SPARTA",
        "source": "WPS SODA API, https://www.soda-pro.com/web-services/radiation/cams-mcclear",
        "version": cls.CRS_VERSION,
        "references": "https://confluence.ecmwf.int/display/CKB/CAMS+solar+radiation+time-series%3A+data+documentation",
    }

    obj = cls()
    obj._atmosphere = build_atmosphere_of_sites(
        times=times,  # WARNING: return the input times, even if they are not tz-aware!
        latitude=latitude,
        longitude=longitude,
        constituents=data_interp.drop(columns=["times"]),
        site_names=site_name,
        global_attrs=global_attrs)
    return obj

compute

compute(
    model: Model = "SPARTA",
    include_atmosphere: bool = False,
    model_kwargs: dict | None = None,
) -> Dataset

Compute clear-sky solar radiation using a radiative transfer model.

This method integrates solar position calculations with atmospheric constituent data to compute clear-sky irradiance components (GHI, DNI, DHI, etc.) using the specified radiative transfer model.

Parameters:

  • model
    (Model, default: "SPARTA" ) –

    Name of the clear-sky model to use. Options: "SPARTA", "Bird"

  • include_atmosphere
    (bool, default: False ) –

    If True, include atmospheric constituents in the output dataset. If False, only radiation components are returned.

  • model_kwargs
    (dict, default: None ) –

    Additional keyword arguments to pass to the model function

Returns:

  • Dataset

    CF-compliant dataset containing computed irradiance components: - ghi: Global Horizontal Irradiance (W/m²) - dni: Direct Normal Irradiance (W/m²) - dhi or dif: Diffuse Horizontal Irradiance (W/m²) - csi: Circumsolar Irradiance (W/m², SPARTA only)

Examples:

>>> import pandas as pd
>>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
>>>
>>> times = pd.date_range("2020-06-15", periods=24, freq="h")
>>> atm = MERRA2DailyAtmosphere.at_sites(
...     times=times,
...     latitude=36.72,
...     longitude=-4.42
... )
>>> result = atm.compute(model="SPARTA")
>>> print(result.ghi.values)

Use different model with custom parameters:

>>> result = atm.compute(
...     model="Bird",
...     model_kwargs={"scheme": "transmittance_parameterization"}
... )
Notes

The method automatically: - Calculates solar position (zenith angle, Earth-Sun distance) - Converts atmospheric units to model requirements - Handles both gridded and site-based data structures

See Also

spartasolar.modlib.sparta : SPARTA model implementation spartasolar.modlib.bird : Bird clear-sky model

Source code in src/spartasolar/atmoslib/_base.py
def compute(
    self,
    model: Model = "SPARTA",
    include_atmosphere: bool = False,  # TODO: currently ignored
    model_kwargs: dict | None = None,
) -> xr.Dataset:
    """Compute clear-sky solar radiation using a radiative transfer model.

    This method integrates solar position calculations with atmospheric
    constituent data to compute clear-sky irradiance components (GHI, DNI,
    DHI, etc.) using the specified radiative transfer model.

    Parameters
    ----------
    model : Model, default "SPARTA"
        Name of the clear-sky model to use. Options: "SPARTA", "Bird"
    include_atmosphere : bool, default False
        If True, include atmospheric constituents in the output dataset.
        If False, only radiation components are returned.
    model_kwargs : dict, optional
        Additional keyword arguments to pass to the model function

    Returns
    -------
    xr.Dataset
        CF-compliant dataset containing computed irradiance components:
        - ghi: Global Horizontal Irradiance (W/m²)
        - dni: Direct Normal Irradiance (W/m²)
        - dhi or dif: Diffuse Horizontal Irradiance (W/m²)
        - csi: Circumsolar Irradiance (W/m², SPARTA only)

    Examples
    --------
    >>> import pandas as pd
    >>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
    >>>
    >>> times = pd.date_range("2020-06-15", periods=24, freq="h")
    >>> atm = MERRA2DailyAtmosphere.at_sites(
    ...     times=times,
    ...     latitude=36.72,
    ...     longitude=-4.42
    ... )
    >>> result = atm.compute(model="SPARTA")
    >>> print(result.ghi.values)

    Use different model with custom parameters:

    >>> result = atm.compute(
    ...     model="Bird",
    ...     model_kwargs={"scheme": "transmittance_parameterization"}
    ... )

    Notes
    -----
    The method automatically:
    - Calculates solar position (zenith angle, Earth-Sun distance)
    - Converts atmospheric units to model requirements
    - Handles both gridded and site-based data structures

    See Also
    --------
    spartasolar.modlib.sparta : SPARTA model implementation
    spartasolar.modlib.bird : Bird clear-sky model
    """

    model = validate_type(model, Model)
    model_func = getattr(modlib, model)
    model_vars = inspect.getfullargspec(model_func).args

    is_regular_grid = "site" not in self.dataset.dims

    # compute solar geometry...
    sw_eval = sunwhere.regular_grid if is_regular_grid else sunwhere.sites
    solpos = sw_eval(
        # self.dataset.time.values,  # WARNING: this returns a utc tz-naive numpy array of dtype datetime64[ns] !!!
        self.dataset.indexes["time"],  # NOTE: with `indexes` we get a pandas DatetimeIndex with tz info preserved!
        latitude=self.dataset.lat.values,
        longitude=self.dataset.lon.values,
        algorithm=config.get_option("sunwhere.algorithm", default="psa"),
        refraction=config.get_option("sunwhere.refraction", default=True),
        engine=config.get_option("sunwhere.engine", default="numexpr")
    )

    new_coord_names = {"location": "site", "latitude": "lat", "longitude": "lon"}
    cosz = solpos.cosz.rename({old: new for old, new in new_coord_names.items() if old in solpos.cosz.dims})

    if is_regular_grid:
        n_lats = self.dataset.sizes["lat"]
        n_lons = self.dataset.sizes["lon"]
        ecf = solpos.ecf.expand_dims(dim={"lat": n_lats, "lon": n_lons}, axis=(1, 2))
    else:
        n_locs = self.dataset.sizes["site"]
        ecf = solpos.ecf.expand_dims(dim={"site": n_locs}, axis=1)

    kwargs = {"cosz": cosz, "ecf": ecf}

    # atmosphere...
    def get_with_proper_units(var):
        if var not in self.dataset.data_vars:
            raise ValueError(f"variable `{var}` required by the model `{model}` is not available in this atmosphere")
        if var == "pressure":
            return self.dataset[var] * 1e-2  # Pa to hPa
        if var == "pwater":
            return pwater_in_kg_m2_to_cm(self.dataset[var])
        if var == "ozone":
            return ozone_in_kg_m2_to_cm(self.dataset[var])
        return self.dataset[var]
    variables = set(model_vars).intersection(self.dataset.data_vars)  # variables required by this model
    kwargs = kwargs | {var: get_with_proper_units(var) for var in variables}

    # and call the clearsky model...
    result = model_func(**(kwargs | (model_kwargs or {})))

    # encapsulate the result in a CF-compliant xarray Dataset
    if is_regular_grid:
        return build_atmosphere_on_regular_grid(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            global_attrs=get_global_attrs(feature_type="grid")
        )
    else:
        site_values = self.dataset.coords.get("site").values
        n_sites = self.dataset.sizes["site"]
        site_names = validate_site_names(site_values, n_sites)
        return build_atmosphere_of_sites(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            site_names=site_names,
            global_attrs=get_global_attrs(feature_type="timeSeries")
        )

Custom atmosphere

CustomAtmosphere

CustomAtmosphere()

Bases: BaseAtmosphere

Custom atmospheric data provider.

This class allows users to supply their own atmospheric constituent data from measurements, numerical weather prediction models, or other sources. Data can be provided for specific sites (time series) or regular grids.

See module documentation for examples.

Validates that the database path exists (if specified) and initializes the internal atmosphere dataset to None.

Raises:

Methods:

  • __init_subclass__

    Automatically sets the database path for subclasses.

  • at_sites

    Create custom atmospheric data for specific sites.

  • compute

    Compute clear-sky solar radiation using a radiative transfer model.

  • on_regular_grid

    Create custom atmospheric data on a regular spatial grid.

Source code in src/spartasolar/atmoslib/_base.py
def __init__(self):
    """Initialize the atmosphere instance.

    Validates that the database path exists (if specified) and initializes
    the internal atmosphere dataset to None.

    Raises
    ------
    AttributeError
        If database_path is specified but does not exist
    """
    if self.database_path is not None and not self.database_path.exists():
        raise AttributeError(f"missing path `{self.database_path}`")

    self._atmosphere: xr.DataArray = None

__init_subclass__

__init_subclass__(database_path: str, **kwargs)

Automatically sets the database path for subclasses.

Parameters:

  • database_path
    (str or None) –

    The directory path where the specific atmosphere data is stored. Pass None for sources that do not use a file database (e.g. CustomAtmosphere or API-based retrievers).

Source code in src/spartasolar/atmoslib/_base.py
def __init_subclass__(cls, database_path: str, **kwargs):
    """Automatically sets the database path for subclasses.

    Parameters
    ----------
    database_path : str or None
        The directory path where the specific atmosphere data is stored.
        Pass ``None`` for sources that do not use a file database
        (e.g. ``CustomAtmosphere`` or API-based retrievers).
    """
    super().__init_subclass__(**kwargs)
    cls.database_path = None if database_path is None else Path(database_path)

at_sites classmethod

Create custom atmospheric data for specific sites.

Parameters:

  • times
    (ndarray or DatetimeIndex) –

    Time stamps for the data (length n_times)

  • latitude
    (float or Sequence[float]) –

    Latitude(s) in degrees North [-90, 90] (length n_sites)

  • longitude
    (float or Sequence[float]) –

    Longitude(s) in degrees East [-180, 180] (length n_sites)

  • constituents
    (dict[str, ndarray]) –

    Atmospheric variables as 2D arrays with shape (n_times, n_sites). Standard variable names: 'pressure' (Pa), 'pwater' (cm), 'ozone' (atm-cm), 'alpha', 'beta', 'ssa', 'albedo'

  • site_names
    (Sequence[str], default: None ) –

    Names for each site

  • var_attrs
    (dict, default: None ) –

    Custom attributes for variables (CF conventions)

  • global_attrs
    (dict, default: None ) –

    Custom global attributes for the dataset

Returns:

Examples:

>>> times = pd.date_range("2020-01-01", periods=24, freq="h")
>>> atm = CustomAtmosphere.at_sites(
...     times=times,
...     latitude=36.72,
...     longitude=-4.42,
...     constituents={
...         "pressure": np.full((24, 1), 101325.0),
...         "pwater": np.linspace(1.0, 2.0, 24).reshape(24, 1),
...         "ozone": np.full((24, 1), 0.3)
...     }
... )
Source code in src/spartasolar/atmoslib/custom.py
@classmethod
def at_sites(
    cls,
    times: np.ndarray[tuple[int], np.dtype[np.datetime64]] | pd.DatetimeIndex,
    latitude: Sequence[float] | float,
    longitude: Sequence[float] | float,
    constituents: dict[str, np.ndarray[tuple[int, int], float]],  # shape: (time, site)
    site_names: Sequence[str] | None = None,
    var_attrs: dict | None = None,
    global_attrs: dict | None = None,
) -> Self:
    """Create custom atmospheric data for specific sites.

    Parameters
    ----------
    times : np.ndarray or pd.DatetimeIndex
        Time stamps for the data (length n_times)
    latitude : float or Sequence[float]
        Latitude(s) in degrees North [-90, 90] (length n_sites)
    longitude : float or Sequence[float]
        Longitude(s) in degrees East [-180, 180] (length n_sites)
    constituents : dict[str, np.ndarray]
        Atmospheric variables as 2D arrays with shape (n_times, n_sites).
        Standard variable names: 'pressure' (Pa), 'pwater' (cm), 'ozone' (atm-cm),
        'alpha', 'beta', 'ssa', 'albedo'
    site_names : Sequence[str], optional
        Names for each site
    var_attrs : dict, optional
        Custom attributes for variables (CF conventions)
    global_attrs : dict, optional
        Custom global attributes for the dataset

    Returns
    -------
    CustomAtmosphere
        Instance with atmospheric data loaded

    Examples
    --------
    >>> times = pd.date_range("2020-01-01", periods=24, freq="h")
    >>> atm = CustomAtmosphere.at_sites(
    ...     times=times,
    ...     latitude=36.72,
    ...     longitude=-4.42,
    ...     constituents={
    ...         "pressure": np.full((24, 1), 101325.0),
    ...         "pwater": np.linspace(1.0, 2.0, 24).reshape(24, 1),
    ...         "ozone": np.full((24, 1), 0.3)
    ...     }
    ... )
    """

    obj = cls()
    obj._atmosphere = build_atmosphere_of_sites(
        times=times,
        latitude=latitude,
        longitude=longitude,
        constituents=constituents,
        site_names=site_names,
        var_attrs=var_attrs,
        global_attrs=global_attrs)
    return obj

compute

compute(
    model: Model = "SPARTA",
    include_atmosphere: bool = False,
    model_kwargs: dict | None = None,
) -> Dataset

Compute clear-sky solar radiation using a radiative transfer model.

This method integrates solar position calculations with atmospheric constituent data to compute clear-sky irradiance components (GHI, DNI, DHI, etc.) using the specified radiative transfer model.

Parameters:

  • model
    (Model, default: "SPARTA" ) –

    Name of the clear-sky model to use. Options: "SPARTA", "Bird"

  • include_atmosphere
    (bool, default: False ) –

    If True, include atmospheric constituents in the output dataset. If False, only radiation components are returned.

  • model_kwargs
    (dict, default: None ) –

    Additional keyword arguments to pass to the model function

Returns:

  • Dataset

    CF-compliant dataset containing computed irradiance components: - ghi: Global Horizontal Irradiance (W/m²) - dni: Direct Normal Irradiance (W/m²) - dhi or dif: Diffuse Horizontal Irradiance (W/m²) - csi: Circumsolar Irradiance (W/m², SPARTA only)

Examples:

>>> import pandas as pd
>>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
>>>
>>> times = pd.date_range("2020-06-15", periods=24, freq="h")
>>> atm = MERRA2DailyAtmosphere.at_sites(
...     times=times,
...     latitude=36.72,
...     longitude=-4.42
... )
>>> result = atm.compute(model="SPARTA")
>>> print(result.ghi.values)

Use different model with custom parameters:

>>> result = atm.compute(
...     model="Bird",
...     model_kwargs={"scheme": "transmittance_parameterization"}
... )
Notes

The method automatically: - Calculates solar position (zenith angle, Earth-Sun distance) - Converts atmospheric units to model requirements - Handles both gridded and site-based data structures

See Also

spartasolar.modlib.sparta : SPARTA model implementation spartasolar.modlib.bird : Bird clear-sky model

Source code in src/spartasolar/atmoslib/_base.py
def compute(
    self,
    model: Model = "SPARTA",
    include_atmosphere: bool = False,  # TODO: currently ignored
    model_kwargs: dict | None = None,
) -> xr.Dataset:
    """Compute clear-sky solar radiation using a radiative transfer model.

    This method integrates solar position calculations with atmospheric
    constituent data to compute clear-sky irradiance components (GHI, DNI,
    DHI, etc.) using the specified radiative transfer model.

    Parameters
    ----------
    model : Model, default "SPARTA"
        Name of the clear-sky model to use. Options: "SPARTA", "Bird"
    include_atmosphere : bool, default False
        If True, include atmospheric constituents in the output dataset.
        If False, only radiation components are returned.
    model_kwargs : dict, optional
        Additional keyword arguments to pass to the model function

    Returns
    -------
    xr.Dataset
        CF-compliant dataset containing computed irradiance components:
        - ghi: Global Horizontal Irradiance (W/m²)
        - dni: Direct Normal Irradiance (W/m²)
        - dhi or dif: Diffuse Horizontal Irradiance (W/m²)
        - csi: Circumsolar Irradiance (W/m², SPARTA only)

    Examples
    --------
    >>> import pandas as pd
    >>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
    >>>
    >>> times = pd.date_range("2020-06-15", periods=24, freq="h")
    >>> atm = MERRA2DailyAtmosphere.at_sites(
    ...     times=times,
    ...     latitude=36.72,
    ...     longitude=-4.42
    ... )
    >>> result = atm.compute(model="SPARTA")
    >>> print(result.ghi.values)

    Use different model with custom parameters:

    >>> result = atm.compute(
    ...     model="Bird",
    ...     model_kwargs={"scheme": "transmittance_parameterization"}
    ... )

    Notes
    -----
    The method automatically:
    - Calculates solar position (zenith angle, Earth-Sun distance)
    - Converts atmospheric units to model requirements
    - Handles both gridded and site-based data structures

    See Also
    --------
    spartasolar.modlib.sparta : SPARTA model implementation
    spartasolar.modlib.bird : Bird clear-sky model
    """

    model = validate_type(model, Model)
    model_func = getattr(modlib, model)
    model_vars = inspect.getfullargspec(model_func).args

    is_regular_grid = "site" not in self.dataset.dims

    # compute solar geometry...
    sw_eval = sunwhere.regular_grid if is_regular_grid else sunwhere.sites
    solpos = sw_eval(
        # self.dataset.time.values,  # WARNING: this returns a utc tz-naive numpy array of dtype datetime64[ns] !!!
        self.dataset.indexes["time"],  # NOTE: with `indexes` we get a pandas DatetimeIndex with tz info preserved!
        latitude=self.dataset.lat.values,
        longitude=self.dataset.lon.values,
        algorithm=config.get_option("sunwhere.algorithm", default="psa"),
        refraction=config.get_option("sunwhere.refraction", default=True),
        engine=config.get_option("sunwhere.engine", default="numexpr")
    )

    new_coord_names = {"location": "site", "latitude": "lat", "longitude": "lon"}
    cosz = solpos.cosz.rename({old: new for old, new in new_coord_names.items() if old in solpos.cosz.dims})

    if is_regular_grid:
        n_lats = self.dataset.sizes["lat"]
        n_lons = self.dataset.sizes["lon"]
        ecf = solpos.ecf.expand_dims(dim={"lat": n_lats, "lon": n_lons}, axis=(1, 2))
    else:
        n_locs = self.dataset.sizes["site"]
        ecf = solpos.ecf.expand_dims(dim={"site": n_locs}, axis=1)

    kwargs = {"cosz": cosz, "ecf": ecf}

    # atmosphere...
    def get_with_proper_units(var):
        if var not in self.dataset.data_vars:
            raise ValueError(f"variable `{var}` required by the model `{model}` is not available in this atmosphere")
        if var == "pressure":
            return self.dataset[var] * 1e-2  # Pa to hPa
        if var == "pwater":
            return pwater_in_kg_m2_to_cm(self.dataset[var])
        if var == "ozone":
            return ozone_in_kg_m2_to_cm(self.dataset[var])
        return self.dataset[var]
    variables = set(model_vars).intersection(self.dataset.data_vars)  # variables required by this model
    kwargs = kwargs | {var: get_with_proper_units(var) for var in variables}

    # and call the clearsky model...
    result = model_func(**(kwargs | (model_kwargs or {})))

    # encapsulate the result in a CF-compliant xarray Dataset
    if is_regular_grid:
        return build_atmosphere_on_regular_grid(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            global_attrs=get_global_attrs(feature_type="grid")
        )
    else:
        site_values = self.dataset.coords.get("site").values
        n_sites = self.dataset.sizes["site"]
        site_names = validate_site_names(site_values, n_sites)
        return build_atmosphere_of_sites(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            site_names=site_names,
            global_attrs=get_global_attrs(feature_type="timeSeries")
        )

on_regular_grid classmethod

Create custom atmospheric data on a regular spatial grid.

Parameters:

  • times
    (ndarray or DatetimeIndex) –

    Time stamps for the data (length n_times)

  • latitude
    (Sequence[float]) –

    Latitude coordinates in degrees North (length n_lats)

  • longitude
    (Sequence[float]) –

    Longitude coordinates in degrees East (length n_lons)

  • constituents
    (dict[str, ndarray]) –

    Atmospheric variables as 3D arrays with shape (n_times, n_lats, n_lons). Standard names: 'pressure', 'pwater', 'ozone', 'alpha', 'beta', 'ssa', 'albedo'

  • var_attrs
    (dict, default: None ) –

    Custom variable attributes

  • global_attrs
    (dict, default: None ) –

    Custom global dataset attributes

Returns:

Examples:

>>> lats = np.linspace(36.0, 41.0, 20)
>>> lons = np.linspace(-5.0, -3.0, 20)
>>> times = pd.date_range("2020-06-15", periods=5, freq="h")
>>> atm = CustomAtmosphere.on_regular_grid(
...     times=times,
...     latitude=lats,
...     longitude=lons,
...     constituents={
...         "pressure": np.full((5, 20, 20), 101325.0),
...         "pwater": np.random.uniform(1.0, 3.0, (5, 20, 20)),
...     }
... )
Source code in src/spartasolar/atmoslib/custom.py
@classmethod
def on_regular_grid(
    cls,
    times: np.ndarray[tuple[int], np.datetime64] | pd.DatetimeIndex,
    latitude: Sequence[float],
    longitude: Sequence[float],
    constituents: dict[str, np.ndarray[tuple[int, int, int], float]],  # shape: (time, lat, lon)
    var_attrs: dict | None = None,
    global_attrs: dict | None = None,
) -> Self:
    """Create custom atmospheric data on a regular spatial grid.

    Parameters
    ----------
    times : np.ndarray or pd.DatetimeIndex
        Time stamps for the data (length n_times)
    latitude : Sequence[float]
        Latitude coordinates in degrees North (length n_lats)
    longitude : Sequence[float]
        Longitude coordinates in degrees East (length n_lons)
    constituents : dict[str, np.ndarray]
        Atmospheric variables as 3D arrays with shape (n_times, n_lats, n_lons).
        Standard names: 'pressure', 'pwater', 'ozone', 'alpha', 'beta', 'ssa', 'albedo'
    var_attrs : dict, optional
        Custom variable attributes
    global_attrs : dict, optional
        Custom global dataset attributes

    Returns
    -------
    CustomAtmosphere
        Instance with gridded atmospheric data

    Examples
    --------
    >>> lats = np.linspace(36.0, 41.0, 20)
    >>> lons = np.linspace(-5.0, -3.0, 20)
    >>> times = pd.date_range("2020-06-15", periods=5, freq="h")
    >>> atm = CustomAtmosphere.on_regular_grid(
    ...     times=times,
    ...     latitude=lats,
    ...     longitude=lons,
    ...     constituents={
    ...         "pressure": np.full((5, 20, 20), 101325.0),
    ...         "pwater": np.random.uniform(1.0, 3.0, (5, 20, 20)),
    ...     }
    ... )
    """

    obj = cls()
    obj._atmosphere = build_atmosphere_on_regular_grid(
        times=times,
        latitude=latitude,
        longitude=longitude,
        constituents=constituents,
        var_attrs=var_attrs,
        global_attrs=global_attrs)
    return obj

Base class

BaseAtmosphere

BaseAtmosphere()

Abstract base class for atmospheric database interfaces.

This class defines the common interface that all atmospheric data sources must implement. It provides methods for loading atmospheric constituent data and computing clear-sky solar radiation using various radiative transfer models.

Subclasses must implement methods to retrieve atmospheric data either at specific sites (time series) or on regular grids. The class handles: - Loading and validation of atmospheric datasets - Integration with solar position calculations - Execution of clear-sky radiation models (SPARTA, Bird, etc.) - CF-compliant metadata management

Attributes:

  • database_path (Path or None) –

    Directory containing the database files (set by subclass)

  • dataset (Dataset) –

    The loaded atmospheric dataset

Examples:

Subclasses should define database_path and implement data retrieval:

>>> class MyAtmosphere(BaseAtmosphere, database_path="/path/to/data"):
...     @classmethod
...     def at_sites(cls, times, latitude, longitude, **kwargs):
...         # Implementation here
...         pass
Notes

This is an abstract base class and cannot be instantiated directly. Use concrete implementations like MERRA2DailyAtmosphere or CustomAtmosphere.

See Also

MERRA2DailyAtmosphere : Most commonly used implementation CustomAtmosphere : For user-provided data

Validates that the database path exists (if specified) and initializes the internal atmosphere dataset to None.

Raises:

Methods:

  • __init_subclass__

    Automatically sets the database path for subclasses.

  • compute

    Compute clear-sky solar radiation using a radiative transfer model.

Source code in src/spartasolar/atmoslib/_base.py
def __init__(self):
    """Initialize the atmosphere instance.

    Validates that the database path exists (if specified) and initializes
    the internal atmosphere dataset to None.

    Raises
    ------
    AttributeError
        If database_path is specified but does not exist
    """
    if self.database_path is not None and not self.database_path.exists():
        raise AttributeError(f"missing path `{self.database_path}`")

    self._atmosphere: xr.DataArray = None

__init_subclass__

__init_subclass__(database_path: str, **kwargs)

Automatically sets the database path for subclasses.

Parameters:

  • database_path
    (str or None) –

    The directory path where the specific atmosphere data is stored. Pass None for sources that do not use a file database (e.g. CustomAtmosphere or API-based retrievers).

Source code in src/spartasolar/atmoslib/_base.py
def __init_subclass__(cls, database_path: str, **kwargs):
    """Automatically sets the database path for subclasses.

    Parameters
    ----------
    database_path : str or None
        The directory path where the specific atmosphere data is stored.
        Pass ``None`` for sources that do not use a file database
        (e.g. ``CustomAtmosphere`` or API-based retrievers).
    """
    super().__init_subclass__(**kwargs)
    cls.database_path = None if database_path is None else Path(database_path)

compute

compute(
    model: Model = "SPARTA",
    include_atmosphere: bool = False,
    model_kwargs: dict | None = None,
) -> Dataset

Compute clear-sky solar radiation using a radiative transfer model.

This method integrates solar position calculations with atmospheric constituent data to compute clear-sky irradiance components (GHI, DNI, DHI, etc.) using the specified radiative transfer model.

Parameters:

  • model
    (Model, default: "SPARTA" ) –

    Name of the clear-sky model to use. Options: "SPARTA", "Bird"

  • include_atmosphere
    (bool, default: False ) –

    If True, include atmospheric constituents in the output dataset. If False, only radiation components are returned.

  • model_kwargs
    (dict, default: None ) –

    Additional keyword arguments to pass to the model function

Returns:

  • Dataset

    CF-compliant dataset containing computed irradiance components: - ghi: Global Horizontal Irradiance (W/m²) - dni: Direct Normal Irradiance (W/m²) - dhi or dif: Diffuse Horizontal Irradiance (W/m²) - csi: Circumsolar Irradiance (W/m², SPARTA only)

Examples:

>>> import pandas as pd
>>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
>>>
>>> times = pd.date_range("2020-06-15", periods=24, freq="h")
>>> atm = MERRA2DailyAtmosphere.at_sites(
...     times=times,
...     latitude=36.72,
...     longitude=-4.42
... )
>>> result = atm.compute(model="SPARTA")
>>> print(result.ghi.values)

Use different model with custom parameters:

>>> result = atm.compute(
...     model="Bird",
...     model_kwargs={"scheme": "transmittance_parameterization"}
... )
Notes

The method automatically: - Calculates solar position (zenith angle, Earth-Sun distance) - Converts atmospheric units to model requirements - Handles both gridded and site-based data structures

See Also

spartasolar.modlib.sparta : SPARTA model implementation spartasolar.modlib.bird : Bird clear-sky model

Source code in src/spartasolar/atmoslib/_base.py
def compute(
    self,
    model: Model = "SPARTA",
    include_atmosphere: bool = False,  # TODO: currently ignored
    model_kwargs: dict | None = None,
) -> xr.Dataset:
    """Compute clear-sky solar radiation using a radiative transfer model.

    This method integrates solar position calculations with atmospheric
    constituent data to compute clear-sky irradiance components (GHI, DNI,
    DHI, etc.) using the specified radiative transfer model.

    Parameters
    ----------
    model : Model, default "SPARTA"
        Name of the clear-sky model to use. Options: "SPARTA", "Bird"
    include_atmosphere : bool, default False
        If True, include atmospheric constituents in the output dataset.
        If False, only radiation components are returned.
    model_kwargs : dict, optional
        Additional keyword arguments to pass to the model function

    Returns
    -------
    xr.Dataset
        CF-compliant dataset containing computed irradiance components:
        - ghi: Global Horizontal Irradiance (W/m²)
        - dni: Direct Normal Irradiance (W/m²)
        - dhi or dif: Diffuse Horizontal Irradiance (W/m²)
        - csi: Circumsolar Irradiance (W/m², SPARTA only)

    Examples
    --------
    >>> import pandas as pd
    >>> from spartasolar.atmoslib import MERRA2DailyAtmosphere
    >>>
    >>> times = pd.date_range("2020-06-15", periods=24, freq="h")
    >>> atm = MERRA2DailyAtmosphere.at_sites(
    ...     times=times,
    ...     latitude=36.72,
    ...     longitude=-4.42
    ... )
    >>> result = atm.compute(model="SPARTA")
    >>> print(result.ghi.values)

    Use different model with custom parameters:

    >>> result = atm.compute(
    ...     model="Bird",
    ...     model_kwargs={"scheme": "transmittance_parameterization"}
    ... )

    Notes
    -----
    The method automatically:
    - Calculates solar position (zenith angle, Earth-Sun distance)
    - Converts atmospheric units to model requirements
    - Handles both gridded and site-based data structures

    See Also
    --------
    spartasolar.modlib.sparta : SPARTA model implementation
    spartasolar.modlib.bird : Bird clear-sky model
    """

    model = validate_type(model, Model)
    model_func = getattr(modlib, model)
    model_vars = inspect.getfullargspec(model_func).args

    is_regular_grid = "site" not in self.dataset.dims

    # compute solar geometry...
    sw_eval = sunwhere.regular_grid if is_regular_grid else sunwhere.sites
    solpos = sw_eval(
        # self.dataset.time.values,  # WARNING: this returns a utc tz-naive numpy array of dtype datetime64[ns] !!!
        self.dataset.indexes["time"],  # NOTE: with `indexes` we get a pandas DatetimeIndex with tz info preserved!
        latitude=self.dataset.lat.values,
        longitude=self.dataset.lon.values,
        algorithm=config.get_option("sunwhere.algorithm", default="psa"),
        refraction=config.get_option("sunwhere.refraction", default=True),
        engine=config.get_option("sunwhere.engine", default="numexpr")
    )

    new_coord_names = {"location": "site", "latitude": "lat", "longitude": "lon"}
    cosz = solpos.cosz.rename({old: new for old, new in new_coord_names.items() if old in solpos.cosz.dims})

    if is_regular_grid:
        n_lats = self.dataset.sizes["lat"]
        n_lons = self.dataset.sizes["lon"]
        ecf = solpos.ecf.expand_dims(dim={"lat": n_lats, "lon": n_lons}, axis=(1, 2))
    else:
        n_locs = self.dataset.sizes["site"]
        ecf = solpos.ecf.expand_dims(dim={"site": n_locs}, axis=1)

    kwargs = {"cosz": cosz, "ecf": ecf}

    # atmosphere...
    def get_with_proper_units(var):
        if var not in self.dataset.data_vars:
            raise ValueError(f"variable `{var}` required by the model `{model}` is not available in this atmosphere")
        if var == "pressure":
            return self.dataset[var] * 1e-2  # Pa to hPa
        if var == "pwater":
            return pwater_in_kg_m2_to_cm(self.dataset[var])
        if var == "ozone":
            return ozone_in_kg_m2_to_cm(self.dataset[var])
        return self.dataset[var]
    variables = set(model_vars).intersection(self.dataset.data_vars)  # variables required by this model
    kwargs = kwargs | {var: get_with_proper_units(var) for var in variables}

    # and call the clearsky model...
    result = model_func(**(kwargs | (model_kwargs or {})))

    # encapsulate the result in a CF-compliant xarray Dataset
    if is_regular_grid:
        return build_atmosphere_on_regular_grid(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            global_attrs=get_global_attrs(feature_type="grid")
        )
    else:
        site_values = self.dataset.coords.get("site").values
        n_sites = self.dataset.sizes["site"]
        site_names = validate_site_names(site_values, n_sites)
        return build_atmosphere_of_sites(
            times=self.dataset.indexes["time"],
            latitude=self.dataset.lat.values,
            longitude=self.dataset.lon.values,
            constituents=result,
            site_names=site_names,
            global_attrs=get_global_attrs(feature_type="timeSeries")
        )

Clear-sky models

SPARTA

SPARTA

SPARTA(
    cosz: float | ndarray = 0.5,
    pressure: float | ndarray = 1013.25,
    albedo: float | ndarray = 0.2,
    pwater: float | ndarray = 1.4,
    ozone: float | ndarray = 0.3,
    beta: float | ndarray = 0.1,
    alpha: float | ndarray = 1.3,
    ssa: float | ndarray = 0.92,
    asy: float | ndarray = 0.65,
    ecf: float | ndarray = 1.0,
    csi_param: str = "sparta",
    csi_hfov: float = 2.5,
    transmittance_scheme: str = "interdependent",
) -> dict[str, ndarray]

Compute clear-sky solar irradiance using the SPARTA model.

SPARTA (Solar PArameterization of the Radiative Transfer of the Atmosphere) is a high-accuracy 2-band broadband clear-sky model that computes direct, diffuse, global, and circumsolar irradiance. The model is fully vectorized and handles nighttime masking automatically.

Parameters:

  • cosz

    (float or ndarray, default: 0.5 ) –

    Cosine of the solar zenith angle [0, 1]. Values ≤ cos(90.5°) ≈ 0.00872 are treated as nighttime.

  • pressure

    (float or ndarray, default: 1013.25 ) –

    Atmospheric surface pressure in hPa (or mb). Typical range: 800-1100 hPa.

  • albedo

    (float or ndarray, default: 0.2 ) –

    Ground surface albedo [0, 1]. Typical values: 0.1-0.3 (vegetation), 0.6-0.9 (snow), 0.05-0.15 (water).

  • pwater

    (float or ndarray, default: 1.4 ) –

    Precipitable water vapor in cm. Typical range: 0.5-6.0 cm.

  • ozone

    (float or ndarray, default: 0.3 ) –

    Total column ozone in atm-cm. Note: 1 atm-cm = 1000 DU (Dobson Units). Typical range: 0.2-0.5 atm-cm (200-500 DU).

  • beta

    (float or ndarray, default: 0.1 ) –

    Ångström turbidity coefficient (aerosol optical depth at 1000 nm). Automatically clipped to [0, 2.2]. Typical range: 0.02-0.5.

  • alpha

    (float or ndarray, default: 1.3 ) –

    Ångström wavelength exponent. Automatically clipped to [0, 2.5]. Typical values: 0.5-1.0 (coarse aerosols), 1.5-2.0 (fine aerosols).

  • ssa

    (float or ndarray, default: 0.92 ) –

    Aerosol single-scattering albedo at ~700 nm [0, 1]. Typical values: 0.85-0.95 (urban), 0.95-0.99 (desert dust).

  • asy

    (float or ndarray, default: 0.65 ) –

    Aerosol asymmetry parameter at ~700 nm [-1, 1]. Typical range: 0.6-0.8.

  • ecf

    (float or ndarray, default: 1.0 ) –

    Eccentricity correction factor for Sun-Earth distance. Range: ~0.967 (early July) to ~1.034 (early January).

  • csi_param

    (str, default: 'sparta' ) –

    Circumsolar irradiance parameterization method: - 'none': Neglects circumsolar component - 'sparta': Uses SPARTA native parameterization (recommended)

  • csi_hfov

    (float, default: 2.5 ) –

    Half field of view angle in degrees for circumsolar evaluation. Typical pyrheliometer values: 2.5° (standard), 2.9° (some instruments).

  • transmittance_scheme

    (str, default: 'interdependent' ) –

    Method for computing atmospheric transmittances: - 'independent': Treats each constituent separately (faster, less accurate) - 'interdependent': Accounts for constituent interactions (more accurate)

Returns:

  • dict[str, float or ndarray]

    Dictionary with the following irradiance components (all in W/m²):

    • 'dni': Direct Normal Irradiance (beam on plane perpendicular to sun)
    • 'dhi': Direct Horizontal Irradiance (beam on horizontal plane)
    • 'dif': Diffuse Horizontal Irradiance (scattered radiation)
    • 'ghi': Global Horizontal Irradiance (total on horizontal plane)
    • 'csi': Circumsolar Normal Irradiance (forward-scattered aureole)

Examples:

Single location and time:

>>> from spartasolar.modlib import SPARTA
>>> result = SPARTA(
...     cosz=0.866,  # 30° solar zenith angle
...     pressure=1013.25,
...     pwater=2.0,
...     ozone=0.3,
...     beta=0.1,
...     alpha=1.3
... )
>>> print(f"DNI: {result['dni']:.1f} W/m²")
>>> print(f"GHI: {result['ghi']:.1f} W/m²")

Vectorized computation for time series:

>>> import numpy as np
>>> # Simulate conditions at different solar elevations
>>> zenith_angles = np.linspace(0, 85, 20)  # degrees
>>> cosz_values = np.cos(np.radians(zenith_angles))
>>> 
>>> result = SPARTA(
...     cosz=cosz_values,
...     pressure=1013.25,
...     pwater=np.linspace(1.0, 3.0, 20),  # varying water vapor
...     ozone=0.3,
...     beta=0.08,
...     alpha=1.4
... )
>>> print(result['ghi'].shape)  # (20,)

High aerosol loading scenario:

>>> result = SPARTA(
...     cosz=0.7,
...     beta=0.5,  # high turbidity
...     alpha=0.8,  # coarse particles
...     ssa=0.88,  # slightly absorbing
...     csi_param='sparta'
... )
>>> csi_fraction = result['csi'] / result['dni']
>>> print(f"CSI fraction: {csi_fraction:.2%}")

Disable circumsolar correction:

>>> result_no_csi = SPARTA(cosz=0.8, csi_param='none')
>>> print(result_no_csi['csi'])  # All zeros
Notes
  • Solar constant used: 1361.1 W/m²
  • Nighttime threshold: cosz ≤ cos(90.5°) ≈ 0.00872
  • All input arrays are automatically broadcast to compatible shapes
  • Invalid/missing values (-999, NaN) are handled gracefully
  • The 'interdependent' scheme is recommended for highest accuracy

The model has been validated against radiative transfer simulations and ground measurements, showing typical errors < 2% for GHI and DNI under most atmospheric conditions.

See Also

spartasolar.modlib.bird : Alternative Bird clear-sky model

References

.. [1] Ruiz-Arias, J. A. (2023). SPARTA: Solar parameterization for the radiative transfer of the cloudless atmosphere. Renewable and Sustainable Energy Reviews*, 188, 113833. https://doi.org/10.1016/j.rser.2023.113833

Source code in src/spartasolar/modlib/sparta.py
def SPARTA(
    cosz: float | np.ndarray = 0.5,
    pressure: float | np.ndarray = 1013.25,
    albedo: float | np.ndarray = 0.2,
    pwater: float | np.ndarray = 1.4,
    ozone: float | np.ndarray = 0.3,
    beta: float | np.ndarray = 0.1,
    alpha: float | np.ndarray = 1.3,
    ssa: float | np.ndarray = 0.92,
    asy: float | np.ndarray = 0.65,
    ecf: float | np.ndarray = 1.0,
    csi_param: str = 'sparta',
    csi_hfov: float = 2.5,
    transmittance_scheme: str = 'interdependent'
) -> dict[str, np.ndarray]:
    """Compute clear-sky solar irradiance using the SPARTA model.

    SPARTA (Solar PArameterization of the Radiative Transfer of the
    Atmosphere) is a high-accuracy 2-band broadband clear-sky model
    that computes direct, diffuse, global, and circumsolar irradiance.
    The model is fully vectorized and handles nighttime masking automatically.

    Parameters
    ----------
    cosz : float or np.ndarray, default 0.5
        Cosine of the solar zenith angle [0, 1]. Values ≤ cos(90.5°) ≈ 0.00872
        are treated as nighttime.
    pressure : float or np.ndarray, default 1013.25
        Atmospheric surface pressure in hPa (or mb). Typical range: 800-1100 hPa.
    albedo : float or np.ndarray, default 0.2
        Ground surface albedo [0, 1]. Typical values: 0.1-0.3 (vegetation),
        0.6-0.9 (snow), 0.05-0.15 (water).
    pwater : float or np.ndarray, default 1.4
        Precipitable water vapor in cm. Typical range: 0.5-6.0 cm.
    ozone : float or np.ndarray, default 0.3
        Total column ozone in atm-cm. Note: 1 atm-cm = 1000 DU (Dobson Units).
        Typical range: 0.2-0.5 atm-cm (200-500 DU).
    beta : float or np.ndarray, default 0.1
        Ångström turbidity coefficient (aerosol optical depth at 1000 nm).
        Automatically clipped to [0, 2.2]. Typical range: 0.02-0.5.
    alpha : float or np.ndarray, default 1.3
        Ångström wavelength exponent. Automatically clipped to [0, 2.5].
        Typical values: 0.5-1.0 (coarse aerosols), 1.5-2.0 (fine aerosols).
    ssa : float or np.ndarray, default 0.92
        Aerosol single-scattering albedo at ~700 nm [0, 1].
        Typical values: 0.85-0.95 (urban), 0.95-0.99 (desert dust).
    asy : float or np.ndarray, default 0.65
        Aerosol asymmetry parameter at ~700 nm [-1, 1].
        Typical range: 0.6-0.8.
    ecf : float or np.ndarray, default 1.0
        Eccentricity correction factor for Sun-Earth distance.
        Range: ~0.967 (early July) to ~1.034 (early January).
    csi_param : str, default 'sparta'
        Circumsolar irradiance parameterization method:
        - 'none': Neglects circumsolar component
        - 'sparta': Uses SPARTA native parameterization (recommended)
    csi_hfov : float, default 2.5
        Half field of view angle in degrees for circumsolar evaluation.
        Typical pyrheliometer values: 2.5° (standard), 2.9° (some instruments).
    transmittance_scheme : str, default 'interdependent'
        Method for computing atmospheric transmittances:
        - 'independent': Treats each constituent separately (faster, less accurate)
        - 'interdependent': Accounts for constituent interactions (more accurate)

    Returns
    -------
    dict[str, float or np.ndarray]
        Dictionary with the following irradiance components (all in W/m²):

        - 'dni': Direct Normal Irradiance (beam on plane perpendicular to sun)
        - 'dhi': Direct Horizontal Irradiance (beam on horizontal plane)
        - 'dif': Diffuse Horizontal Irradiance (scattered radiation)
        - 'ghi': Global Horizontal Irradiance (total on horizontal plane)
        - 'csi': Circumsolar Normal Irradiance (forward-scattered aureole)

    Examples
    --------
    Single location and time:

    >>> from spartasolar.modlib import SPARTA
    >>> result = SPARTA(
    ...     cosz=0.866,  # 30° solar zenith angle
    ...     pressure=1013.25,
    ...     pwater=2.0,
    ...     ozone=0.3,
    ...     beta=0.1,
    ...     alpha=1.3
    ... )
    >>> print(f"DNI: {result['dni']:.1f} W/m²")
    >>> print(f"GHI: {result['ghi']:.1f} W/m²")

    Vectorized computation for time series:

    >>> import numpy as np
    >>> # Simulate conditions at different solar elevations
    >>> zenith_angles = np.linspace(0, 85, 20)  # degrees
    >>> cosz_values = np.cos(np.radians(zenith_angles))
    >>> 
    >>> result = SPARTA(
    ...     cosz=cosz_values,
    ...     pressure=1013.25,
    ...     pwater=np.linspace(1.0, 3.0, 20),  # varying water vapor
    ...     ozone=0.3,
    ...     beta=0.08,
    ...     alpha=1.4
    ... )
    >>> print(result['ghi'].shape)  # (20,)

    High aerosol loading scenario:

    >>> result = SPARTA(
    ...     cosz=0.7,
    ...     beta=0.5,  # high turbidity
    ...     alpha=0.8,  # coarse particles
    ...     ssa=0.88,  # slightly absorbing
    ...     csi_param='sparta'
    ... )
    >>> csi_fraction = result['csi'] / result['dni']
    >>> print(f"CSI fraction: {csi_fraction:.2%}")

    Disable circumsolar correction:

    >>> result_no_csi = SPARTA(cosz=0.8, csi_param='none')
    >>> print(result_no_csi['csi'])  # All zeros

    Notes
    -----
    - Solar constant used: 1361.1 W/m²
    - Nighttime threshold: cosz ≤ cos(90.5°) ≈ 0.00872
    - All input arrays are automatically broadcast to compatible shapes
    - Invalid/missing values (-999, NaN) are handled gracefully
    - The 'interdependent' scheme is recommended for highest accuracy

    The model has been validated against radiative transfer simulations
    and ground measurements, showing typical errors < 2% for GHI and DNI
    under most atmospheric conditions.

    See Also
    --------
    spartasolar.modlib.bird : Alternative Bird clear-sky model

    References
    ----------
    .. [1] Ruiz-Arias, J. A. (2023). SPARTA: Solar parameterization for
           the radiative transfer of the cloudless atmosphere. Renewable
           and Sustainable Energy Reviews*, 188, 113833.
           https://doi.org/10.1016/j.rser.2023.113833
    """

    cosz, pressure, albedo, pwater, ozone, beta, alpha, ssa, asy, ecf, restore_shape = \
        cast_arrays(cosz, pressure, albedo, pwater, ozone, beta, alpha, ssa, asy, ecf)

    hfov = csi_hfov
    if np.isscalar(hfov):
        hfov = np.full(cosz.shape, hfov)

    INP_SHAPE = cosz.shape
    COSZ_MIN = np.cos(np.radians(90.5))
    SC = 1361.1  # W/m2, solar constant
    BF = (0.46472, 0.52113)  # band fractions

    nighttime = cosz <= COSZ_MIN

    def notna(ar):
        return (~np.isnan(ar)) & (ar != -999) & (~nighttime)

    domain = (
        notna(cosz) & notna(ecf) & notna(pressure) & notna(ozone) & notna(pwater) &
        notna(albedo) & notna(beta) & notna(alpha) & notna(ssa) & notna(asy)
    )

    # .. initialize outputs
    Ebn = np.full(INP_SHAPE, np.nan)  # direct normal irradiance, W/m2
    Ebh = np.full(INP_SHAPE, np.nan)  # direct horizontal irradiance, W/m2
    Edh = np.full(INP_SHAPE, np.nan)  # diffuse horizontal irradiance, W/m2
    Egh = np.full(INP_SHAPE, np.nan)  # global horizontal irradiance, W/m2
    Ecn = np.full(INP_SHAPE, np.nan)  # circumsolar normal irradiance, W/m2

    # .. airmasses
    amo = airmass(cosz[domain], "ozone")
    amr = airmass(cosz[domain], "rayleigh")
    amw = airmass(cosz[domain], "water")
    ama = airmass(cosz[domain], "aerosol")
    amp = np.full(ama.shape, 1.66)  # air mass for sky reflectance

    # DIRECT IRRADIANCE...

    To1, To2 = ozone_transmittance(amo, ozone[domain], transmittance_scheme)
    TR1, TR2 = rayleigh_transmittance(amr, pressure[domain], transmittance_scheme)
    Tg1, Tg2 = umgas_transmittance(amr, pressure[domain], transmittance_scheme)
    Tw1, Tw2 = water_transmittance(amw, pwater[domain], transmittance_scheme)
    Ta1, Ta2 = aerosol_transmittance(ama, beta[domain], alpha[domain], transmittance_scheme)

    # .. aerosol absorption band transmittances
    Taa1 = Ta1**(1.-ssa[domain])
    Taa2 = Ta2**(1.-ssa[domain])

    # .. aerosol scattering band transmittances
    Tas1 = Ta1**ssa[domain]
    Tas2 = Ta2**ssa[domain]

    # .. absorption band transmittances
    Tabs1 = To1*Tg1*Tw1*Taa1
    Tabs2 = To2*Tg2*Tw2*Taa2

    # .. scattering band transmittances
    Tscat1 = TR1*Tas1
    Tscat2 = TR2*Tas2

    # .. extinction band transmittances
    T1 = Tabs1*Tscat1
    T2 = Tabs2*Tscat2

    Tb = BF[0]*T1 + BF[1]*T2  # extinction broadband transmittance

    Ebn[domain] = np.clip(SC*ecf[domain]*Tb, 0., np.inf)
    Ebh = Ebn*cosz

    # DIFFUSE IRRADIANCE...

    FR = rayleigh_forward_scattering(amr, pressure[domain], transmittance_scheme)
    Fa = aerosol_forward_scattering(ama)
    rsky1, rsky2 = sky_reflectance(amp, ozone[domain], pressure[domain], pwater[domain],
                                   beta[domain], alpha[domain], ssa[domain], transmittance_scheme)

    # .. rayleigh scattering
    ray_scat1 = FR*Tabs1*(1.-TR1)*cosz[domain]
    ray_scat2 = FR*Tabs2*(1.-TR2)*cosz[domain]

    # .. aerosol scattering
    aer_scat1 = Fa*Tabs1*TR1*(1.-Tas1)*cosz[domain]
    aer_scat2 = Fa*Tabs2*TR2*(1.-Tas2)*cosz[domain]

    # .. ground-sky multiple scattering
    sky_scat1 = rsky1*albedo[domain]*(T1*cosz[domain] + ray_scat1 + aer_scat1) / (1.-rsky1*albedo[domain])
    sky_scat2 = rsky2*albedo[domain]*(T2*cosz[domain] + ray_scat2 + aer_scat2) / (1.-rsky2*albedo[domain])

    scat1 = ray_scat1 + aer_scat1 + sky_scat1  # scattering "transmittance" in band 1
    scat2 = ray_scat2 + aer_scat2 + sky_scat2  # scattering "transmittance" in band 2
    scatb = BF[0]*scat1 + BF[1]*scat2  # broadband scattering "transmittance"

    Edh[domain] = np.clip(SC*ecf[domain]*scatb, 0., np.inf)

    # GLOBAL IRRADIANCE...

    Egh = Ebh + Edh

    # CIRCUMSOLAR IRRADIANCE...

    csr = np.full(Ebn.shape, 0.)
    if csi_param == "sparta":
        Tab = BF[0]*Ta1 + BF[1]*Ta2
        Tab[(Ta1 >= 0.9999) & (Ta2 >= 0.9999)] = 1.
        csr[domain] = aerosol_circumsolar_ratio(alpha[domain], asy[domain], Tab, hfov[domain])
    Ecn = (csr / (1. - csr)) * Ebn

    # .. circumsolar correction
    Ebn = Ebn / (1. - csr)
    Ebh = Ebn*cosz
    Edh = Egh - Ebh

    # .. mask nighttime
    Ebn[nighttime] = 0.
    Ebh[nighttime] = 0.
    Edh[nighttime] = 0.
    Egh[nighttime] = 0.
    Ecn[nighttime] = 0.

    Ebn = restore_shape(Ebn)
    Ebh = restore_shape(Ebh)
    Edh = restore_shape(Edh)
    Egh = restore_shape(Egh)
    Ecn = restore_shape(Ecn)

    return {"dni": Ebn, "dhi": Ebh, "dif": Edh, "ghi": Egh, "csi": Ecn}

Bird & Hulstrom

BIRD

BIRD(
    cosz: float | ndarray = 0.5,
    pressure: float | ndarray = 1013.25,
    albedo: float | ndarray = 0.2,
    pwater: float | ndarray = 1.4,
    ozone: float | ndarray = 0.3,
    beta: float | ndarray = 0.1,
    alpha: float | ndarray = 1.3,
    ssa: float | ndarray = 0.92,
    asy: float | ndarray = 0.65,
    ecf: float | ndarray = 1.0,
) -> dict[str, ndarray]

Calculate solar irradiance using the Bird & Hulstrom model.

The model estimates solar radiation through individual transmittance processes for Rayleigh scattering, ozone absorption, uniformly mixed gases, water vapor, and aerosol extinction. It also accounts for multiple reflections between the ground and the sky.

Parameters:

  • cosz

    (float or ndarray, default: 0.5 ) –

    Cosine of the solar zenith angle.

  • pressure

    (float or ndarray, default: 1013.25 ) –

    Atmospheric surface pressure in hPa.

  • albedo

    (float or ndarray, default: 0.2 ) –

    Ground surface albedo in the range [0, 1].

  • pwater

    (float or ndarray, default: 1.4 ) –

    Precipitable water in cm.

  • ozone

    (float or ndarray, default: 0.3 ) –

    Ozone vertical path length in atm-cm (1 atm-cm = 1000 DU).

  • beta

    (float or ndarray, default: 0.1 ) –

    Angstrom turbidity coefficient (AOD at 1000 nm).

  • alpha

    (float or ndarray, default: 1.3 ) –

    Angstrom wavelength exponent.

  • ssa

    (float or ndarray, default: 0.92 ) –

    Aerosol single-scattering albedo at approximately 700 nm.

  • asy

    (float or ndarray, default: 0.65 ) –

    Aerosol asymmetry parameter.

  • ecf

    (float or ndarray, default: 1.0 ) –

    Eccentricity correction factor for the Sun-Earth orbit.

Returns:

  • dict[str, ndarray]

    Dictionary containing irradiance components in W/m²: dni, dhi, dif, and ghi.

Notes
- The model uses a fixed solar constant of 1353 W/m².
- Nighttime values are automatically masked (set to 0) for zenith 
  angles greater than 90.5°.
- The algorithm includes a 0.9662 correction factor for the 
  direct normal component as per the original publication.
References
- Bird, R. E., & Hulstrom, R. L. (1981). A simplified clear sky 
  model for direct and diffuse insolation on horizontal surfaces. 
  Solar Energy Research Institute (SERI).
Source code in src/spartasolar/modlib/bird.py
def BIRD(
    cosz: float | np.ndarray = 0.5,
    pressure: float | np.ndarray = 1013.25,
    albedo: float | np.ndarray = 0.2,
    pwater: float | np.ndarray = 1.4,
    ozone: float | np.ndarray = 0.3,
    beta: float | np.ndarray = 0.1,
    alpha: float | np.ndarray = 1.3,
    ssa: float | np.ndarray = 0.92,
    asy: float | np.ndarray = 0.65,
    ecf: float | np.ndarray = 1.0
) -> dict[str, np.ndarray]:
    """Calculate solar irradiance using the Bird & Hulstrom model.

    The model estimates solar radiation through individual transmittance 
    processes for Rayleigh scattering, ozone absorption, uniformly mixed gases, 
    water vapor, and aerosol extinction. It also accounts for multiple 
    reflections between the ground and the sky.

    Parameters
    ----------
    cosz : float or np.ndarray, default 0.5
        Cosine of the solar zenith angle.
    pressure : float or np.ndarray, default 1013.25
        Atmospheric surface pressure in hPa.
    albedo : float or np.ndarray, default 0.2
        Ground surface albedo in the range [0, 1].
    pwater : float or np.ndarray, default 1.4
        Precipitable water in cm.
    ozone : float or np.ndarray, default 0.3
        Ozone vertical path length in atm-cm (1 atm-cm = 1000 DU).
    beta : float or np.ndarray, default 0.1
        Angstrom turbidity coefficient (AOD at 1000 nm).
    alpha : float or np.ndarray, default 1.3
        Angstrom wavelength exponent.
    ssa : float or np.ndarray, default 0.92
        Aerosol single-scattering albedo at approximately 700 nm.
    asy : float or np.ndarray, default 0.65
        Aerosol asymmetry parameter.
    ecf : float or np.ndarray, default 1.0
        Eccentricity correction factor for the Sun-Earth orbit.

    Returns
    -------
    dict[str, np.ndarray]
        Dictionary containing irradiance components in W/m²:
        ``dni``, ``dhi``, ``dif``, and ``ghi``.

    Notes
    -----
        - The model uses a fixed solar constant of 1353 W/m².
        - Nighttime values are automatically masked (set to 0) for zenith 
          angles greater than 90.5°.
        - The algorithm includes a 0.9662 correction factor for the 
          direct normal component as per the original publication.

    References
    ----------
        - Bird, R. E., & Hulstrom, R. L. (1981). A simplified clear sky 
          model for direct and diffuse insolation on horizontal surfaces. 
          Solar Energy Research Institute (SERI).
    """

    cosz, pressure, albedo, pwater, ozone, beta, alpha, ssa, asy, ecf, restore_shape = \
        cast_arrays(cosz, pressure, albedo, pwater, ozone, beta, alpha, ssa, asy, ecf)

    INP_SHAPE = cosz.shape
    COSZ_MIN = np.cos(np.radians(90.5))
    SC = 1353.  # W/m2, solar constant

    nighttime = cosz <= COSZ_MIN

    def notna(ar):
        return (~np.isnan(ar)) & (ar != -999) & (~nighttime)

    domain = (
        notna(cosz) & notna(ecf) & notna(pressure) &
        notna(ozone) & notna(pwater) & notna(albedo) &
        notna(beta) & notna(alpha) & notna(ssa) & notna(asy)
    )

    # .. initialize outputs
    Ebn = np.full(INP_SHAPE, np.nan)  # direct normal irradiance, W/m2
    Ebh = np.full(INP_SHAPE, np.nan)  # direct horizontal irradiance, W/m2
    Edh = np.full(INP_SHAPE, np.nan)  # diffuse horizontal irradiance, W/m2
    Egh = np.full(INP_SHAPE, np.nan)  # global horizontal irradiance, W/m2

    # .. airmass
    c = [0.48353, 0.095846,  96.741, 1.7540]
    sza = np.degrees(np.arccos(cosz))[domain]
    am = np.maximum(1., 1. / (cosz[domain] + c[0]*(sza**c[1])/((c[2]-sza)**c[3])))
    amr = am * (pressure[domain] / 1013.25)

    # DIRECT IRRADIANCE...

    TR = np.clip(np.exp(-0.0903*(amr**.84)*(1.+amr-amr**1.01)), 0., 1.)
    uo = am*ozone[domain]
    To = np.clip(
        1 - (0.1611*uo/((1.+139.48*uo)**0.3035) -
             0.002715*uo/((1.+(0.044*uo))+0.0003*uo**2)), 0., 1.)
    Tg = np.clip(np.exp(-0.0127*amr**0.26), 0., 1.)
    uw = am*pwater[domain]
    Tw = np.clip(1 - 2.4959*uw / (((1 + 79.034*uw)**0.6828) + 6.385*uw), 0., 1.)
    taua = beta[domain]*(0.2758*(0.38**(-alpha[domain])) + 0.35*(0.5**(-alpha[domain])))
    Ta = np.clip(np.exp(-(taua**0.873)*(1+taua-taua**0.7088)*am**0.9108), 0., 1.)

    Ebn[domain] = np.clip(0.9662*SC*ecf[domain]*TR*To*Tg*Tw*Ta, 0., np.inf)
    Ebh = Ebn*cosz

    # DIFFUSE IRRADIANCE...

    Taa = np.clip(1-(1-ssa[domain])*(1-am+am**1.06)*(1-Ta), 0., 1.)
    Tas = Ta/Taa
    Tabs = To*Tg*Taa*Tw
    Ba = 0.5*(1+asy[domain])
    rhos = 0.0685 + (1-Ba)*(1-Tas)
    Ed0h = SC*cosz[domain]*np.clip(0.79*Tabs*(0.5*(1-TR) + Ba*(1-Tas))/(1.-am+am**1.02), 0., 1.)

    Egh[domain] = (Ebh[domain] + Ed0h)/(1-albedo[domain]*rhos)
    Edh[domain] = Egh[domain] - Ebh[domain]

    # .. mask nighttime
    Ebn[nighttime] = 0.
    Ebh[nighttime] = 0.
    Edh[nighttime] = 0.
    Egh[nighttime] = 0.

    Ebn = restore_shape(Ebn)
    Ebh = restore_shape(Ebh)
    Edh = restore_shape(Edh)
    Egh = restore_shape(Egh)

    return {"dni": Ebn, "dhi": Ebh, "dif": Edh, "ghi": Egh}

Configuration

config

Configuration management for SPARTA-Solar.

This module handles the persistent storage and retrieval of user settings using a TOML configuration file located in the standard user configuration directory.

The configuration system manages: - API credentials (e.g., SODA's user email) - Local storage paths for cached data - Algorithm preferences (e.g., solar position algorithm) - Service-specific settings

Configuration Flow: 1. On first import, checks if config file exists 2. If not found, creates it with default template 3. Loads configuration into memory (_GLOBAL_CONFIG) 4. Changes via set_option() are session-only 5. Persistent changes require manual editing of config.toml

Configuration File Location: - Linux: ~/.config/spartasolar/config.toml - macOS: ~/Library/Application Support/spartasolar/config.toml - Windows: C:\Users\\AppData\Local\spartasolar\config.toml

Examples:

>>> from spartasolar.config import get_config_path, get_option, set_option

>>> # Get configuration file path
>>> config_path = get_config_path()
>>> print(config_path)
PosixPath('/home/user/.config/spartasolar/config.toml')

>>> # Retrieve an option
>>> email = get_option('crs_soda.user_email')

>>> # Set option for current session only
>>> set_option('sunwhere.algorithm', 'spa')

>>> # Get data directory (returns Path object)
>>> data_dir = get_option('merra2_daily.data_dir')
See Also
- get_config_path(): Get path to configuration file
- get_option(): Retrieve configuration value
- set_option(): Modify configuration (session only)
- show_config(): Display all current settings

Functions:

  • get_config_path

    Get the path to the user's configuration file.

  • get_option

    Retrieve the value of a specific configuration option.

  • set_option

    Temporarily update a global option for the current session.

  • show_config

    Print all current global options to the console.

get_config_path

get_config_path() -> Path

Get the path to the user's configuration file.

Returns:

  • Path

    Absolute path to config.toml within the standard system-specific user configuration directory.

Source code in src/spartasolar/config.py
def get_config_path() -> Path:
    """Get the path to the user's configuration file.

    Returns
    -------
    Path
        Absolute path to ``config.toml`` within the standard
        system-specific user configuration directory.
    """
    path = platformdirs.user_config_path(appname="sparta-solar", ensure_exists=True)
    return path / "config.toml"

get_option

get_option(name: str, default: Any = None) -> Any

Retrieve the value of a specific configuration option.

Options are organized in tables (sections) within the TOML file. This function uses dot notation to access nested values.

Parameters:

  • name
    (str) –

    Name of the option to retrieve using the format <table-name>.<option-name> (for example, "crs_soda.user_email").

  • default
    (Any, default: None ) –

    Value to return if the option is missing.

Returns:

  • Any

    Option value. Returns default if the option is missing. Options named "data_dir" are automatically converted to Path objects.

Examples:

>>> from spartasolar.config import get_option

>>> # Get solar position algorithm
>>> algorithm = get_option('sunwhere.algorithm')
>>> print(algorithm)
'psa'

>>> # Get with default value
>>> email = get_option('crs_soda.user_email', default='user@example.com')

>>> # Data directories return Path objects
>>> from pathlib import Path
>>> data_dir = get_option('merra2_daily.data_dir')
>>> isinstance(data_dir, (Path, type(None)))
True
Source code in src/spartasolar/config.py
def get_option(name: str, default: Any = None) -> Any:
    """Retrieve the value of a specific configuration option.

    Options are organized in tables (sections) within the TOML file.
    This function uses dot notation to access nested values.

    Parameters
    ----------
    name : str
        Name of the option to retrieve using the format
        ``<table-name>.<option-name>`` (for example,
        ``"crs_soda.user_email"``).
    default : Any, default None
        Value to return if the option is missing.

    Returns
    -------
    Any
        Option value. Returns ``default`` if the option is missing.
        Options named ``"data_dir"`` are automatically converted to
        ``Path`` objects.

    Examples
    --------
        >>> from spartasolar.config import get_option

        >>> # Get solar position algorithm
        >>> algorithm = get_option('sunwhere.algorithm')
        >>> print(algorithm)
        'psa'

        >>> # Get with default value
        >>> email = get_option('crs_soda.user_email', default='user@example.com')

        >>> # Data directories return Path objects
        >>> from pathlib import Path
        >>> data_dir = get_option('merra2_daily.data_dir')
        >>> isinstance(data_dir, (Path, type(None)))
        True
    """
    table_name, option_name = name.split(".")
    if (table := _GLOBAL_CONFIG.get(table_name, None)) is None:
        logger.warning(f"missing table `{table_name}`")
        return default
    if (value := table.get(option_name, None)) is None:
        return default
    if option_name == "data_dir":
        return Path(value)
    return value

set_option

set_option(name: str, value: Any) -> None

Temporarily update a global option for the current session.

Modifies configuration values in memory only. Changes are lost when the Python session ends. To make persistent changes, edit the config.toml file directly.

Parameters:

  • name
    (str) –

    Name of the option to update in format <table>.<option>.

  • value
    (Any) –

    New value to assign. Path objects for "data_dir" options are automatically converted to strings.

Notes
Session-only changes are NOT saved to the config file. Restart
the Python session to revert to file values.

Examples:

>>> from spartasolar.config import set_option, get_option

>>> # Change solar position algorithm
>>> set_option('sunwhere.algorithm', 'spa')
>>> get_option('sunwhere.algorithm')
'spa'

>>> # Set data directory with Path object
>>> from pathlib import Path
>>> set_option('merra2_daily.data_dir', Path('/custom/path'))
Notes
To persist changes, manually edit the configuration file at the
path returned by ``get_config_path``.
Source code in src/spartasolar/config.py
def set_option(name: str, value: Any) -> None:
    """Temporarily update a global option for the current session.

    Modifies configuration values in memory only. Changes are lost when
    the Python session ends. To make persistent changes, edit the
    config.toml file directly.

    Parameters
    ----------
    name : str
        Name of the option to update in format ``<table>.<option>``.
    value : Any
        New value to assign. ``Path`` objects for ``"data_dir"`` options
        are automatically converted to strings.

    Notes
    -----
        Session-only changes are NOT saved to the config file. Restart
        the Python session to revert to file values.

    Examples
    --------
        >>> from spartasolar.config import set_option, get_option

        >>> # Change solar position algorithm
        >>> set_option('sunwhere.algorithm', 'spa')
        >>> get_option('sunwhere.algorithm')
        'spa'

        >>> # Set data directory with Path object
        >>> from pathlib import Path
        >>> set_option('merra2_daily.data_dir', Path('/custom/path'))

    Notes
    -----
        To persist changes, manually edit the configuration file at the
        path returned by ``get_config_path``.
    """

    table_name, option_name = name.split(".")
    if _GLOBAL_CONFIG.get(table_name, None) is None:
        logger.warning(f"missing table `{table_name}`")
        return None
    if option_name == "data_dir" and isinstance(value, Path):
        value = value.as_posix()
    _GLOBAL_CONFIG[table_name][option_name] = value

show_config

show_config() -> None

Print all current global options to the console.

Notes

This function uses pprint for formatted output of the global configuration state.

Source code in src/spartasolar/config.py
def show_config() -> None:
    """Print all current global options to the console.

    Notes
    -----
    This function uses ``pprint`` for formatted output of the global
    configuration state.
    """
    from pprint import pprint
    return pprint(_GLOBAL_CONFIG, indent=2, width=20)