Solar Geometry

Overview

Introduction Solar geometry is the mathematical description of where the sun appears in the sky relative to an observer and engineered surfaces. In photovoltaics (PV), this geometry controls incident irradiance, row-to-row shading risk, and tracker orientation, which in turn drive energy yield, clipping behavior, and levelized cost outcomes. At its core, solar geometry combines spherical astronomy, site orientation, terrain effects, and collector kinematics into a computational pipeline that converts sun position angles into actionable design and operations variables. A concise reference for the astronomical side is Solar zenith angle, while production-grade PV modeling practices are implemented in pvlib-python.

For business and technical users, this category matters because geometric errors propagate quickly into finance and reliability decisions. A 2–3° bias in incidence angle can shift transposition estimates; a mistaken assumption about axis tilt on sloped terrain can cause under-designed backtracking; and incorrect row-shading estimates can bias annual energy production, bankability studies, and inverter loading assumptions. The functions in this category turn these geometric dependencies into reproducible spreadsheet-native calculations that remain consistent with pvlib’s tested implementations.

The category spans five practical layers of a PV workflow. First, incidence metrics characterize the relationship between the sun vector and panel normal: AOI returns angle of incidence in degrees, while AOI_PROJECTION returns its cosine projection (including sign when the sun is behind the plane). Second, terrain-axis geometry converts site slope and tracker orientation into usable tracker-frame parameters: CALC_AXIS_TILT and CROSS_AXIS_TILT. Third, row masking functions quantify blocked sky and shading geometry in fixed-tilt row fields: MASK_ANGLE_PASSIAS, MASKING_ANGLE, and PROJ_SOLAR_ZENITH. Fourth, beam shading extent is directly computed by SHADED_FRACTION1D. Fifth, tracker kinematics are solved and translated to plane orientation using SINGLEAXIS and SURFACE_ORIENT.

Taken together, these tools support both early-stage feasibility and high-fidelity operational modeling. They allow an analyst to move from “Where is the sun relative to my array?” to “How much of my row is shaded?” and finally to “What is the actual operating tilt/azimuth and incidence at this timestamp?” in a traceable chain.

When to Use It Solar-geometry tooling is most valuable when the job is not merely to compute one angle but to make an engineering or business decision that depends on directional relationships, seasonal behavior, and array layout constraints.

One common job is tracker-layout optimization on non-flat terrain. A developer has topographic surveys, selected row azimuth, and candidate ground coverage ratios (GCRs), and must determine whether backtracking assumptions remain valid across blocks. In this case, CALC_AXIS_TILT and CROSS_AXIS_TILT are used first to convert terrain orientation into tracker-axis frame variables. Next, SINGLEAXIS models tracker rotation under backtracking assumptions. Finally, SHADED_FRACTION1D quantifies residual beam shading under specific sun angles and row spacing. The result is a decision-ready comparison of land utilization versus shading losses.

Another high-value job is fixed-tilt row spacing and diffuse-loss assessment for commercial rooftops or utility fixed-tilt fields. Teams need to test whether a tighter pitch sacrifices too much morning/evening irradiance and diffuse sky exposure. Here, MASKING_ANGLE evaluates point-wise masking along module slant height and MASK_ANGLE_PASSIAS provides a representative average masking metric for diffuse shading models. AOI and AOI_PROJECTION can then be used to compute direct-incidence geometry for candidate tilts/azimuths. This helps balance structural density and annual yield.

A third scenario is operations diagnostics and model reconciliation. Performance engineers often see mismatch between expected and measured power at low sun angles. Geometry-first diagnostics can isolate causes quickly. PROJ_SOLAR_ZENITH projects sun position into the tracker-reference plane; SINGLEAXIS and SURFACE_ORIENT reconstruct effective plane orientation from tracker rotation logs; AOI then verifies whether incidence behavior is physically plausible. If discrepancy concentrates in specific azimuth windows, SHADED_FRACTION1D helps test row-shading hypotheses and torque-tube offset assumptions.

These tools are also appropriate for bankability workflows, digital twin calibration, and what-if studies for repowering. They are less appropriate for tasks that require atmospheric radiative transfer, bifacial view-factor modeling, or detailed thermal-electrical coupling on their own; in those cases, solar geometry is still required, but it is only one input layer.

How It Works The underlying implementation in this category is grounded in pvlib-python, especially modules in pvlib.irradiance, pvlib.tracking, and pvlib.shading. Conceptually, the math uses vectors and rotations in 3D space, then derives lower-dimensional quantities for row models.

At the incidence layer, define a unit surface normal vector \mathbf{n} and a unit sun vector \mathbf{s}. The angle of incidence is

heta_i = \cos^{-1}(\mathbf{n}\cdot\mathbf{s}).

AOI returns \theta_i in degrees. AOI_PROJECTION returns

\mu_i = \mathbf{n}\cdot\mathbf{s}=\cos(\theta_i),

which is often the direct-beam projection factor in transposition equations when positive. A negative value indicates the sun is geometrically behind the plane, which is useful for filtering non-illuminated intervals.

For tracker geometry on sloped terrain, the tracker axis is not always horizontal in global coordinates. CALC_AXIS_TILT maps terrain slope orientation and magnitude into effective axis tilt. CROSS_AXIS_TILT then computes slope component perpendicular to the axis. This perpendicular component strongly influences backtracking because mutual shading onset depends on inter-row geometry in the normal plane to row direction, not only on global slope.

The row-shading subproblem often uses 2D projections. PROJ_SOLAR_ZENITH reduces 3D sun position to an equivalent zenith in a plane defined by axis tilt and azimuth, which simplifies shading and tracker logic. This is mathematically equivalent to projecting the sun vector into a rotated frame aligned with row geometry.

Masking-angle tools quantify sky obstruction by a preceding row. MASKING_ANGLE computes point-wise blocking angle as a function of surface tilt, GCR, and normalized slant position. MASK_ANGLE_PASSIAS returns an average masking angle over the slant height (Passias formulation), frequently used as a compact descriptor in diffuse irradiance loss modeling. Intuitively, if pitch decreases (higher GCR), the blocked sky dome increases and masking angle rises. If tilt increases while pitch is fixed, vertical overlap geometry can increase masking near lower portions of the panel.

SHADED_FRACTION1D extends this idea to direct-beam row shading and returns a bounded fraction in [0,1] of collector width shaded by the neighboring row. It incorporates sun position, row orientation, collector width, pitch, axis tilt, optional cross-axis slope, and axis-to-surface offset. That makes it practical for both fixed-tilt and single-axis settings where structural geometry (for example, torque tube offset) matters.

