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

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions