Skip to content

Implement ANTS-2D bifacial irradiance model#2740

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

Implement ANTS-2D bifacial irradiance model#2740
kandersolar wants to merge 46 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])

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.

* Add parameters ``g0`` and ``g1`` to allow segmented ground surfaces in
:py:func:`~pvlib.bifacial.utils.vf_ground_sky_2d_integ` and
:py:func:`~pvlib.bifacial.utils.vf_row_ground_2d_integ`. (:pull:`2740`)
* Accelerate :py:func:`~pvlib.bifacial.utils.vf_ground_sky_2d_integ` by one or
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 a comment here about "this makes bifacial.infinite_sheds faster would be appreciated.

Comment thread pvlib/bifacial/utils.py
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.

Comment thread pvlib/bifacial/utils.py
spacing (pitch). [unitless]
max_zenith : numeric, default 87
height : float, optional
Height of the center point of the row above the ground; must be in the
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.

Center point means the center of the row slant length? If that's correct I'd make it

    Height above ground of the center point of the row slant length; must be in the

Comment thread pvlib/bifacial/utils.py
Distance between two rows; must be in the same units as ``height``.
Required if ``g0`` is not zero or ``g1`` is not one.
g0 : numeric, default 0
Position on the ground surface, as a fraction of the row-to-row
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
Position on the ground surface, as a fraction of the row-to-row
Left position on the ground surface, as a fraction of the row-to-row

g0 is the left position on the ground, I think.

Comment thread pvlib/bifacial/utils.py
spacing. ``g0=0`` corresponds to ground underneath the middle of the
left row. ``g0`` should be less than ``g1``. [unitless]
g1 : numeric, default 1
Position on the ground surface, as a fraction of the row-to-row
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
Position on the ground surface, as a fraction of the row-to-row
Right position on the ground surface, as a fraction of the row-to-row

Comment thread pvlib/bifacial/utils.py
Required if ``g0`` is not zero or ``g1`` is not one.
g0 : numeric, default 0
Position on the ground surface, as a fraction of the row-to-row
spacing. ``g0=0`` corresponds to ground underneath the middle of the
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
spacing. ``g0=0`` corresponds to ground underneath the middle of the
spacing. ``g0=0`` corresponds to ground underneath the center of the

Comment thread pvlib/bifacial/utils.py
left row. ``g0`` should be less than ``g1``. [unitless]
g1 : numeric, default 1
Position on the ground surface, as a fraction of the row-to-row
spacing. ``g1=1`` corresponds to ground underneath the middle of the
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
spacing. ``g1=1`` corresponds to ground underneath the middle of the
spacing. ``g1=1`` corresponds to ground underneath the center of the

Comment thread pvlib/bifacial/utils.py
Comment on lines +74 to +75
Maximum number of rows to consider on either side of the current
row. [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.

Suggested change
Maximum number of rows to consider on either side of the current
row. [unitless]
Maximum number of rows to consider on either side of the left and right
rows. [unitless]

Related to the [max_rows, max_rows+1] comment below.

Comment thread pvlib/bifacial/utils.py
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?

Comment thread pvlib/bifacial/utils.py
same units as ``pitch``. Required if ``g0`` is not zero or ``g1`` is
not one.
pitch : float, optional
Distance between two rows; must be in the same units as ``height``.
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
Distance between two rows; must be in the same units as ``height``.
Distance between centers of two rows; must be in the same units as ``height``.

"centers" is arbritrary. I think we want to avoid the confusion that the distance is the gap between the right edge of left row, and the left edge of right row.

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