Tracker kinematics are handled by SINGLEAXIS, which computes tracker rotation angle and corresponding incidence/orientation outputs for each timestamp. In true-tracking mode, the ideal rotation minimizes incidence. In backtracking mode, the commanded angle is modified to reduce or avoid row-to-row shadowing given GCR and cross-axis geometry. A simplified conceptual objective is

heta_{\text{cmd}}=\arg\min_{\theta\in[-\theta_{\max},\theta_{\max}]}\;\theta_i(\theta)

subject to anti-shading constraints when backtracking is enabled. Because the true constraint set depends on row geometry and sun projection, the implementation is more nuanced than a closed-form single equation, but this expression captures intent.

Finally, SURFACE_ORIENT converts tracker rotation back to conventional surface tilt and azimuth, allowing downstream irradiance and loss models to work in a standard plane-of-array format. In many model chains, this is the bridge between tracker-control calculations and transposition/irradiance modules.

Key assumptions users should check before interpreting outputs:

  • Angle conventions (degrees, azimuth reference) must be consistent across data feeds.
  • Apparent versus true solar zenith choice must match upstream ephemeris calculations.
  • Row models are geometric; they do not by themselves model electrical mismatch or diffuse anisotropy in full detail.
  • GCR and pitch definitions should be validated against the project’s structural design basis.
  • Backtracking interpretations depend on terrain decomposition into axis and cross-axis components.

When these assumptions are handled carefully, the category provides a robust geometric foundation for energy and shading calculations.

Practical Example Consider a 120 MW single-axis project on rolling terrain where the engineering team must choose between two candidate row spacings before final civil grading. The business question is straightforward: does tighter spacing improve land efficiency enough to justify additional shading losses and operational complexity?

Step 1: Define geometry and data. The team collects hourly apparent zenith and azimuth from the site’s weather year, establishes axis azimuth (north-south rows), imports terrain slope statistics from survey data, and drafts two spacing scenarios (e.g., pitch 6.5 m and 7.2 m). They also define module collector width and an estimated surface-to-axis offset based on the tracker design.

Step 2: Convert terrain into tracker-frame parameters. For each representative terrain class, they compute CALC_AXIS_TILT and CROSS_AXIS_TILT. This avoids a common modeling error where global slope is fed directly into backtracking assumptions without axis decomposition.

Step 3: Run tracker kinematics. Using sun positions and geometry inputs, they evaluate SINGLEAXIS for each timestamp and scenario. This returns tracker rotation along with AOI and derived orientation fields. If one scenario requires more aggressive backtracking near sunrise/sunset, that appears immediately in the rotation series.

Step 4: Convert to standard plane orientation. They pass tracker rotation outputs into SURFACE_ORIENT to obtain tilt/azimuth pairs that can be consumed uniformly by transposition and loss models.

Step 5: Compute direct-incidence geometry. They evaluate AOI and AOI_PROJECTION over the year to quantify how often incidence is favorable and how much beam component is geometrically available. Negative projection windows are flagged as non-illuminated orientation intervals.

Step 6: Quantify row shading explicitly. They run SHADED_FRACTION1D for each scenario using timestamped sun geometry and tracker rotation. This produces an interpretable time series of shaded fraction, which can be aggregated into monthly and annual shading indicators. If residual shading remains high despite backtracking in one terrain class, that class becomes a candidate for grading or altered spacing.

Step 7: Evaluate masking and diffuse penalties (if fixed-tilt subarrays exist or for comparative geometry diagnostics). They compute MASKING_ANGLE across slant positions and MASK_ANGLE_PASSIAS as an averaged descriptor. These metrics help explain why two layouts with similar beam-shading behavior can still differ in diffuse acceptance.

Step 8: Use projected sun diagnostics for troubleshooting edge cases. In intervals with unexpected clipping between modeled and measured performance during commissioning simulations, PROJ_SOLAR_ZENITH is used to validate row-plane projections and identify whether apparent anomalies are geometric or likely due to non-geometric losses.

Step 9: Make the decision. The team compares both spacing scenarios on annual net yield, low-sun performance volatility, and constructability constraints. Suppose the tighter pitch improves land use but raises annual shaded-fraction-weighted losses by 0.9% and increases morning ramp variance. Finance may still accept it if CAPEX savings dominate; otherwise the wider pitch is selected. The key benefit is that the trade-off is now supported by transparent geometric evidence rather than assumptions.

This workflow illustrates why the category is valuable in Excel-centric environments: each function is modular, auditable, and can be staged in tabular calculations while preserving consistency with pvlib.

How to Choose Use this category by matching the engineering question to the correct geometry layer. A practical sequence is: define axis/terrain frame, compute tracker or surface orientation, then evaluate incidence and shading. The table below summarizes which function to select.

Use Case Question Primary Function What It Returns Best For Tradeoffs / Notes
What is the incidence angle between sun and surface? AOI Angle in degrees Direct irradiance geometry checks Angle form is intuitive but less convenient than cosine for flux scaling
What is the cosine projection of beam on the surface? AOI_PROJECTION \cos(\theta_i) (signed) Beam transposition factors, back-side checks Must handle negative values explicitly
What axis tilt results from slope + axis azimuth? CALC_AXIS_TILT Effective axis tilt Sloped-site tracker setup Inputs are geometric; does not optimize layout itself
What is terrain slope perpendicular to axis? CROSS_AXIS_TILT Cross-axis tilt Backtracking realism on uneven ground Sensitive to azimuth convention consistency
What is point-wise masking along module height? MASKING_ANGLE Masking angle by slant position Detailed fixed-tilt row diagnostics Requires slant-height parameterization
What is average masking angle over a row? MASK_ANGLE_PASSIAS Average masking angle Compact diffuse-blocking indicator Averaging can hide localized extremes
What is projected solar zenith in row/axis plane? PROJ_SOLAR_ZENITH Projected zenith angle Simplified row-plane shading logic Projection aids interpretation but is not full irradiance alone
How much of collector width is beam-shaded by adjacent row? SHADED_FRACTION1D Fraction from 0 to 1 Timestamp shading losses for rows Geometric model; not an electrical mismatch model
What tracker rotation and orientation solve single-axis tracking? SINGLEAXIS Tracker angle, AOI, surface tilt/azimuth Operational and simulation tracker kinematics Backtracking outcome depends on GCR and cross-axis settings
How do I convert tracker angle into surface tilt/azimuth? SURFACE_ORIENT Surface tilt and azimuth Integrating tracker outputs with transposition workflows Requires correct axis orientation conventions

A quick decision flow:

