Skip to content

calc_surface_orientation broadcasting error for 2-D inputs in 0.15.1 (regression vs 0.15.0) #2747

@ArthurOnnoTerabase

Description

@ArthurOnnoTerabase

Describe the bug

pvlib.tracking.calc_surface_orientation regressed in 0.15.1: passing 2-D arrays for tracker_theta (and broadcasting-compatible 2-D axis_tilt / axis_azimuth, e.g. shape (timestamps, sites)) raises a broadcasting ValueError. The same call works in 0.15.0 and earlier.

The cause is the new _unit_normal helper introduced in #2702, which builds its return value with np.column_stack((x, y, z)). np.column_stack only stacks 1-D inputs into columns; for 2-D inputs of shape (T, N) it concatenates along axis=1, producing a (T, 3*N) array instead of the intended (T, N, 3). The subsequent unit_normal[:, 0] / unit_normal[:, 1] then collapse to shape (T,), and the downstream

python surface_azimuth = np.where(surface_tilt == 0., axis_azimuth - 90., surface_azimuth)

fails because the condition and true branch are still (T, N) while the false branch is now (T,).

To Reproduce

import numpy as np
import pvlib

T, N = 4655, 105
tracker_theta = np.zeros((T, N))
axis_tilt = np.zeros((T, N))
axis_azimuth = np.full((T, N), 180.0)

pvlib.tracking.calc_surface_orientation(tracker_theta, axis_tilt, axis_azimuth)
# pvlib 0.15.0: returns dict of (T, N) arrays — works
# pvlib 0.15.1: ValueError: operands could not be broadcast together with shapes (4655,105) (4655,105) (4655,)

Bisected against pip-installed releases on Python 3.11 / numpy 2.4.4 / pandas 3.0.2:

pvlib result
0.11.2 → 0.15.0 works
0.15.1 broken

Expected behavior

Return surface_tilt and surface_azimuth arrays with the same shape as the broadcast inputs (the 0.15.0 behavior).

Versions

  • pvlib.__version__: 0.15.1 (broken), 0.15.0 (works)
  • pandas.__version__: 3.0.2
  • numpy.__version__: 2.4.4
  • python: 3.11.9
  • platform: Windows

Additional context

Regression introduced in #2702 (milestone v0.15.1). Suggested one-line fix in
_unit_normal:

# before
return np.column_stack((x, y, z))
# after
return np.stack((x, y, z), axis=-1)

np.stack(..., axis=-1) preserves the leading dimensions for inputs of any rank and is equivalent to the old behavior for 1-D inputs, so it should be a drop-in replacement.

Hit in production while batch-processing tracker geometry across (timestamps × bays); pinned to pvlib<0.15.1 as a workaround.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions