Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
96697f1
Add cross_axis_tilt to nomenclature.rst
echedey-ls Jan 8, 2025
8eebd5f
Rename, cross_axis_slope -> cross_axis_tilt
echedey-ls Jan 8, 2025
0aa8836
Add renamed_kwarg_warning
echedey-ls Jan 8, 2025
01d5c0b
Fix typo in renamed_kwarg_warning docstring
echedey-ls Jan 8, 2025
e72ca32
Deprecate in v0.13.1 / Removal in v0.15.0
echedey-ls Sep 7, 2025
08bcde4
move descriptions, link to nomenclature term
echedey-ls Sep 7, 2025
6f9da5e
Merge branch 'main' into cross-axis-whatever-name-consistent-name
echedey-ls Sep 7, 2025
3ca225e
merge fix
echedey-ls Sep 7, 2025
c74127c
whatsnew
echedey-ls Sep 7, 2025
3006aa1
Dax review
echedey-ls Sep 20, 2025
2cebae7
Change: deprecate *axis_tilt in favour of *axis_slope
echedey-ls Sep 24, 2025
27b24e7
Typo in v0.13.1 whatnew
echedey-ls Sep 24, 2025
0b6336e
Merge branch 'main' into cross-axis-whatever-name-consistent-name
echedey-ls Sep 24, 2025
3625a5a
flake8 may forgive me
echedey-ls Sep 24, 2025
de94021
whatsnew improvements
echedey-ls Sep 24, 2025
716c880
Merge branch 'main' into cross-axis-whatever-name-consistent-name
echedey-ls Oct 1, 2025
7e52234
move whatsnew entries
echedey-ls Oct 1, 2025
d8467bc
rm whatnew duplicated from merge entry
echedey-ls Oct 1, 2025
50b1740
Merge branch 'main' into cross-axis-whatever-name-consistent-name
echedey-ls Nov 26, 2025
b84adc6
axis_azimuth description revision feat. cwhanse
echedey-ls Dec 8, 2025
25410b3
Move definition from pvsystem's SingleAxisTrackerMount to nomenclature
echedey-ls Dec 8, 2025
4c8a029
add axis_slope definition, as per figure in cross_axis_slope
echedey-ls Dec 8, 2025
c964df4
bump deprecation version from 0.13.1 to 0.13.2
echedey-ls Dec 8, 2025
24415fd
Move Deprecations to its section, instead of Breaking Changes
echedey-ls Dec 8, 2025
0350466
Description update of axis_azimuth
echedey-ls Dec 8, 2025
6de2087
Merge branch 'main' into cross-axis-whatever-name-consistent-name
echedey-ls Dec 8, 2025
3ed6172
picky linter
echedey-ls Dec 8, 2025
30a4513
Merge branch 'cross-axis-whatever-name-consistent-name' of https://gi…
echedey-ls Dec 8, 2025
5d0719a
nomenclature cross_axis_slope redaction
echedey-ls Dec 8, 2025
9ecf9e4
Cliff's fixing my confusion
echedey-ls Dec 9, 2025
e96e340
Bump deprecation version to 0.14.0 (from 0.13.2)
echedey-ls Dec 16, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/examples/shading/plot_martinez_shade_loss.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@
shading_row_rotation=tracker_theta,
collector_width=width,
pitch=pitch,
cross_axis_slope=cross_axis_tilt,
cross_axis_tilt=cross_axis_tilt,
)

# %%
Expand Down
12 changes: 6 additions & 6 deletions docs/examples/shading/plot_shaded_fraction1d_ns_hsat_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
collector_width = 3.2 # m
pitch = 4.15 # m
gcr = collector_width / pitch
cross_axis_slope = -5 # degrees
cross_axis_tilt = -5 # degrees
surface_to_axis_offset = 0.07 # m

# Generate a time range for the simulation
Expand All @@ -76,7 +76,7 @@
max_angle=(-50, 50), # (min, max) degrees
backtrack=False,
gcr=gcr,
cross_axis_tilt=cross_axis_slope,
cross_axis_tilt=cross_axis_tilt,
)["tracker_theta"]