graph TD
    A[Start: What are you solving?] --> B{Tracker system?}
    B -- Yes --> C[Compute terrain frame]
    C --> C1[Use CALC_AXIS_TILT]
    C1 --> C2[Use CROSS_AXIS_TILT]
    C2 --> D[Run SINGLEAXIS]
    D --> E[Convert with SURFACE_ORIENT]
    E --> F[Need shading amount?]
    F -- Yes --> G[Use SHADED_FRACTION1D]
    F -- No --> H[Use AOI or AOI_PROJECTION]
    B -- No --> I[Fixed-tilt row analysis]
    I --> J[Use AOI or AOI_PROJECTION]
    I --> K[Use MASKING_ANGLE]
    K --> L[Need average mask metric?]
    L -- Yes --> M[Use MASK_ANGLE_PASSIAS]
    I --> N[Need row-plane projection?]
    N -- Yes --> O[Use PROJ_SOLAR_ZENITH]

In practice, most advanced workflows combine multiple functions rather than choosing only one. A reliable pattern is to begin with frame-defining tools (CALC_AXIS_TILT, CROSS_AXIS_TILT), proceed to orientation/kinematics (SINGLEAXIS, SURFACE_ORIENT), and finish with incidence and shading diagnostics (AOI, AOI_PROJECTION, PROJ_SOLAR_ZENITH, MASKING_ANGLE, MASK_ANGLE_PASSIAS, SHADED_FRACTION1D). This layered approach minimizes geometry inconsistencies and produces outputs that are easier to audit in design reviews and investment committees.

AOI

This function computes the angle between the solar vector and the surface normal.

It is a fundamental calculation for determining the amount of direct irradiance reaching a PV module or thermal collector.

Excel Usage

=AOI(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth)
  • surface_tilt (list[list], required): Panel tilt from horizontal (degrees).
  • surface_azimuth (list[list], required): Panel azimuth direction (degrees).
  • solar_zenith (list[list], required): Solar zenith angle (degrees).
  • solar_azimuth (list[list], required): Solar azimuth angle (degrees).

Returns (list[list]): 2D list of AOI values (degrees), or an error string.

Example 1: Sun directly normal to surface

Inputs:

surface_tilt surface_azimuth solar_zenith solar_azimuth
30 180 30 180

Excel formula:

=AOI({30}, {180}, {30}, {180})

Expected output:

0

Example 2: East-facing surface in morning sun

Inputs:

surface_tilt surface_azimuth solar_zenith solar_azimuth
25 90 40 100

Excel formula:

=AOI({25}, {90}, {40}, {100})

Expected output:

15.888

Example 3: Vectorized AOI for two timesteps

Inputs:

surface_tilt surface_azimuth solar_zenith solar_azimuth
20 20 180 180 20 60 180 210

Excel formula:

=AOI({20,20}, {180,180}, {20,60}, {180,210})

Expected output:

Result
0
43.4178
Example 4: Scalar inputs are accepted

Inputs:

surface_tilt surface_azimuth solar_zenith solar_azimuth
35 180 50 170

Excel formula:

=AOI(35, 180, 50, 170)

Expected output:

16.4129

Python Code

Show Code
import pandas as pd
import numpy as np
from pvlib.irradiance import aoi as result_func

def aoi(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth):
    """
    Calculate the angle of incidence (AOI) for a surface.

    See: https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.irradiance.aoi.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        surface_tilt (list[list]): Panel tilt from horizontal (degrees).
        surface_azimuth (list[list]): Panel azimuth direction (degrees).
        solar_zenith (list[list]): Solar zenith angle (degrees).
        solar_azimuth (list[list]): Solar azimuth angle (degrees).

    Returns:
        list[list]: 2D list of AOI values (degrees), or an error string.
    """
    try:
        def flatten_num(data):
            if not isinstance(data, list): return [float(data)]
            flat = []
            for row in data:
                row = row if isinstance(row, list) else [row]
                for val in row:
                    if val == "": flat.append(float('nan'))
                    else: flat.append(float(val))
            return flat

        t_list = flatten_num(surface_tilt)
        a_list = flatten_num(surface_azimuth)
        z_list = flatten_num(solar_zenith)
        az_list = flatten_num(solar_azimuth)

        n = len(t_list)
        if n == 0 or len(a_list) != n or len(z_list) != n or len(az_list) != n:
            return "Error: All input arrays must have the same non-zero length"

        res = result_func(
            surface_tilt=np.array(t_list),
            surface_azimuth=np.array(a_list),
            solar_zenith=np.array(z_list),
            solar_azimuth=np.array(az_list)
        )

        return [[float(v) if not pd.isna(v) else ""] for v in res]
    except Exception as e:
        return f"Error: {str(e)}"

Online Calculator

Panel tilt from horizontal (degrees).
Panel azimuth direction (degrees).
Solar zenith angle (degrees).
Solar azimuth angle (degrees).

AOI_PROJECTION

This function computes the cosine of the angle of incidence.

It returns the projection of the sun’s unit vector onto the surface’s normal unit vector. Negative values indicate the sun is behind the surface.

Excel Usage

=AOI_PROJECTION(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth)
  • surface_tilt (list[list], required): Panel tilt from horizontal (degrees).
  • surface_azimuth (list[list], required): Panel azimuth direction (degrees).
  • solar_zenith (list[list], required): Solar zenith angle (degrees).
  • solar_azimuth (list[list], required): Solar azimuth angle (degrees).

Returns (list[list]): 2D list of AOI projections (unitless), or an error string.

Example 1: Projection for sun directly normal to surface

Inputs:

surface_tilt surface_azimuth solar_zenith solar_azimuth
30 180 30 180

Excel formula:

=AOI_PROJECTION({30}, {180}, {30}, {180})

Expected output:

1

Example 2: Projection negative when sun is behind surface

Inputs:

surface_tilt surface_azimuth solar_zenith solar_azimuth
30 180 30 0

Excel formula:

=AOI_PROJECTION({30}, {180}, {30}, {0})

Expected output:

0.5

Example 3: Vectorized projection over two sun positions

Inputs:

surface_tilt surface_azimuth solar_zenith solar_azimuth
20 20 180 180 20 60 180 210

Excel formula:

=AOI_PROJECTION({20,20}, {180,180}, {20,60}, {180,210})

Expected output:

Result
1
0.726361
Example 4: Scalar projection inputs

Inputs:

surface_tilt surface_azimuth solar_zenith solar_azimuth
20 180 20 180

Excel formula:

=AOI_PROJECTION(20, 180, 20, 180)

Expected output:

1

Python Code

Show Code
import pandas as pd
import numpy as np
from pvlib.irradiance import aoi_projection as result_func

