Skip to content

Implement ANTS-2D bifacial irradiance model#2740

Open
kandersolar wants to merge 52 commits into
pvlib:mainfrom
kandersolar:ants2d
Open

Implement ANTS-2D bifacial irradiance model#2740
kandersolar wants to merge 52 commits into
pvlib:mainfrom
kandersolar:ants2d

Conversation

@kandersolar
Copy link
Copy Markdown
Member

@kandersolar kandersolar commented Apr 21, 2026

  • [ ] Closes #xxxx
  • 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.

@AdamRJensen, @cwhanse, and I have a paper describing a new bifacial irradiance model called ANTS-2D. It is similar to pvlib's infinite_sheds model, but extended to allow:

  • a discretized module surface (for, e.g., cell-level irradiance values)
  • a discretized ground surface, with variable albedo
  • sloped terrain, in the fashion of pvlib's existing functionality in that area
  • the Perez transposition model, in addition to Hay-Davies and isotropic
  • computation of ground-level irradiance (for agriPV)
  • fast computation by using analytical integrated view factors instead of burdensome numerical integrals

Details available open-access here: https://doi.org/10.1109/JPHOTOV.2026.3677506

This PR is rather large. To summarize:

  • Add g0 and g1 parameters to the view factor functions in pvlib.bifacial.utils. These are analogous to x0 and x1 in vf_row_sky_2d_integ and extend the functions to subset the ground surface.
  • Change vf_ground_sky_2d_integ to use Hottel's crossed-string rule instead of burdensome numerical integration. This makes the npoints and vectorize parameters unnecessary.
  • Change some signatures in pvlib.bifacial.utils to be cleaner with the new calculations.
  • Minor edits in pvlib.bifacial.infinite_sheds to accommodate the utils changes
  • Create pvlib.bifacial.ant2d, which houses the model itself and uses the new utils functionality.

Let me know if it would help reviewers to split it up and review separate PRs, starting with utils.

@kandersolar kandersolar added this to the v0.15.2 milestone Apr 21, 2026
@kandersolar kandersolar marked this pull request as ready for review April 30, 2026 15:36
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.

Some first impressions; also, you may want to link to it in other bifacial "See also" sections.