# %%
Expand Down Expand Up @@ -112,7 +112,7 @@
collector_width=collector_width,
pitch=pitch,
surface_to_axis_offset=surface_to_axis_offset,
cross_axis_slope=cross_axis_slope,
cross_axis_tilt=cross_axis_tilt,
shading_row_rotation=rotation_angle,
),
)
Expand All @@ -130,7 +130,7 @@
collector_width=collector_width,
pitch=pitch,
surface_to_axis_offset=surface_to_axis_offset,
cross_axis_slope=cross_axis_slope,
cross_axis_tilt=cross_axis_tilt,
shading_row_rotation=rotation_angle,
),
# shaded fraction in the evening
Expand All @@ -143,7 +143,7 @@
collector_width=collector_width,
pitch=pitch,
surface_to_axis_offset=surface_to_axis_offset,
cross_axis_slope=cross_axis_slope,
cross_axis_tilt=cross_axis_tilt,
shading_row_rotation=rotation_angle,
),
)
Expand All @@ -161,7 +161,7 @@
collector_width=collector_width,
pitch=pitch,
surface_to_axis_offset=surface_to_axis_offset,
cross_axis_slope=cross_axis_slope,
cross_axis_tilt=cross_axis_tilt,
shading_row_rotation=rotation_angle,
),
0, # no shaded fraction in the evening
Expand Down
14 changes: 14 additions & 0 deletions docs/sphinx/source/user_guide/extras/nomenclature.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,20 @@ There is a convention on consistent variable names throughout the library:
bhi
Beam/direct horizontal irradiance

cross_axis_tilt
Cross-axis tilt angle [°].
Consider two parallel rows of modules at different height;
``cross_axis_tilt`` is the angle formed by the line formed by the
intersection between the slope containing the tracker axes and a plane
perpendicular to the tracker axes, and the horizontal plane.
Cross-axis tilt is measured by 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 tilt if the tracker axes plane
slopes up to the east.
Use :func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate
``cross_axis_tilt``

dhi
Diffuse horizontal irradiance

Expand Down
1 change: 1 addition & 0 deletions docs/sphinx/source/whatsnew/v0.13.1.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ Deprecations
* Deprecate :py:func:`~pvlib.modelchain.get_orientation`. (:pull:`2495`)
* Rename parameter name ``aparent_azimuth`` to ``solar_azimuth`` in :py:func:`~pvlib.tracking.singleaxis`.
(:issue:`2479`, :pull:`2480`)
* Rename parameter ``cross_axis_slope`` to ``cross_axis_tilt`` in :py:func:`pvlib.shading.shaded_fraction1d`. (:issue:`2334`, :pull:`2543`)

Bug fixes
~~~~~~~~~
Expand Down
2 changes: 1 addition & 1 deletion pvlib/_deprecation.py
Original file line number Diff line number Diff line change
Expand Up @@ -334,7 +334,7 @@ def renamed_kwarg_warning(since, old_param_name, new_param_name, removal=""):
Not compatible with positional-only arguments.

.. note::
Documentation for the function may updated to reflect the new parameter
Affected function docstring may be updated to reflect the new parameter
name; it is suggested to add a |.. versionchanged::| directive.

Parameters
Expand Down
32 changes: 20 additions & 12 deletions pvlib/shading.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
import pandas as pd
from pvlib.tools import sind, cosd

from pvlib._deprecation import renamed_kwarg_warning