def aoi_projection(surface_tilt, surface_azimuth, solar_zenith, solar_azimuth):
    """
    Calculate the dot product of the sun position and surface normal (cosine of AOI).

    See: https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.irradiance.aoi_projection.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        surface_tilt (list[list]): Panel tilt from horizontal (degrees).
        surface_azimuth (list[list]): Panel azimuth direction (degrees).
        solar_zenith (list[list]): Solar zenith angle (degrees).
        solar_azimuth (list[list]): Solar azimuth angle (degrees).

    Returns:
        list[list]: 2D list of AOI projections (unitless), or an error string.
    """
    try:
        def flatten_num(data):
            if not isinstance(data, list): return [float(data)]
            flat = []
            for row in data:
                row = row if isinstance(row, list) else [row]
                for val in row:
                    if val == "": flat.append(float('nan'))
                    else: flat.append(float(val))
            return flat

        t_list = flatten_num(surface_tilt)
        a_list = flatten_num(surface_azimuth)
        z_list = flatten_num(solar_zenith)
        az_list = flatten_num(solar_azimuth)

        n = len(t_list)
        if n == 0 or len(a_list) != n or len(z_list) != n or len(az_list) != n:
            return "Error: All input arrays must have the same non-zero length"

        res = result_func(
            surface_tilt=np.array(t_list),
            surface_azimuth=np.array(a_list),
            solar_zenith=np.array(z_list),
            solar_azimuth=np.array(az_list)
        )

        return [[float(v) if not pd.isna(v) else ""] for v in res]
    except Exception as e:
        return f"Error: {str(e)}"

Online Calculator

Panel tilt from horizontal (degrees).
Panel azimuth direction (degrees).
Solar zenith angle (degrees).
Solar azimuth angle (degrees).

CALC_AXIS_TILT

This function computes the inclination of a tracker’s rotation axis when it is installed on a sloped plane.

It uses the slope’s tilt and azimuth along with the tracker’s axis azimuth to determine the effective axis tilt relative to horizontal.

Excel Usage

=CALC_AXIS_TILT(slope_azimuth, slope_tilt, axis_azimuth)
  • slope_azimuth (float, required): Direction of the normal to the slope (degrees).
  • slope_tilt (float, required): Tilt of the slope relative to horizontal (degrees).
  • axis_azimuth (float, required): Direction of the tracker axis on the horizontal plane (degrees).

Returns (float): The axis tilt (degrees), or an error string.

Example 1: Axis tilt on a 10 degree south slope

Inputs:

slope_azimuth slope_tilt axis_azimuth
180 10 180

Excel formula:

=CALC_AXIS_TILT(180, 10, 180)

Expected output:

10

Example 2: Axis tilt on flat terrain

Inputs:

slope_azimuth slope_tilt axis_azimuth
180 0 180

Excel formula:

=CALC_AXIS_TILT(180, 0, 180)

Expected output:

0

Example 3: East-west axis on south-facing slope

Inputs:

slope_azimuth slope_tilt axis_azimuth
180 10 90

Excel formula:

=CALC_AXIS_TILT(180, 10, 90)

Expected output:

6.18618e-16

Example 4: Gentle north slope with north-south axis

Inputs:

slope_azimuth slope_tilt axis_azimuth
0 5 0

Excel formula:

=CALC_AXIS_TILT(0, 5, 0)

Expected output:

5

Python Code

Show Code
from pvlib.tracking import calc_axis_tilt as result_func

def calc_axis_tilt(slope_azimuth, slope_tilt, axis_azimuth):
    """
    Calculate tracker axis tilt on sloped terrain.

    See: https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.tracking.calc_axis_tilt.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        slope_azimuth (float): Direction of the normal to the slope (degrees).
        slope_tilt (float): Tilt of the slope relative to horizontal (degrees).
        axis_azimuth (float): Direction of the tracker axis on the horizontal plane (degrees).

    Returns:
        float: The axis tilt (degrees), or an error string.
    """
    try:
        sa = float(slope_azimuth)
        st = float(slope_tilt)
        aa = float(axis_azimuth)

        res = result_func(
            slope_azimuth=sa,
            slope_tilt=st,
            axis_azimuth=aa
        )

        return float(res)
    except Exception as e:
        return f"Error: {str(e)}"

Online Calculator

Direction of the normal to the slope (degrees).
Tilt of the slope relative to horizontal (degrees).
Direction of the tracker axis on the horizontal plane (degrees).

CROSS_AXIS_TILT

This function computes the tilt perpendicular to the tracker axis, which is essential for accurate backtracking on uneven ground.

It helps layout engineers determine the effective slope between tracker rows, ensuring that backtracking logic correctly avoids mutual shading.

Excel Usage

=CROSS_AXIS_TILT(slope_azimuth, slope_tilt, axis_azimuth, axis_tilt)
  • slope_azimuth (float, required): Direction of the normal to the slope (degrees).
  • slope_tilt (float, required): Tilt of the slope relative to horizontal (degrees).
  • axis_azimuth (float, required): Direction of the tracker axis (degrees).
  • axis_tilt (float, required): Tilt of the tracker axis relative to horizontal (degrees).

Returns (float): The cross-axis tilt (degrees), or an error string.

Example 1: Cross-axis tilt on an east-sloping terrain

Inputs:

slope_azimuth slope_tilt axis_azimuth axis_tilt
90 5 180 0

Excel formula:

=CROSS_AXIS_TILT(90, 5, 180, 0)

Expected output:

-5

Example 2: Cross-axis tilt is zero on flat terrain

Inputs:

slope_azimuth slope_tilt axis_azimuth axis_tilt
180 0 180 0

Excel formula:

=CROSS_AXIS_TILT(180, 0, 180, 0)

Expected output:

0

Example 3: Axis aligned with slope direction

Inputs:

slope_azimuth slope_tilt axis_azimuth axis_tilt
180 8 180 8

Excel formula:

=CROSS_AXIS_TILT(180, 8, 180, 8)

Expected output:

0

Example 4: West-sloping terrain case

Inputs:

slope_azimuth slope_tilt axis_azimuth axis_tilt
270 6 180 0

Excel formula:

=CROSS_AXIS_TILT(270, 6, 180, 0)

Expected output:

6

Python Code

Show Code
from pvlib.tracking import calc_cross_axis_tilt as result_func

def cross_axis_tilt(slope_azimuth, slope_tilt, axis_azimuth, axis_tilt):
    """
    Calculate cross-axis tilt for single-axis trackers on sloped terrain.

    See: https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.tracking.calc_cross_axis_tilt.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        slope_azimuth (float): Direction of the normal to the slope (degrees).
        slope_tilt (float): Tilt of the slope relative to horizontal (degrees).
        axis_azimuth (float): Direction of the tracker axis (degrees).
        axis_tilt (float): Tilt of the tracker axis relative to horizontal (degrees).

    Returns:
        float: The cross-axis tilt (degrees), or an error string.
    """
    try:
        sa = float(slope_azimuth)
        st = float(slope_tilt)
        aa = float(axis_azimuth)
        at = float(axis_tilt)

        res = result_func(
            slope_azimuth=sa,
            slope_tilt=st,
            axis_azimuth=aa,
            axis_tilt=at
        )

        return float(res)
    except Exception as e:
        return f"Error: {str(e)}"

