Skip to content

Implement ANTS-2D bifacial irradiance model#2740

Open
kandersolar wants to merge 48 commits intopvlib:mainfrom
kandersolar:ants2d
Open

Implement ANTS-2D bifacial irradiance model#2740
kandersolar wants to merge 48 commits intopvlib: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 on lines +53 to +54
Projected solar zenith angle, defined around the same axis as
``tracker_rotation``. [degree].
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 don't understand this definition. A vector can be projected onto a plane or another vector. An angle is formed by two vectors. "around" suggests an orientation of a vector.

tan_phi was defined in the docstrings for _solar_project_tangent as

Tangent of the angle between the zenith vector and the sun vector
projected to the plane defined by the zenith vector and surface_azimuth.

So I would expect phi to be the angle of the sun vector projected to a plane formed by....

Since phi isn't clear, tracker_rotation becomes unclear also.

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 edited to Projected solar zenith angle, defined as a right-handed rotation around the same axis as ``tracker_rotation`` , paralleling the description of tracker_rotation. I would not object to taking the approach of the original tan_phi though.

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.

The angle phi is a rotation? Or is the projection of vectors that form phi somehow a rotation? I'm still not seeing this clearly.

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.

How about

phi : numeric
        Projected solar zenith angle, defined as a right-handed rotation on
        the plane perpendicular to the rotation axis, with respect to horizontal. [degree].

?

Both phi and tracker_rotation are currently defined based one on another, which may make it extra difficult to see. I would add rotation axis to the definition of tracker_rotation too. And clarify later (notes section?, the same params definitions?, IDK) to interpret fixed-tilt as an specific case of SAT.

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

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.

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>
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.

3 participants