def ground_angle(surface_tilt, gcr, slant_height):
"""
Expand Down Expand Up @@ -344,6 +346,12 @@ def projected_solar_zenith_angle(solar_zenith, solar_azimuth,
return theta_T


@renamed_kwarg_warning(
since="0.13.1",
old_param_name="cross_axis_slope",
new_param_name="cross_axis_tilt",
removal="0.15.0",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
removal="0.15.0",

We usually don't specify a removal version as there's not standard timeline for when minor versions come out. If we really wanted to specify a removal, I think this should be in the form of a date, e.g., removal="Earliest September 2026".

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I never thought of that, but looks a promising idea

Copy link
Member

@AdamRJensen AdamRJensen Sep 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm interested in what @wholmgren thinks of this?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I missed this for a few weeks. I don't think timeline is particularly relevant so long as we are using SemVer (or something close to it). I personally think we should specify removal versions, they should default to the 0.(m+2).0 release, and we should use our existing test machinery to enforce that. If we had several minor releases in a very short period of time (a few weeks) then I'd be ok pushing out the removal to a future release.

)
def shaded_fraction1d(
solar_zenith,
solar_azimuth,
Expand All @@ -354,7 +362,7 @@ def shaded_fraction1d(
pitch,
axis_tilt=0,
surface_to_axis_offset=0,
cross_axis_slope=0,
cross_axis_tilt=0,
shading_row_rotation=None,
):
r"""
Expand Down Expand Up @@ -397,10 +405,10 @@ def shaded_fraction1d(
surface_to_axis_offset : numeric, default 0
Distance between the rotating axis and the collector surface.
May be used to account for a torque tube offset.
cross_axis_slope : numeric, default 0
Angle of the plane containing the rows' axes from
cross_axis_tilt : numeric, default 0
Angle of the plane containing the rows' axes relative to
horizontal. Right-handed rotation with respect to the rows axes.
In degrees :math:`^{\circ}`.
See :term:`cross_axis_tilt`. In degrees :math:`^{\circ}`.
shading_row_rotation : numeric, optional
Right-handed rotation of the row casting the shadow, with respect
to the row axis. In degrees :math:`^{\circ}`.
Expand Down Expand Up @@ -430,7 +438,7 @@ def shaded_fraction1d(
+------------------+----------------------------+ |
| :math:`\theta_2` | ``shaded_row_rotation`` | Degrees |
+------------------+----------------------------+ :math:`^{\circ}` |
| :math:`\beta_c` | ``cross_axis_slope`` | |
| :math:`\beta_c` | ``cross_axis_tilt`` | |
+------------------+----------------------------+---------------------+
| :math:`p` | ``pitch`` | Any consistent |
+------------------+----------------------------+ length unit across |
Expand All @@ -452,7 +460,7 @@ def shaded_fraction1d(
>>> shaded_fraction1d(solar_zenith=80, solar_azimuth=135,
... axis_azimuth=90, shaded_row_rotation=30, shading_row_rotation=30,
... collector_width=2, pitch=3, axis_tilt=0,
... surface_to_axis_offset=0.05, cross_axis_slope=0)
... surface_to_axis_offset=0.05, cross_axis_tilt=0)
0.47755694708090535

**Fixed-tilt north-facing array on sloped terrain**
Expand All @@ -466,7 +474,7 @@ def shaded_fraction1d(
>>> shaded_fraction1d(solar_zenith=80, solar_azimuth=75.5,
... axis_azimuth=270, shaded_row_rotation=50, shading_row_rotation=30,
... collector_width=2.5, pitch=4, axis_tilt=10,
... surface_to_axis_offset=0.05, cross_axis_slope=0)
... surface_to_axis_offset=0.05, cross_axis_tilt=0)
0.793244836197256

**N-S single-axis tracker on sloped terrain**
Expand All @@ -478,7 +486,7 @@ def shaded_fraction1d(

>>> shaded_fraction1d(solar_zenith=80, solar_azimuth=90, axis_azimuth=180,
... shaded_row_rotation=-30, collector_width=1.4, pitch=3, axis_tilt=0,
... surface_to_axis_offset=0.10, cross_axis_slope=7)
... surface_to_axis_offset=0.10, cross_axis_tilt=7)
0.8242176864434579

Note the previous example only is valid for the shaded fraction of the
Expand All @@ -493,7 +501,7 @@ def shaded_fraction1d(

>>> shaded_fraction1d(solar_zenith=80, solar_azimuth=270, axis_azimuth=180,
... shaded_row_rotation=30, collector_width=1.4, pitch=3, axis_tilt=0,
... surface_to_axis_offset=0.10, cross_axis_slope=7)
... surface_to_axis_offset=0.10, cross_axis_tilt=7)
0.018002567182254348

You must switch the input/output depending on the
Expand Down Expand Up @@ -528,7 +536,7 @@ def shaded_fraction1d(
# calculate repeated elements
thetas_1_S_diff = shading_row_rotation - projected_solar_zenith
thetas_2_S_diff = shaded_row_rotation - projected_solar_zenith
thetaS_rotation_diff = projected_solar_zenith - cross_axis_slope
thetaS_rotation_diff = projected_solar_zenith - cross_axis_tilt

cos_theta_2_S_diff_abs = np.abs(cosd(thetas_2_S_diff))

Expand All @@ -548,7 +556,7 @@ def shaded_fraction1d(
/ collector_width
* cosd(thetaS_rotation_diff)
/ cos_theta_2_S_diff_abs
/ cosd(cross_axis_slope)
/ cosd(cross_axis_tilt)
)
)

Expand Down Expand Up @@ -660,7 +668,7 @@ def direct_martinez(
>>> solar_zenith=80, solar_azimuth=180,
>>> axis_azimuth=90, shaded_row_rotation=25,
>>> collector_width=0.5, pitch=1, surface_to_axis_offset=0,
>>> cross_axis_slope=5.711, shading_row_rotation=50)
>>> cross_axis_tilt=5.711, shading_row_rotation=50)
>>> # calculation of the number of shaded blocks
>>> shaded_blocks = np.ceil(total_blocks*shaded_fraction)
>>> # apply the Martinez power losses to the calculated shading
Expand Down
14 changes: 4 additions & 10 deletions pvlib/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
def singleaxis(apparent_zenith, solar_azimuth,
axis_tilt=0, axis_azimuth=0, max_angle=90,
backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0):
"""
r"""
Determine the rotation angle of a single-axis tracker when given particular
solar zenith and azimuth angles.

Expand Down Expand Up @@ -80,15 +80,9 @@ def singleaxis(apparent_zenith, solar_azimuth,
2/7 is default. ``gcr`` must be <=1.

cross_axis_tilt : float, default 0.0
The angle, relative to horizontal, of the line formed by the
intersection between the slope containing the tracker axes and a plane
perpendicular to the tracker axes. The cross-axis tilt 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 tilt if the tracker axes plane slopes down to the
west. Use :func:`~pvlib.tracking.calc_cross_axis_tilt` to calculate
``cross_axis_tilt``. [degrees]
Angle of the plane containing the rows' axes relative to
Copy link
Member

@cwhanse cwhanse Dec 3, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is correct. It describes an angle "relative to horizontal", i.e., an angle in the earth-centered reference frame.

cross_axis_slope is $\beta_c$ in Figure 4 of [1], which is in the tracker-centered frame. In the extreme case that axis_slope=90, cross_axis_slope is an angle within the horizontal (earth-centered) plane. @kandersolar is this correct?

The old definition of cross_axis_tilt has a similar problem.

I think cross_axis_slope has to somehow be stated relative to the tracker coordinates, i.e., the angle formed by a line perpendicular to two tracker rotation axes, and the tracker x-axis.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@cwhanse is correct; cross_axis_slope is defined in the (possibly tilted) tracker reference frame.

Copy link
Member Author

@echedey-ls echedey-ls Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm including that figure in the nomenclature page. I feel unable to write a reasonably good definition.

Copy link
Member Author

@echedey-ls echedey-ls Dec 8, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Description in SingleAxisTrackerMount was good I think, I copy-pasted it into the nomenclature. I also added the image, I believe it helps pretty much.

horizontal. Right-handed rotation with respect to the rows axes.
See :term:`cross_axis_tilt`. In degrees :math:`^{\circ}`.

Returns
-------
Expand Down
23 changes: 21 additions & 2 deletions tests/test_shading.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
from pvlib import shading
from pvlib.tools import atand

from .conftest import fail_on_pvlib_version
from pvlib._deprecation import pvlibDeprecationWarning


@pytest.fixture
def test_system():
Expand Down Expand Up @@ -258,7 +261,7 @@ def sf1d_premises_and_expected():
),
) # fmt: skip