Online Calculator

Direction of the normal to the slope (degrees).
Tilt of the slope relative to horizontal (degrees).
Direction of the tracker axis (degrees).
Tilt of the tracker axis relative to horizontal (degrees).

MASK_ANGLE_PASSIAS

This function computes the average masking angle for a row in a large multi-row PV array using the Passias model.

Unlike the standard masking angle which is evaluated at a specific point, this function integrates the view factor over the entire module surface to provide a representative average value for diffuse shading models.

Excel Usage

=MASK_ANGLE_PASSIAS(surface_tilt, gcr)
  • surface_tilt (list[list], required): Panel tilt from horizontal (degrees).
  • gcr (float, required): Ground coverage ratio (panel width / row spacing).

Returns (list[list]): 2D list of average masking angles (degrees), or an error string.

Example 1: Average masking angle for 20 deg tilt

Inputs:

surface_tilt gcr
20 0.5

Excel formula:

=MASK_ANGLE_PASSIAS({20}, 0.5)

Expected output:

7.20981

Example 2: Higher tilt increases average masking angle

Inputs:

surface_tilt gcr
35 0.5

Excel formula:

=MASK_ANGLE_PASSIAS({35}, 0.5)

Expected output:

11.1343

Example 3: Lower ground coverage ratio case

Inputs:

surface_tilt gcr
25 0.3

Excel formula:

=MASK_ANGLE_PASSIAS({25}, 0.3)

Expected output:

4.44217

Example 4: Vectorized surface tilt inputs

Inputs:

surface_tilt gcr
10 20 30 0.5

Excel formula:

=MASK_ANGLE_PASSIAS({10,20,30}, 0.5)

Expected output:

Result
3.79416
7.20981
10.0028

Python Code

Show Code
import pandas as pd
import numpy as np
from pvlib.shading import masking_angle_passias as result_func

def mask_angle_passias(surface_tilt, gcr):
    """
    Calculate the average masking angle over the slant height of a row.

    See: https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.shading.masking_angle_passias.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        surface_tilt (list[list]): Panel tilt from horizontal (degrees).
        gcr (float): Ground coverage ratio (panel width / row spacing).

    Returns:
        list[list]: 2D list of average masking angles (degrees), or an error string.
    """
    try:
        def flatten_num(data):
            if not isinstance(data, list): return [float(data)]
            flat = []
            for row in data:
                row = row if isinstance(row, list) else [row]
                for val in row:
                    if val == "": flat.append(float('nan'))
                    else: flat.append(float(val))
            return flat

        t_list = flatten_num(surface_tilt)
        if len(t_list) == 0:
            return "Error: surface_tilt array cannot be empty"

        gc = float(gcr) if gcr is not None else 0.5

        res = result_func(
            surface_tilt=np.array(t_list),
            gcr=gc
        )

        return [[float(v) if not pd.isna(v) else ""] for v in res]
    except Exception as e:
        return f"Error: {str(e)}"

Online Calculator

Panel tilt from horizontal (degrees).
Ground coverage ratio (panel width / row spacing).

MASKING_ANGLE

This function computes the masking angle for a point on a collector’s surface in a multi-row array.

The masking angle indicates the portion of the sky dome blocked by the preceding row. It depends on the surface tilt, ground coverage ratio (GCR), and the position up the module slant height.

Excel Usage

=MASKING_ANGLE(surface_tilt, gcr, slant_height)
  • surface_tilt (list[list], required): Panel tilt from horizontal (degrees).
  • gcr (float, required): Ground coverage ratio (panel width / row spacing).
  • slant_height (list[list], required): Fraction of the module’s slant height (0 at bottom, 1 at top).

Returns (list[list]): 2D list of masking angles (degrees), or an error string.

Example 1: Masking angle at the bottom of the row

Inputs:

surface_tilt gcr slant_height
20 0.5 0

Excel formula:

=MASKING_ANGLE({20}, 0.5, {0})

Expected output:

17.878

Example 2: Masking angle at the top of the row

Inputs:

surface_tilt gcr slant_height
20 0.5 1

Excel formula:

=MASKING_ANGLE({20}, 0.5, {1})

Expected output:

0

Example 3: Masking angle at mid slant height

Inputs:

surface_tilt gcr slant_height
20 0.5 0.5

Excel formula:

=MASKING_ANGLE({20}, 0.5, {0.5})

Expected output:

6.37692

Example 4: Vectorized slant-height evaluation

Inputs:

surface_tilt gcr slant_height
20 20 20 0.5 0 0.5 1

Excel formula:

=MASKING_ANGLE({20,20,20}, 0.5, {0,0.5,1})

Expected output:

Result
17.878
6.37692
0

Python Code

Show Code
import pandas as pd
import numpy as np
from pvlib.shading import masking_angle as result_func

def masking_angle(surface_tilt, gcr, slant_height):
    """
    Calculate the elevation angle below which diffuse irradiance is blocked.

    See: https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.shading.masking_angle.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        surface_tilt (list[list]): Panel tilt from horizontal (degrees).
        gcr (float): Ground coverage ratio (panel width / row spacing).
        slant_height (list[list]): Fraction of the module's slant height (0 at bottom, 1 at top).

    Returns:
        list[list]: 2D list of masking angles (degrees), or an error string.
    """
    try:
        def flatten_num(data):
            if not isinstance(data, list): return [float(data)]
            flat = []
            for row in data:
                row = row if isinstance(row, list) else [row]
                for val in row:
                    if val == "": flat.append(float('nan'))
                    else: flat.append(float(val))
            return flat

        t_list = flatten_num(surface_tilt)
        h_list = flatten_num(slant_height)

        n = len(t_list)
        if n == 0 or len(h_list) != n:
            return "Error: surface_tilt and slant_height arrays must have the same non-zero length"

        gc = float(gcr) if gcr is not None else 0.5

        res = result_func(
            surface_tilt=np.array(t_list),
            gcr=gc,
            slant_height=np.array(h_list)
        )

        return [[float(v) if not pd.isna(v) else ""] for v in res]
    except Exception as e:
        return f"Error: {str(e)}"

Online Calculator

Panel tilt from horizontal (degrees).
Ground coverage ratio (panel width / row spacing).
Fraction of the module's slant height (0 at bottom, 1 at top).

PROJ_SOLAR_ZENITH

This function projects the solar zenith angle onto a plane defined by an axis tilt and azimuth.

This projection is critical for modeling single-axis trackers and for simplifying 3D shading problems into a 2D plane perpendicular to the collector rows.

