Source code for pvlib.bifacial.ants2d

r"""
Functions for the ANTS 2D bifacial irradiance model.
"""

import numpy as np
import pandas as pd
from pvlib.tools import cosd, sind, tand, acosd
from pvlib.atmosphere import get_relative_airmass
from pvlib.bifacial import utils
from pvlib.irradiance import (
    aoi_projection, haydavies, perez, perez_driesse, get_extra_radiation
)
from pvlib.shading import projected_solar_zenith_angle, shaded_fraction1d
from pvlib.tracking import calc_surface_orientation


def _shaded_fraction(tracker_rotation, phi, gcr, x0=0, x1=1):
    """
    Calculate fraction of row slant height interal (x0, x1) that is shaded
    from direct irradiance by the row in front toward the sun.

    The array is assumed to be installed on horizontal terrain, with
    ``tracker_rotation`` and ``phi`` having their zero points facing
    vertically.  Inputs ``tracker_rotation``, ``phi``, and ``gcr`` must be
    adjusted to account for terrain slope, if necessary.

    Parameters
    ----------
    tracker_rotation : numeric
        Tracker rotation angle as a right-handed rotation around
        the axis defined by ``axis_tilt`` and ``axis_azimuth``.  For example,
        with ``axis_tilt=0`` and ``axis_azimuth=180``, ``tracker_theta > 0``
        results in ``surface_azimuth`` to the West while ``tracker_theta < 0``
        results in ``surface_azimuth`` to the East. [degree]
    phi : numeric
        Projected solar zenith angle. [degrees]
    gcr : numeric
        Ground coverage ratio, which is the ratio of row slant length to row
        spacing (pitch). [unitless]
    x0 : numeric, default 0.
        Position on the row's slant length, as a fraction of the slant length.
        ``x0=0`` corresponds to the bottom of the row. ``x0`` should be less
        than ``x1``. [unitless]
    x1 : numeric, default 1.
        Position on the row's slant length, as a fraction of the slant length.
        ``x1`` should be greater than ``x0``. [unitless]

    Returns
    -------
    f_x : numeric
        Fraction of row slant height from the bottom that is shaded from
        direct irradiance.

    References
    ----------
    .. [1] Kevin Anderson and Mark Mikofski, "Slope-Aware Backtracking for
       Single-Axis Trackers", Technical Report NREL/TP-5K00-76626, July 2020.
       https://www.nrel.gov/docs/fy20osti/76626.pdf
    """
    # keep track of scalar inputs so that we can have output match at the end
    squeeze = []
    if np.isscalar(x0) and np.isscalar(x1):
        squeeze.append(0)
    if np.isscalar(tracker_rotation):
        squeeze.append(1)

    # note: ground slope is already accounted for in phi and gcr, so don't
    # apply it here.
    # also, we have PSZA instead of solar position, so use fake azimuths to
    # trick shaded_fraction1d into accepting it as-is.
    # direction of positive phi by right-hand rule:
    f_s = shaded_fraction1d(phi, solar_azimuth=90,
                            axis_azimuth=0,
                            shaded_row_rotation=tracker_rotation,
                            collector_width=1, pitch=1/gcr)

    # dimensions: row segment, time
    f_s = np.atleast_1d(f_s)[np.newaxis, :]
    x0 = np.atleast_1d(x0)[:, np.newaxis]
    x1 = np.atleast_1d(x1)[:, np.newaxis]

    swap = tracker_rotation < 0
    x0, x1 = np.where(swap, 1 - x1, x0), np.where(swap, 1 - x0, x1)

    f_s = np.clip((f_s - x0) / (x1 - x0), a_min=0, a_max=1)
    f_s = f_s.squeeze(axis=tuple(squeeze))

    return f_s