test_data["cross_axis_slope"] = atand(
test_data["cross_axis_tilt"] = atand(
(test_data["z_R"] - test_data["z_L"])
/ (test_data["x_L"] - test_data["x_R"])
)
Expand Down Expand Up @@ -314,7 +317,7 @@ def test_shaded_fraction1d_unprovided_shading_row_rotation():
test_data = pd.DataFrame(
columns=[
"shaded_row_rotation", "surface_to_axis_offset", "collector_width",
"solar_zenith", "cross_axis_slope", "pitch", "solar_azimuth",
"solar_zenith", "cross_axis_tilt", "pitch", "solar_azimuth",
"axis_azimuth", "expected_sf",
],
data=[
Expand All @@ -329,6 +332,22 @@ def test_shaded_fraction1d_unprovided_shading_row_rotation():
assert_allclose(sf, expected_sf, atol=1e-2)


@fail_on_pvlib_version("0.15.0")
def test_shaded_fraction1d_renamed_cross_axis_slope2cross_axis_tilt():
# Tests shaded_fraction1d with cross_axis_slope instead of cross_axis_tilt
with pytest.warns(pvlibDeprecationWarning, match="cross_axis_slope"):
shading.shaded_fraction1d(
solar_zenith=60,
solar_azimuth=90,
axis_azimuth=180,
shaded_row_rotation=30,
collector_width=3,
pitch=7,
surface_to_axis_offset=0,
cross_axis_slope=0,
)


@pytest.fixture
def direct_martinez_Table2():
"""
Expand Down
Loading