Excel Usage

=PROJ_SOLAR_ZENITH(solar_zenith, solar_azimuth, axis_tilt, axis_azimuth)
  • solar_zenith (list[list], required): Sun’s apparent zenith angle (degrees).
  • solar_azimuth (list[list], required): Sun’s azimuth angle (degrees).
  • axis_tilt (float, optional, default: 0): Tilt angle of the reference axis (degrees).
  • axis_azimuth (float, optional, default: 0): Azimuth angle of the reference axis (degrees).

Returns (list[list]): 2D list of projected solar zenith angles (degrees), or an error string.

Example 1: Projection on a north-south axis at noon

Inputs:

solar_zenith solar_azimuth axis_tilt axis_azimuth
30 180 0 180

Excel formula:

=PROJ_SOLAR_ZENITH({30}, {180}, 0, 180)

Expected output:

0

Example 2: Projection on east-west axis

Inputs:

solar_zenith solar_azimuth axis_tilt axis_azimuth
30 180 0 90

Excel formula:

=PROJ_SOLAR_ZENITH({30}, {180}, 0, 90)

Expected output:

30

Example 3: Projection with tilted axis

Inputs:

solar_zenith solar_azimuth axis_tilt axis_azimuth
50 200 10 180

Excel formula:

=PROJ_SOLAR_ZENITH({50}, {200}, 10, 180)

Expected output:

19.0672

Example 4: Vectorized projected zenith angles

Inputs:

solar_zenith solar_azimuth axis_tilt axis_azimuth
20 40 60 120 180 240 0 180

Excel formula:

=PROJ_SOLAR_ZENITH({20,40,60}, {120,180,240}, 0, 180)

Expected output:

Result
-17.4952
0
56.3099

Python Code

Show Code
import pandas as pd
import numpy as np
from pvlib.shading import projected_solar_zenith_angle as result_func

def proj_solar_zenith(solar_zenith, solar_azimuth, axis_tilt=0, axis_azimuth=0):
    """
    Calculate the projected solar zenith angle in the tracker reference plane.

    See: https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.shading.projected_solar_zenith_angle.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        solar_zenith (list[list]): Sun's apparent zenith angle (degrees).
        solar_azimuth (list[list]): Sun's azimuth angle (degrees).
        axis_tilt (float, optional): Tilt angle of the reference axis (degrees). Default is 0.
        axis_azimuth (float, optional): Azimuth angle of the reference axis (degrees). Default is 0.

    Returns:
        list[list]: 2D list of projected solar zenith angles (degrees), or an error string.
    """
    try:
        def flatten_num(data):
            if not isinstance(data, list): return [float(data)]
            flat = []
            for row in data:
                row = row if isinstance(row, list) else [row]
                for val in row:
                    if val == "": flat.append(float('nan'))
                    else: flat.append(float(val))
            return flat

        z_list = flatten_num(solar_zenith)
        az_list = flatten_num(solar_azimuth)

        n = len(z_list)
        if n == 0 or len(az_list) != n:
            return "Error: solar_zenith and solar_azimuth arrays must have the same non-zero length"

        at = float(axis_tilt) if axis_tilt is not None else 0.0
        aa = float(axis_azimuth) if axis_azimuth is not None else 0.0

        res = result_func(
            solar_zenith=np.array(z_list),
            solar_azimuth=np.array(az_list),
            axis_tilt=at,
            axis_azimuth=aa
        )

        return [[float(v) if not pd.isna(v) else ""] for v in res]
    except Exception as e:
        return f"Error: {str(e)}"

Online Calculator

Sun's apparent zenith angle (degrees).
Sun's azimuth angle (degrees).
Tilt angle of the reference axis (degrees).
Azimuth angle of the reference axis (degrees).

SHADED_FRACTION1D

This function computes the beam-shading fraction for row-based PV systems (both fixed-tilt and single-axis trackers).

It models row-to-row shading based on collector geometry, sun angles, and row orientation. It supports terrain slope perpendicular to the rows (cross-axis slope) and torque tube offsets. A value of 1 indicates complete shading, while 0 indicates no shading.

Excel Usage

=SHADED_FRACTION1D(solar_zenith, solar_azimuth, axis_azimuth, shaded_row_rotation, collector_width, pitch, axis_tilt, surface_to_axis_offset, cross_axis_slope)
  • solar_zenith (list[list], required): Sun’s apparent zenith angle (degrees).
  • solar_azimuth (list[list], required): Sun’s azimuth angle (degrees).
  • axis_azimuth (float, required): Direction of the row axes (degrees).
  • shaded_row_rotation (list[list], required): Rotation angle of the receiving row (degrees).
  • collector_width (float, required): Width (vertical length) of the tilted row.
  • pitch (float, required): Axis-to-axis spacing between rows.
  • axis_tilt (float, optional, default: 0): Tilt of the row axis from horizontal (degrees).
  • surface_to_axis_offset (float, optional, default: 0): Distance between the rotation axis and the collector surface.
  • cross_axis_slope (float, optional, default: 0): Terrain slope perpendicular to the rows (degrees).

Returns (list[list]): 2D list of shaded fractions (0 to 1), or an error string.

Example 1: Shaded fraction in high zenith morning condition

Inputs:

solar_zenith solar_azimuth axis_azimuth shaded_row_rotation collector_width pitch axis_tilt surface_to_axis_offset cross_axis_slope
80 135 90 30 2 3 0 0.05 0

Excel formula:

=SHADED_FRACTION1D({80}, {135}, 90, {30}, 2, 3, 0, 0.05, 0)

Expected output:

0.477557

Example 2: Lower zenith reduces shading fraction

Inputs:

solar_zenith solar_azimuth axis_azimuth shaded_row_rotation collector_width pitch axis_tilt surface_to_axis_offset cross_axis_slope
50 135 90 30 2 3 0 0.05 0

Excel formula:

=SHADED_FRACTION1D({50}, {135}, 90, {30}, 2, 3, 0, 0.05, 0)

Expected output:

0

Example 3: Vectorized shading fraction for two times

Inputs:

solar_zenith solar_azimuth axis_azimuth shaded_row_rotation collector_width pitch axis_tilt surface_to_axis_offset cross_axis_slope
80 70 135 145 90 30 30 2 3 0 0.05 0

Excel formula:

=SHADED_FRACTION1D({80,70}, {135,145}, 90, {30,30}, 2, 3, 0, 0.05, 0)

Expected output:

Result
0.477557
0.246733
Example 4: Shaded fraction with cross-axis slope

Inputs:

solar_zenith solar_azimuth axis_azimuth shaded_row_rotation collector_width pitch axis_tilt surface_to_axis_offset cross_axis_slope
80 90 180 -30 1.4 3 0 0.1 7