def _ants2d_singleside(tracker_rotation, cos_aoi, phi, gcr, height, pitch,
                       dni, dhi, ground_irradiance, albedo, x0, x1, g0, g1,
                       max_rows):
    r"""
    Calculate plane-of-array irradiance components on one side of a row
    of modules.

    Parameters
    ----------
    tracker_rotation : numeric
        Tracker rotation angle as a right-handed rotation around
        the axis defined by ``axis_tilt`` and ``axis_azimuth``.  For example,
        with ``axis_tilt=0`` and ``axis_azimuth=180``, ``tracker_theta > 0``
        results in ``surface_azimuth`` to the West while ``tracker_theta < 0``
        results in ``surface_azimuth`` to the East. [degree]
    cos_aoi : numeric
        Cosine of the angle of incidence of beam irradiance; can be
        calculated using :py:func:`pvlib.irradiance.aoi_projection`. [unitless]
    phi : numeric
        Project solar zenith angle; calculate with
        :py:func:`pvlib.shading.projected_solar_zenith_angle`. [degree]
    gcr : float
        Ground coverage ratio, ratio of row slant length to row spacing.
        [unitless]
    height : float
        Height of the center point of the row above the ground; must be in the
        same units as ``pitch``.
    pitch : float
        Distance between two rows; must be in the same units as ``height``.
    dni : numeric
        Direct normal irradiance. [Wm⁻²]
    dhi : numeric
        Diffuse horizontal irradiance. [Wm⁻²]
    ground_irradiance : numeric
        Irradiance incident on the ground surface, partitioned according
        to ``x0`` and ``x1``. Sum of direct and diffuse components. [Wm⁻²]
    albedo : numeric
        Surface albedo. If a scalar, it is applied to all ground segments and
        timestamps.  Otherwise, must be specified as an array with shape
        (n_ground_segments, n_timestamps). [unitless]
    x0 : numeric, default 0
        Position on the row's slant length, as a fraction of the slant length.
        ``x0=0`` corresponds to the left edge of the row.
        ``x0`` should be less than ``x1``.  If specified as array, it
        must have the same length as ``x1``. [unitless]
    x1 : numeric, default 1
        Position on the row's slant length, as a fraction of the slant length.
        ``x1=1`` corresponds to the right edge of the row.
        ``x1`` should be greater than ``x0``. If specified as array, it
        must have the same length as ``x0``. [unitless]
    g0 : numeric
        Position on the ground surface, as a fraction of the row-to-row
        spacing. ``g0=0`` corresponds to ground underneath the middle of the
        left row. ``g0`` should be less than ``g1``. If specified as array, it
        must have the same length as ``g1``. [unitless]
    g1 : numeric
        Position on the ground surface, as a fraction of the row-to-row
        spacing. ``g1=1`` corresponds to ground underneath the middle of the
        right row. ``g1`` should be greater than ``g0``. If specified as
        array, it must have the same length as ``g0``. [unitless]
    max_rows : int
        Number of array units (sky wedges, ground segments, etc) to consider.
        [unitless]

    Returns
    -------
    output : dict of ``numpy.ndarray``

        ``output`` includes the following quantities:

        - ``poa_global``: total POA irradiance. [Wm⁻²]
        - ``poa_diffuse``: total diffuse POA irradiance from all sources.
          [Wm⁻²]
        - ``poa_direct``: direct POA irradiance. [Wm⁻²]
        - ``poa_sky_diffuse``: sky diffuse POA irradiance. [Wm⁻²]
        - ``poa_ground_diffuse``: ground-reflected diffuse POA irradiance.
          [Wm⁻²]
        - ``shaded_fraction``: fraction of row slant height from the bottom
          that is shaded from direct irradiance by adjacent rows. [unitless]

        Each array has shape (len(x0), len(tracker_rotation)).

    References
    ----------
    .. [1] K. S. Anderson, A. R. Jensen, and C. W. Hansen, "A Bifacial View
           Factor Model Considering Terrain Slope and Nonuniform Albedo,"
           IEEE JPV, 2026. :doi:`10.1109/JPHOTOV.2026.3677506`
    """
    # reminder of base dimensions: ground segment, row segment, time

    # in-plane beam component
    projection = np.array(np.clip(cos_aoi, a_min=0, a_max=None))
    row_shaded_fraction = _shaded_fraction(tracker_rotation, phi, gcr, x0, x1)
    poa_direct = dni * projection * (1 - row_shaded_fraction)
    poa_direct = poa_direct[0]  # drop ground segment dimension

    # in-plane sky diffuse component
    vf_row_sky = utils.vf_row_sky_2d_integ(tracker_rotation, gcr, x0, x1)
    poa_sky_diffuse = vf_row_sky * dhi
    poa_sky_diffuse = poa_sky_diffuse[0]  # drop ground segment dimension

    # in-plane ground-reflected component
    vf_row_ground = utils.vf_row_ground_2d_integ(surface_tilt=tracker_rotation,
                                                 gcr=gcr, height=height,
                                                 pitch=pitch,
                                                 x0=x0, x1=x1, g0=g0, g1=g1,
                                                 max_rows=max_rows)
    poa_ground_diffuse = vf_row_ground * albedo * ground_irradiance
    # sum over ground segments
    poa_ground_diffuse = np.sum(poa_ground_diffuse, axis=0)

    # add sky and ground-reflected irradiance on the row by irradiance
    # component
    poa_diffuse = poa_ground_diffuse + poa_sky_diffuse
    poa_global = poa_direct + poa_diffuse

    # all arrays are now 2D with shape (n_row_segments, len(tracker_rotation))
    output = {
        'poa_global': poa_global,
        'poa_direct': poa_direct,
        'poa_diffuse': poa_diffuse,
        'poa_sky_diffuse': poa_sky_diffuse,
        'poa_ground_diffuse': poa_ground_diffuse,
        'shaded_fraction': row_shaded_fraction
    }
    return output


