Skip to content

calculate surface angles using unit normal#2702

Open
cwhanse wants to merge 7 commits intopvlib:mainfrom
cwhanse:aoi_3d
Open

calculate surface angles using unit normal#2702
cwhanse wants to merge 7 commits intopvlib:mainfrom
cwhanse:aoi_3d

Conversation

@cwhanse
Copy link
Copy Markdown
Member

@cwhanse cwhanse commented Feb 28, 2026

  • Closes Ambiguous descriptions of axis_azimuth and axis_tilt in pvlib.tracking.singleaxis() docs #1976
  • I am familiar with the contributing guidelines
  • I attest that all AI-generated material has been vetted for accuracy and is in compliance with the pvlib license
  • Tests added
  • Updates entries in docs/sphinx/source/reference for API changes.
  • Adds description and name entries in the appropriate "what's new" file in docs/sphinx/source/whatsnew for all changes. Includes link to the GitHub Issue with :issue:`num` or this Pull Request with :pull:`num`. Includes contributor name and/or GitHub username (link with :ghuser:`user`).
  • New code is fully documented. Includes numpydoc compliant docstrings, examples, and comments where necessary.
  • Pull request is nearly complete and ready for detailed review.
  • Maintainer: Appropriate GitHub Labels (including remote-data) and Milestone are assigned to the Pull Request and linked Issue.

Changes the calculation of surface angles for tracking.singleaxis to allow for negative axis_tilt.

Changes the output of singleaxis in the case of surface_tilt=0, to consistently return surface_azimuth=axis_azimuth.

Comment on lines +298 to +299
surface_azimuth = np.degrees(
np.arctan2(unit_normal[:, 0], unit_normal[:, 1]))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I would include this line in the function that calculates the unit normal so that it is self-contained w.r.t. its conventions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Are you suggesting that _unit_normal calculate and return surface_azimuth? I'm unclear what you are suggesting.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

That was the thought, yes. Angles in, angle out is independent of the method. I guess the function name would change too.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Thanks for the suggestion. I guess I'm preferring _unit_normal as the function because it may find other uses. We could package the few lines here into a helper _calc_surface_azimuth but I don't see that as much of an improvement.

Copy link
Copy Markdown
Member

@echedey-ls echedey-ls left a comment

Choose a reason for hiding this comment

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

I've compared _unit_normal with an implementation I did based on scipy rotation objects, and they match. The 3d axis basis is the same E-N-Up, left-hand rotations.

I just left this comment in the issue, I hope not to delay a fix on negative tilt handling.

Unit normal to rotated tracker surface, in global E-N-Up coordinates,
given by R*(0, 0, 1)^T, where:

R = Rz(-axis_azimuth) Rx(-axis_tilt) Ry(theta) *
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
R = Rz(-axis_azimuth) Rx(-axis_tilt) Ry(theta) *
R = Rz(-axis_azimuth) Rx(-axis_tilt) Ry(theta)

rotation by -axis_tilt about the x-axis, where axis_tilt is negated
because pvlib's convention is that the positive y-axis is tilted
downwards. Ry is a rotation by theta
about the y-axis. theta is negated so that a negative.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I think this sentence was not ended correctly, right?

example, for a tracker with ``axis_azimuth``=180 and ``axis_tilt``=10,
the north end is higher than the south end of the axis.

axis_azimuth : float, default 0
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

All the examples refer to azimuth 180, so why is the default zero?

Copy link
Copy Markdown
Member Author

@cwhanse cwhanse Apr 7, 2026

Choose a reason for hiding this comment

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

I don't think there's a particular reason for the default of 0. It's the least common orientation in the northern hemisphere but perhaps its most common in the southern hemisphere. 0 has been the default for a long time, so maybe inertia wins? Examples are different question, though.

@@ -84,7 +84,7 @@ def singleaxis(apparent_zenith, solar_azimuth,
intersection between the slope containing the tracker axes and a plane
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I haven't looked at these functions much before, and it took me a while to realize that "slope" usually refers to the terrain or ground itself, or in this case surface floating above the ground, rather than an attribute of a line or surface. I the documentation could be improved by changing "slope" to "sloped surface" or "sloped ground"or "sloped terrain" or similar depending on the context.

If this gets a few thumbs up I can make specific suggestions.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I agree that this description can be improved. "slope" is not specific. The precise definition should describe an intersection of two planes (because that's how a line is defined, and lines define angles). I think "slope" is intended to mean the plane that contains the tracker axes. @kandersolar

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Ambiguous descriptions of axis_azimuth and axis_tilt in pvlib.tracking.singleaxis() docs

3 participants