Excel formula:

=SHADED_FRACTION1D({80}, {90}, 180, {-30}, 1.4, 3, 0, 0.1, 7)

Expected output:

0.824218

Python Code

Show Code
import pandas as pd
import numpy as np
from pvlib.shading import shaded_fraction1d as result_func

def shaded_fraction1d(solar_zenith, solar_azimuth, axis_azimuth, shaded_row_rotation, collector_width, pitch, axis_tilt=0, surface_to_axis_offset=0, cross_axis_slope=0):
    """
    Calculate the fraction of a collector width shaded by an adjacent row.

    See: https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.shading.shaded_fraction1d.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        solar_zenith (list[list]): Sun's apparent zenith angle (degrees).
        solar_azimuth (list[list]): Sun's azimuth angle (degrees).
        axis_azimuth (float): Direction of the row axes (degrees).
        shaded_row_rotation (list[list]): Rotation angle of the receiving row (degrees).
        collector_width (float): Width (vertical length) of the tilted row.
        pitch (float): Axis-to-axis spacing between rows.
        axis_tilt (float, optional): Tilt of the row axis from horizontal (degrees). Default is 0.
        surface_to_axis_offset (float, optional): Distance between the rotation axis and the collector surface. Default is 0.
        cross_axis_slope (float, optional): Terrain slope perpendicular to the rows (degrees). Default is 0.

    Returns:
        list[list]: 2D list of shaded fractions (0 to 1), or an error string.
    """
    try:
        def flatten_num(data):
            if not isinstance(data, list): return [float(data)]
            flat = []
            for row in data:
                row = row if isinstance(row, list) else [row]
                for val in row:
                    if val == "": flat.append(float('nan'))
                    else: flat.append(float(val))
            return flat

        z_list = flatten_num(solar_zenith)
        az_list = flatten_num(solar_azimuth)
        rot_list = flatten_num(shaded_row_rotation)

        n = len(z_list)
        if n == 0 or len(az_list) != n or len(rot_list) != n:
            return "Error: All input arrays must have the same non-zero length"

        aa = float(axis_azimuth)
        cw = float(collector_width)
        pi = float(pitch)
        at = float(axis_tilt) if axis_tilt is not None else 0.0
        off = float(surface_to_axis_offset) if surface_to_axis_offset is not None else 0.0
        cas = float(cross_axis_slope) if cross_axis_slope is not None else 0.0

        res = result_func(
            solar_zenith=np.array(z_list),
            solar_azimuth=np.array(az_list),
            axis_azimuth=aa,
            shaded_row_rotation=np.array(rot_list),
            collector_width=cw,
            pitch=pi,
            axis_tilt=at,
            surface_to_axis_offset=off,
            cross_axis_slope=cas
        )

        return [[float(v) if not pd.isna(v) else ""] for v in res]
    except Exception as e:
        return f"Error: {str(e)}"

Online Calculator

Sun's apparent zenith angle (degrees).
Sun's azimuth angle (degrees).
Direction of the row axes (degrees).
Rotation angle of the receiving row (degrees).
Width (vertical length) of the tilted row.
Axis-to-axis spacing between rows.
Tilt of the row axis from horizontal (degrees).
Distance between the rotation axis and the collector surface.
Terrain slope perpendicular to the rows (degrees).

SINGLEAXIS

This function calculates the optimal rotation angle for a single-axis tracker to minimize the angle of incidence (AOI) with the sun.

It can optionally account for axis tilt, axis azimuth, and backtracking to avoid row-to-row shading. The function returns the tracker rotation angle, AOI, and the resulting surface orientation (tilt and azimuth).

Excel Usage

=SINGLEAXIS(apparent_zenith, solar_azimuth, axis_tilt, axis_azimuth, max_angle, backtrack, gcr, cross_axis_tilt)
  • apparent_zenith (list[list], required): Solar apparent zenith angles (degrees).
  • solar_azimuth (list[list], required): Solar apparent azimuth angles (degrees).
  • axis_tilt (float, optional, default: 0): Tilt of the rotation axis (degrees).
  • axis_azimuth (float, optional, default: 0): Compass direction of the rotation axis (degrees).
  • max_angle (float, optional, default: 90): Maximum rotation angle in either direction (degrees).
  • backtrack (bool, optional, default: true): Enable backtracking to avoid row shading.
  • gcr (float, optional, default: 0.2857): Ground coverage ratio (ratio of panel area to ground area).
  • cross_axis_tilt (float, optional, default: 0): Terrain slope perpendicular to the tracker axis (degrees).

Returns (list[list]): 2D list [[tracker_theta, aoi, surface_tilt, surface_azimuth]], or an error string.

Example 1: Perfect south tracker at noon

Inputs:

apparent_zenith solar_azimuth axis_tilt axis_azimuth max_angle backtrack gcr
30 180 0 180 60 true 0.3

Excel formula:

=SINGLEAXIS({30}, {180}, 0, 180, 60, TRUE, 0.3)

Expected output:

Result
0 30 0 270
Example 2: Morning sun rotates tracker eastward

Inputs:

apparent_zenith solar_azimuth axis_tilt axis_azimuth max_angle backtrack gcr
45 120 0 180 60 true 0.3

Excel formula:

=SINGLEAXIS({45}, {120}, 0, 180, 60, TRUE, 0.3)

Expected output:

Result
-40.8934 20.7048 40.8934 90
Example 3: Vectorized tracker results for two timesteps

Inputs:

apparent_zenith solar_azimuth axis_tilt axis_azimuth max_angle backtrack gcr
30 60 180 220 0 180 60 true 0.3

Excel formula:

=SINGLEAXIS({30,60}, {180,220}, 0, 180, 60, TRUE, 0.3)

Expected output:

Result
0 30 0 270
48.0699 41.5608 48.0699 270
Example 4: True-tracking without backtracking

Inputs:

apparent_zenith solar_azimuth axis_tilt axis_azimuth max_angle backtrack gcr
50 200 0 180 75 false 0.3

Excel formula:

=SINGLEAXIS({50}, {200}, 0, 180, 75, FALSE, 0.3)

Expected output:

Result
22.176 46.0418 22.176 270

Python Code

Show Code
import pandas as pd
import numpy as np
from pvlib.tracking import singleaxis as result_func