Comment thread pvlib/bifacial/utils.py
def vf_ground_sky_2d_integ(surface_tilt, gcr, height, pitch, max_rows=10,
npoints=100, vectorize=False):
@renamed_kwarg_warning("0.15.2", "surface_tilt", "tracker_rotation")
def vf_ground_sky_2d_integ(tracker_rotation, gcr, height, pitch, g0=0, g1=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.

Suggested change
def vf_ground_sky_2d_integ(tracker_rotation, gcr, height, pitch, g0=0, g1=1,
def vf_ground_sky_2d_integ(tracker_rotation, gcr, height, pitch, *, g0=0, g1=1,

The rationale is that previous versions assume this call signature

vf_ground_sky_2d_integ(0, 0.5, 2, 4, 20, 200), where max_rows=20 and npoints=200

This wouldn't fail in the new version [1], where values g0 and g1 would take 20 and 200 respectively.

[1] At this branch

>>> import pvlib
>>> pvlib.bifacial.utils.vf_ground_sky_2d_integ(0, 0.5, 2, 4, 20, 200)
array(1.14662412e-05)

[2] On main

array([0.49984351])

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.

Making these keyword-only arguments would be breaking, which makes me reluctant to take it on here. @echedey-ls how about the alternative of moving g0 and g1 to the end of the signature so that existing usage is not affected?

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 should work, yes, assuming nobody suppresses the warnings by setting those old params to None. Cause whenever they get deleted, then that function call would fail. I can only think of an approach that allows code to never fail, but that requires a deprecation period for positional params max_rows and npoints prior to merging this PR. [1, 2]

Your suggestion seems to be the most agile, the use-case I propose to be an absolute edge-case, and I prefer to merge this PR in next minor release rather than to make it wait for two minor versions.

[1] Decorator for deprecating positional-allowed params https://github.com/pyvista/pyvista/blob/main/pyvista/_deprecate_positional_args.py
[2] Example usage of [1] https://github.com/pyvista/pyvista/blob/21dd07fb3444356c61aacaf83510ba71cebd9780/pyvista/plotting/helpers.py#L65

Comment thread pvlib/bifacial/utils.py
Comment on lines +514 to +515
def vf_row_ground_2d_integ(surface_tilt, gcr, height=None, pitch=None,
x0=0, x1=1, g0=0, g1=1, max_rows=20):
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'd say this one is also subject to the problem described in previous comment

Copy link
Copy Markdown
Member

@cwhanse cwhanse left a comment

Choose a reason for hiding this comment

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

I'm OK reviewing the whole PR, if we can do it in a sequence of smaller review bites.

Comment thread docs/sphinx/source/whatsnew/v0.15.2.rst
Comment thread pvlib/bifacial/utils.py Outdated
Comment thread pvlib/bifacial/utils.py Outdated
Comment thread pvlib/bifacial/utils.py Outdated
Comment thread pvlib/bifacial/utils.py Outdated
Comment thread pvlib/bifacial/utils.py Outdated
Comment thread pvlib/bifacial/utils.py Outdated
Comment thread pvlib/bifacial/utils.py Outdated
Comment thread pvlib/bifacial/utils.py Outdated
Comment on lines +115 to +117
# TODO seems like this should be np.arange(-max_rows, max_rows+1)?
# see GH #1867
k = np.arange(-max_rows, max_rows)[:, np.newaxis, np.newaxis]
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 do (-max_rows, max_rows + 1) or is this a breaking change needing deprecation?

Copy link
Copy Markdown
Member Author

@kandersolar kandersolar May 6, 2026

Choose a reason for hiding this comment

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

I went through and fixed all these to what I think makes sense. Which + constant is appropriate depends on the function. I also went ahead and fixed #1867, calling it a bugfix.

Edit: not sure what I've done is the right way forward. Will discuss in #1867.

Comment thread pvlib/bifacial/utils.py Outdated
kandersolar and others added 2 commits May 6, 2026 14:24
Co-Authored-By: Cliff Hansen <5393711+cwhanse@users.noreply.github.com>
Comment thread pvlib/bifacial/utils.py
surface_tilt : numeric
Surface tilt angle in degrees from horizontal, e.g., surface facing up
= 0, surface facing horizon = 90. [degree]
tracker_rotation : numeric
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'm not persuaded that tracker_rotation and surface_tilt are interchangeable. When axis_tilt is not 0 the rotation angle is not the same as the angle from horizontal. In this case, the 2D geometry being considered is in the plane perpendicular to the tracker rotation axis, not perpendicular to the ground. Does that matter?

Also, with tracker_rotation as the input, this public function relies on context outside the scope of the function: axis_tilt and axis_aximuth are needed to know what tracker_rotation is. If we need the input to be tracker_rotation then maybe consider making this function private.

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'm not persuaded that tracker_rotation and surface_tilt are interchangeable. When axis_tilt is not 0 the rotation angle is not the same as the angle from horizontal. In this case, the 2D geometry being considered is in the plane perpendicular to the tracker rotation axis, not perpendicular to the ground. Does that matter?

I think tracker_rotation is correct here:

  • Adding g0 and g1 (rather than implicitly hardcoding them as 0/1) means that we can't assume symmetry around the ground midpoint anymore. That means that we need to distinguish east- and west-facing rotations of the same tilt, so a signed quantity like tracker_rotation is needed.
  • The VF must be calculated from the ground's point of view. When axis_tilt is nonzero, that means the ground surface is tilted too (the model assumes that the tracking axis is parallel to the ground surface). In the reference frame aligned with the ground, tracker_rotation is the required surface_tilt analog.

Also, with tracker_rotation as the input, this public function relies on context outside the scope of the function: axis_tilt and axis_azimuth are needed to know what tracker_rotation is. If we need the input to be tracker_rotation then maybe consider making this function private.

Yeah, I guess it is a little awkward. Making the function private seems a shame, since it's useful in other contexts. Would noting the geometric link between tracker_rotation, g0, and g1 be a sufficient alternative? g0 and g1 are documented using left and right, so maybe something like:

    tracker_rotation : numeric
        Tracker rotation angle.  Positive rotations indicate raising the
        row's right edge.  [degree]

By the way, we are talking about vf_ground_sky_2d_integ, but the existing vf_ground_sky_2d signature has the same issue (tracker rotation input, but no axis tilt/azimuth): https://pvlib-python.readthedocs.io/en/stable/reference/generated/pvlib.bifacial.utils.vf_ground_sky_2d.html

Comment thread pvlib/bifacial/utils.py Outdated
Comment thread pvlib/bifacial/utils.py
-------
fgnd_sky : numeric
Integration of view factor over the length between adjacent, interior
rows. Shape matches that of ``surface_tilt``. [unitless]
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.

Replace surface_tilt or not, pending outcome of comment about tracker_rotation

Comment thread pvlib/bifacial/utils.py
kandersolar and others added 2 commits May 20, 2026 11:59
Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>
@williamhobbs
Copy link
Copy Markdown
Contributor

Is there a reason that pvlib.irradiance.perez_driesse is not an option? That would be useful to include if it's compatible.

Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>
@kandersolar
Copy link
Copy Markdown
Member Author

Is there a reason that pvlib.irradiance.perez_driesse is not an option? That would be useful to include if it's compatible.

The only reason is that I did not think to include it in the reference paper, so including it here would be going beyond the reference. If no reviewers object to that in this case, I'd be fine adding it in (and maybe documenting it as an extension).

@williamhobbs
Copy link
Copy Markdown
Contributor

I'd be fine adding it in (and maybe documenting it as an extension).

That sounds great to me!

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.

4 participants