def _apply_sky_diffuse_model(dni, dhi, model, solar_zenith, solar_azimuth,
                             dni_extra, airmass):

    # Section II.C.2

    if model in ['haydavies', 'perez', 'perez-driesse']:
        # determine circumsolar irradiance, add it to DNI

        if model == 'haydavies':
            if dni_extra is None:
                raise ValueError(f'Must supply dni_extra for {model} model')
            diffuse_model_func = haydavies
            extra_kwargs = {}

        elif model in ['perez', 'perez-driesse']:
            # note: horizon brightening is ignored
            if dni_extra is None or airmass is None:
                raise ValueError(
                    f'Must supply dni_extra and airmass for {model} model')
            if model == 'perez':
                diffuse_model_func = perez
            else:
                diffuse_model_func = perez_driesse
            extra_kwargs = {'airmass': airmass}

        kwargs = dict(
            dhi=dhi, dni=dni, dni_extra=dni_extra,
            solar_zenith=solar_zenith, solar_azimuth=solar_azimuth,
            return_components=True
        )
        # Call the model first time within the horizontal plane - to subtract
        # circumsolar_horiz from DHI
        sky_diffuse_comps_horizontal = diffuse_model_func(
            surface_tilt=0, surface_azimuth=180, **kwargs, **extra_kwargs)
        circumsolar_horiz = sky_diffuse_comps_horizontal['poa_circumsolar']

        # Call the model a second time where circumsolar_normal is facing
        # directly towards sun, and can be added to DNI
        sky_diffuse_comps_normal = diffuse_model_func(
            surface_tilt=solar_zenith, surface_azimuth=solar_azimuth,
            **kwargs, **extra_kwargs)
        circumsolar_normal = sky_diffuse_comps_normal['poa_circumsolar']

        dhi = dhi - circumsolar_horiz
        dni = dni + circumsolar_normal
    elif model != 'isotropic':
        raise ValueError(f"Invalid model: {model}")

    return dni, dhi


def _apply_ground_slope(height, pitch, gcr, tracker_rotation, ghi, dni, dhi,
                        solar_zenith, solar_azimuth, axis_tilt, axis_azimuth,
                        cross_axis_slope):
    # Section II.C.3, equations 14-19
    # transform all inputs to the rotated ANTS-2D frame, where the terrain
    # is horizontal
    slope_azimuth = axis_azimuth + np.degrees(
        np.arctan2(sind(cross_axis_slope),
                   cosd(cross_axis_slope) * sind(axis_tilt))
    )
    slope_tilt = acosd(cosd(axis_tilt) * cosd(cross_axis_slope))

    height = height * cosd(slope_tilt)
    pitch = pitch / cosd(cross_axis_slope)
    gcr = gcr * cosd(cross_axis_slope)
    tracker_rotation = tracker_rotation - cross_axis_slope
    # put back to [-180, 180]:
    tracker_rotation = ((tracker_rotation + 180) % 360) - 180

    ghi = dhi + dni * np.maximum(
        aoi_projection(slope_tilt, slope_azimuth,
                       solar_zenith, solar_azimuth),
        0)
    # dhi: no need to adjust; the blocked view is only near the
    #      the horizon, and that part of the sky is blocked by rows anyway
    # dni: no adjustment needed; the measurement plane is not affected
    return height, pitch, gcr, tracker_rotation, ghi