def singleaxis(apparent_zenith, solar_azimuth, axis_tilt=0, axis_azimuth=0, max_angle=90, backtrack=True, gcr=0.2857, cross_axis_tilt=0):
    """
    Determine the rotation angle and incidence angle for a single-axis tracker.

    See: https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.tracking.singleaxis.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        apparent_zenith (list[list]): Solar apparent zenith angles (degrees).
        solar_azimuth (list[list]): Solar apparent azimuth angles (degrees).
        axis_tilt (float, optional): Tilt of the rotation axis (degrees). Default is 0.
        axis_azimuth (float, optional): Compass direction of the rotation axis (degrees). Default is 0.
        max_angle (float, optional): Maximum rotation angle in either direction (degrees). Default is 90.
        backtrack (bool, optional): Enable backtracking to avoid row shading. Default is True.
        gcr (float, optional): Ground coverage ratio (ratio of panel area to ground area). Default is 0.2857.
        cross_axis_tilt (float, optional): Terrain slope perpendicular to the tracker axis (degrees). Default is 0.

    Returns:
        list[list]: 2D list [[tracker_theta, aoi, surface_tilt, surface_azimuth]], or an error string.
    """
    try:
        def flatten_num(data):
            if not isinstance(data, list): return [float(data)]
            flat = []
            for row in data:
                row = row if isinstance(row, list) else [row]
                for val in row:
                    if val == "": flat.append(float('nan'))
                    else: flat.append(float(val))
            return flat

        zen_list = flatten_num(apparent_zenith)
        az_list = flatten_num(solar_azimuth)

        n = len(zen_list)
        if n == 0 or len(az_list) != n:
            return "Error: zenith and azimuth arrays must have the same non-zero length"

        at = float(axis_tilt) if axis_tilt is not None else 0.0
        aa = float(axis_azimuth) if axis_azimuth is not None else 0.0
        ma = float(max_angle) if max_angle is not None else 90.0
        bt = bool(backtrack) if backtrack is not None else True
        gc = float(gcr) if gcr is not None else 0.2857
        cat = float(cross_axis_tilt) if cross_axis_tilt is not None else 0.0

        res = result_func(
            apparent_zenith=np.array(zen_list),
            solar_azimuth=np.array(az_list),
            axis_tilt=at,
            axis_azimuth=aa,
            max_angle=ma,
            backtrack=bt,
            gcr=gc,
            cross_axis_tilt=cat
        )

        # res is a DataFrame with columns: tracker_theta, aoi, surface_tilt, surface_azimuth
        out = []
        tt = res['tracker_theta']
        aoi = res['aoi']
        st = res['surface_tilt']
        sa = res['surface_azimuth']

        for i in range(n):
            out.append([
                float(tt[i]) if not pd.isna(tt[i]) else "",
                float(aoi[i]) if not pd.isna(aoi[i]) else "",
                float(st[i]) if not pd.isna(st[i]) else "",
                float(sa[i]) if not pd.isna(sa[i]) else ""
            ])
        return out
    except Exception as e:
        return f"Error: {str(e)}"

Online Calculator

Solar apparent zenith angles (degrees).
Solar apparent azimuth angles (degrees).
Tilt of the rotation axis (degrees).
Compass direction of the rotation axis (degrees).
Maximum rotation angle in either direction (degrees).
Enable backtracking to avoid row shading.
Ground coverage ratio (ratio of panel area to ground area).
Terrain slope perpendicular to the tracker axis (degrees).

SURFACE_ORIENT

This function converts the rotation angle of a single-axis tracker into the conventional surface tilt and azimuth angles.

It accounts for the tilt and orientation of the tracker’s axis. This is useful for passing tracker geometry into standard transposition and irradiance models.

Excel Usage

=SURFACE_ORIENT(tracker_theta, axis_tilt, axis_azimuth)
  • tracker_theta (list[list], required): Tracker rotation angle (degrees).
  • axis_tilt (float, optional, default: 0): Tilt of the rotation axis (degrees).
  • axis_azimuth (float, optional, default: 0): Compass direction of the rotation axis (degrees).

Returns (list[list]): 2D list [[surface_tilt, surface_azimuth]], or an error string.

Example 1: 30 degree westward rotation on N-S axis

Inputs:

tracker_theta axis_tilt axis_azimuth
30 0 0

Excel formula:

=SURFACE_ORIENT({30}, 0, 0)

Expected output:

Result
30 90
Example 2: 30 degree eastward rotation on N-S axis

Inputs:

tracker_theta axis_tilt axis_azimuth
-30 0 0

Excel formula:

=SURFACE_ORIENT({-30}, 0, 0)

Expected output:

Result
30 270
Example 3: Horizontal tracker orientation

Inputs:

tracker_theta axis_tilt axis_azimuth
0 0 180

Excel formula:

=SURFACE_ORIENT({0}, 0, 180)

Expected output:

Result
0 270
Example 4: Vectorized tracker rotation inputs

Inputs:

tracker_theta axis_tilt axis_azimuth
-20 0 20 0 180

Excel formula:

=SURFACE_ORIENT({-20,0,20}, 0, 180)

Expected output:

Result
20 90
0 270
20 270

Python Code

Show Code
import pandas as pd
import numpy as np
from pvlib.tracking import calc_surface_orientation as result_func

def surface_orient(tracker_theta, axis_tilt=0, axis_azimuth=0):
    """
    Calculate surface tilt and azimuth for a given tracker rotation.

    See: https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.tracking.calc_surface_orientation.html

    This example function is provided as-is without any representation of accuracy.

    Args:
        tracker_theta (list[list]): Tracker rotation angle (degrees).
        axis_tilt (float, optional): Tilt of the rotation axis (degrees). Default is 0.
        axis_azimuth (float, optional): Compass direction of the rotation axis (degrees). Default is 0.

    Returns:
        list[list]: 2D list [[surface_tilt, surface_azimuth]], or an error string.
    """
    try:
        def flatten_num(data):
            if not isinstance(data, list): return [float(data)]
            flat = []
            for row in data:
                row = row if isinstance(row, list) else [row]
                for val in row:
                    if val == "": flat.append(float('nan'))
                    else: flat.append(float(val))
            return flat

        theta_list = flatten_num(tracker_theta)
        if len(theta_list) == 0:
            return "Error: tracker_theta cannot be empty"

        at = float(axis_tilt) if axis_tilt is not None else 0.0
        aa = float(axis_azimuth) if axis_azimuth is not None else 0.0

        res = result_func(
            tracker_theta=np.array(theta_list),
            axis_tilt=at,
            axis_azimuth=aa
        )

        # res is a DataFrame or OrderedDict
        st = res['surface_tilt']
        sa = res['surface_azimuth']

        out = []
        for i in range(len(theta_list)):
            out.append([
                float(st[i]) if not pd.isna(st[i]) else "",
                float(sa[i]) if not pd.isna(sa[i]) else ""
            ])
        return out
    except Exception as e:
        return f"Error: {str(e)}"

Online Calculator

Tracker rotation angle (degrees).
Tilt of the rotation axis (degrees).
Compass direction of the rotation axis (degrees).