[docs] def get_irradiance(tracker_rotation, axis_azimuth, solar_zenith, solar_azimuth, gcr, height, pitch, ghi, dhi, dni, albedo, model='perez', dni_extra=None, airmass=None, row_segments=1, ground_segments=10, axis_tilt=0, cross_axis_slope=0, max_rows=None, return_ground_components=False): """ Get front and rear irradiance using the ANTS-2D bifacial irradiance model. The ANTS-2D model [1]_ assumes the PV system comprises parallel, evenly spaced rows on flat or uniformly sloped ground. Rows can be on fixed racking or single axis trackers. The model calculates irradiance at a location far from the ends of any rows, in effect, assuming that the rows (sheds) are infinitely long, and parallel to the (potentially sloped) ground surface. The model accounts for the following effects: - restricted view of the sky from module surfaces due to the nearby rows. - restricted view of the ground from module surfaces due to nearby rows. - restricted view of the sky from the ground due to rows. - shading of module surfaces by nearby rows. - nonuniform ground albedo. - sloped ground surface. - cell-level irradiance nonuniformity. Parameters ---------- tracker_rotation : numeric Tracker rotation angle as a right-handed rotation around the axis defined by ``axis_tilt`` and ``axis_azimuth``. For example, with ``axis_tilt=0`` and ``axis_azimuth=180``, ``tracker_theta > 0`` results in ``surface_azimuth`` to the West while ``tracker_theta < 0`` results in ``surface_azimuth`` to the East. [degree] For fixed-tilt systems, pretend the array is a tracker and input the tilt as a scalar. For example, a 20 degree south-facing array would have ``tracker_rotation=20`` and ``axis_azimuth=90``. axis_azimuth : numeric Axis azimuth angle in degrees. North = 0°; East = 90°; South = 180°; West = 270°. For fixed-tilt systems, add or subtract 90 degrees from the panel azimuth so that ``tracker_rotation`` is still a right-handed rotation. For south-facing arrays (azimuth=180°), set ``axis_azimuth=90``. For north-facing arrays (azimuth=0°), set ``axis_azimuth=270``. solar_zenith : numeric Refraction-corrected solar zenith angle. [degree] solar_azimuth : numeric Solar azimuth angle. [degree] gcr : float Ground coverage ratio, ratio of row slant length to row spacing. [unitless] height : float Height of the center point of the row above the ground; must be in the same units as ``pitch``. pitch : float Distance between two rows; must be in the same units as ``height``. ghi : numeric Global horizontal irradiance. [Wm⁻²] dhi : numeric Diffuse horizontal irradiance. [Wm⁻²] dni : numeric Direct normal irradiance. [Wm⁻²] albedo : numeric Surface albedo. If a scalar, it is applied to all ground segments and timestamps. Otherwise, must be specified as an array with shape (``n_ground_segments``, ``len(tracker_rotation)``). [unitless] model : str, default 'perez' Irradiance model - can be one of 'isotropic', 'haydavies', 'perez', or 'perez-driesse'. dni_extra : numeric, optional Extraterrestrial direct normal irradiance. Required when ``model='haydavies'``, ``model='perez'``, or ``model='perez-driesse'``. Computed from the datetime index of the inputs if not provided. [Wm⁻²] airmass : numeric, optional Relative airmass. Required when ``model='perez'`` or ``model='perez-driesse'``. Computed from ``solar_zenith`` if needed. [unitless] row_segments : int or list of pairs, default 1 If ``row_segments`` is an int, it defines the number of equal-length segments the row width is divided into. Otherwise, it must be a list of pairs ``(x0, x1)`` where ``x0`` and ``x1`` are fractions of the row width and ``x0 < x1``. Irradiance will be computed and returned for each segment. ground_segments : int or list of pairs, default 10 If ``ground_segments`` is an int, it defines the number of equal-length segments the ground surface is divided into. Otherwise, it must be a list of pairs ``(g0, g1)`` where ``g0`` and ``g1`` are fractions of the pitch and ``g0 < g1``. The pairs must be non-overlapping and must cover the entire ground surface. ``albedo`` can be specified for each segment. axis_tilt : numeric, default 0 Tilt of the axis of rotation with respect to horizontal. [degree] cross_axis_slope : numeric, default 0 The angle, relative to horizontal, of the line formed by the intersection between the plane containing the tracker axes and a plane perpendicular to the tracker axes. The cross-axis slope should be specified using a right-handed convention. For example, trackers with axis azimuth of 180 degrees (heading south) will have a negative cross-axis tilt if the tracker axes plane slopes down to the east and positive cross-axis slope if the tracker axes plane slopes down to the west. Use :func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate ``cross_axis_slope``. [degrees] max_rows : int, optional Number of array units (sky wedges, ground segments, etc) to consider. If not specified, units will be considered to within 4 degrees of the horizon. [unitless] return_ground_components : bool, default False If True, also return the direct and diffuse irradiance incident on the ground. These values are returned in a second dict. Returns ------- output : dict or DataFrame ``output`` is a DataFrame when inputs are Series and ``row_segments=1``, a dict of scalars when inputs are scalars and ``row_segments=1``, and a dict of ``np.ndarray`` otherwise. The following quantities are included: - ``poa_global``: sum of front- and back-side incident irradiance. [Wm⁻²] - ``poa_front``: total incident irradiance on the front surface. [Wm⁻²] - ``poa_back``: total incident irradiance on the back surface. [Wm⁻²] - ``poa_front_direct``: direct irradiance incident on the front surface. [Wm⁻²] - ``poa_front_diffuse``: total diffuse irradiance incident on the front surface. [Wm⁻²] - ``poa_front_sky_diffuse``: sky diffuse irradiance incident on the front surface. [Wm⁻²] - ``poa_front_ground_diffuse``: ground-reflected diffuse irradiance incident on the front surface. [Wm⁻²] - ``shaded_fraction_front``: fraction of row slant height that is shaded from direct irradiance on the front surface by adjacent rows. [unitless] - ``poa_back_direct``: direct irradiance incident on the back surface. [Wm⁻²] - ``poa_back_diffuse``: total diffuse irradiance incident on the back surface. [Wm⁻²] - ``poa_back_sky_diffuse``: sky diffuse irradiance incident on the back surface. [Wm⁻²] - ``poa_back_ground_diffuse``: ground-reflected diffuse irradiance incident on the back surface. [Wm⁻²] - ``shaded_fraction_back``: fraction of row slant height that is shaded from direct irradiance on the back surface by adjacent rows. [unitless] ground_irradiance : dict or DataFrame Only returned when ``return_ground_components=True``. ``ground_irradiance`` is a DataFrame when inputs are Series and ``n_ground_segments=1``, a dict of scalars when inputs are scalars and ``n_ground_segments=1``, and a dict of ``np.ndarray`` otherwise. The following quantities are included: - ``ground_direct``: direct irradiance incident on the ground surface. [Wm⁻²] - ``ground_diffuse``: diffuse irradiance incident on the ground surface. [Wm⁻²] Notes ----- - ``model='perez-driesse'`` was not evaluated in [1]_ and is included in this implementation as an extension. References ---------- .. [1] K. S. Anderson, A. R. Jensen, and C. W. Hansen, "A Bifacial View Factor Model Considering Terrain Slope and Nonuniform Albedo," IEEE JPV, 2026. :doi:`10.1109/JPHOTOV.2026.3677506` """ # so we can return scalars out if needed maybe_array_inputs = [ tracker_rotation, axis_azimuth, solar_zenith, solar_azimuth, gcr, height, pitch, ghi, dhi, dni, albedo, dni_extra, airmass, axis_tilt, cross_axis_slope] all_scalar_inputs = all([ np.isscalar(x) or x is None for x in maybe_array_inputs ]) try: # get the index of the first pandas input, if there is one pd_index = next( x.index for x in maybe_array_inputs if isinstance(x, pd.Series) ) except StopIteration: pd_index = None # no pandas inputs # preparation steps if airmass is None and model in ['perez', 'perez-driesse']: airmass = get_relative_airmass(solar_zenith) if (dni_extra is None and model in ['perez', 'perez-driesse', 'haydavies'] and pd_index is not None and isinstance(pd_index, pd.DatetimeIndex)): dni_extra = get_extra_radiation(pd_index) dni, dhi = _apply_sky_diffuse_model(dni, dhi, model, solar_zenith, solar_azimuth, dni_extra, airmass) true_tracker_rotation = tracker_rotation if axis_tilt != 0 or cross_axis_slope != 0: height, pitch, gcr, tracker_rotation, ghi = _apply_ground_slope( height, pitch, gcr, tracker_rotation, ghi, dni, dhi, solar_zenith, solar_azimuth, axis_tilt, axis_azimuth, cross_axis_slope ) if np.isscalar(row_segments): x_row = np.linspace(0, 1, row_segments+1) x0, x1 = x_row[:-1], x_row[1:] else: x0 = np.array([pair[0] for pair in row_segments]) x1 = np.array([pair[1] for pair in row_segments]) if np.isscalar(ground_segments): x_ground = np.linspace(0, 1, ground_segments+1) g0, g1 = x_ground[:-1], x_ground[1:] else: g0 = np.array([pair[0] for pair in ground_segments]) g1 = np.array([pair[1] for pair in ground_segments]) # dimensions: ground segment, row segment, time albedo = np.atleast_2d(albedo)[:, np.newaxis, :] ghi = np.atleast_1d(ghi)[np.newaxis, np.newaxis, :] dhi = np.atleast_1d(dhi)[np.newaxis, np.newaxis, :] dni = np.atleast_1d(dni)[np.newaxis, np.newaxis, :] tracker_rotation = np.atleast_1d(tracker_rotation) # Calculate some geometric quantities # rows to consider in front and behind current row # ensures that view factors to the sky are computed to within 4 degrees # of the horizon if max_rows is None: max_rows = np.ceil(height / (pitch * tand(4))) phi = projected_solar_zenith_angle(solar_zenith, solar_azimuth, axis_tilt, axis_azimuth) phi = phi - cross_axis_slope # compute this here, as it is expensive and does not differ between the # front and back sides vf_gnd_sky = utils.vf_ground_sky_2d_integ( tracker_rotation, gcr, height, pitch, g0=g0, g1=g1, max_rows=max_rows) vf_gnd_sky = vf_gnd_sky[:, np.newaxis, :] # irradiance components incident on ground surface ground_unshaded_fraction = utils._unshaded_ground_fraction( tracker_rotation, phi, gcr, pitch=pitch, height=height, g0=g0, g1=g1, max_rows=max_rows) ground_shaded_fraction = 1 - ground_unshaded_fraction ground_shaded_fraction = ground_shaded_fraction[:, np.newaxis, :] ground_direct = (1-ground_shaded_fraction) * (ghi - dhi) ground_diffuse = vf_gnd_sky * dhi ground_total = ground_direct + ground_diffuse # inputs shared between front and back calculations params = dict(phi=phi, gcr=gcr, height=height, pitch=pitch, dni=dni, dhi=dhi, ground_irradiance=ground_total, albedo=albedo, g0=g0, g1=g1, max_rows=max_rows) # front front_orientation = calc_surface_orientation(true_tracker_rotation, axis_tilt, axis_azimuth) cos_aoi_front = aoi_projection(**front_orientation, solar_zenith=solar_zenith, solar_azimuth=solar_azimuth) poa_front = _ants2d_singleside(tracker_rotation, cos_aoi_front, x0=x0, x1=x1, **params) # back tracker_rotation_back = true_tracker_rotation + 180 tracker_rotation_back = ((tracker_rotation_back + 180) % 360) - 180 back_orientation = calc_surface_orientation(tracker_rotation_back, axis_tilt, axis_azimuth) cos_aoi_back = aoi_projection(**back_orientation, solar_zenith=solar_zenith, solar_azimuth=solar_azimuth) tracker_rotation_back = tracker_rotation + 180 tracker_rotation_back = ((tracker_rotation_back + 180) % 360) - 180 poa_back = _ants2d_singleside(tracker_rotation_back, cos_aoi_back, x0=1-x1, x1=1-x0, **params) colmap_front = { 'poa_global': 'poa_front', 'poa_direct': 'poa_front_direct', 'poa_diffuse': 'poa_front_diffuse', 'poa_sky_diffuse': 'poa_front_sky_diffuse', 'poa_ground_diffuse': 'poa_front_ground_diffuse', 'shaded_fraction': 'shaded_fraction_front', } colmap_back = { k: v.replace("front", "back") for k, v in colmap_front.items() } for old_key, new_key in colmap_front.items(): poa_front[new_key] = poa_front.pop(old_key) for old_key, new_key in colmap_back.items(): poa_back[new_key] = poa_back.pop(old_key) out = {**poa_front, **poa_back} if row_segments == 1: for k, v in out.items(): out[k] = v[0] # drop row segment dimension if all_scalar_inputs: # drop the second dimension too, so scalars are returned for k, v in out.items(): out[k] = float(v[0]) elif pd_index is not None: out = pd.DataFrame(out, index=pd_index) if return_ground_components: squeeze = [] if ground_segments == 1: squeeze.append(0) # drop ground segment dimension squeeze.append(1) # always drop the row segment dimension if all_scalar_inputs: squeeze.append(2) # drop time dimension squeeze = tuple(squeeze) out_ground = { 'ground_direct': ground_direct.squeeze(axis=squeeze), 'ground_diffuse': ground_diffuse.squeeze(axis=squeeze), } if squeeze == (0, 1, 2): for k, v in out_ground.items(): out_ground[k] = float(v) if pd_index is not None and squeeze == (0, 1): out_ground = pd.DataFrame(out_ground, index=pd_index) return out, out_ground return out