Repository: pvlib/pvlib-python (python)

Issue:
`invalid value` warning when calculating diffuse fraction in `infinite_sheds`
When supplying numpy arrays for `ghi` and `dhi` that have zeros (e.g. for nighttime), a warning is emitted:

```python
import pvlib
import pandas as pd

times = pd.date_range("2019-06-01", freq="1min", periods=1440, tz="Etc/GMT+5")
location = pvlib.location.Location(40, -80)

sp = location.get_solarposition(times)
cs = location.get_clearsky(times)

ret = pvlib.bifacial.infinite_sheds.get_irradiance(
    surface_tilt=20,
    surface_azimuth=180,
    solar_zenith=sp['zenith'],
    solar_azimuth=sp['azimuth'],
    gcr=0.5,
    height=2.0,
    pitch=0.5,
    ghi=cs['ghi'].values,  # arrays
    dhi=cs['dhi'].values,
    dni=cs['dni'].values,
    albedo=0.2,
)
```

```
\lib\site-packages\pvlib\bifacial\infinite_sheds.py:345: RuntimeWarning: invalid value encountered in divide
  diffuse_fraction = np.clip(dhi / ghi, 0., 1.)
```

We could simply suppress the warning, but I think a better solution is to do away with the calculation entirely. This diffuse fraction is used only in the `_poa_ground_shadows` function, which computes the average irradiance reflected from the ground, accounting for the shadows cast by rows and the restricted ground->sky view factor:

https://github.com/pvlib/pvlib-python/blob/216386ef2f2fcdcaf1b3ecb5913abdf54e9eabaf/pvlib/bifacial/infinite_sheds.py#L38

where `df` is the diffuse fraction and `poa_ground` is `ghi * albedo`.  The math is equivalent to the following:

```python
albedo * (f_gnd_beam * (ghi - dhi) + vf_gnd_sky * dhi)
```

This alternative means we don't need to calculate the diffuse fraction at all, and (to me) is an improvement in clarity as well.  I propose dropping `diffuse_fraction` and using the above calculation instead.  Note that the line in question is in a private function, so it is no issue to change the relevant function signature.

Pull request description:
Remove `diffuse_fraction` calculation from `infinite_sheds`
 - [x] Closes #2450
 - [x] I am familiar with the [contributing guidelines](https://pvlib-python.readthedocs.io/en/latest/contributing/index.html)
 - [x] Tests added
 - ~[ ] Updates entries in [`docs/sphinx/source/reference`](https://github.com/pvlib/pvlib-python/blob/main/docs/sphinx/source/reference) for API changes.~
 - [x] Adds description and name entries in the appropriate "what's new" file in [`docs/sphinx/source/whatsnew`](https://github.com/pvlib/pvlib-python/tree/main/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` ``).
 - [x] New code is fully documented. Includes [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) compliant docstrings, examples, and comments where necessary.
 - [x] Pull request is nearly complete and ready for detailed review.
 - [x] Maintainer: Appropriate GitHub Labels (including `remote-data`) and Milestone are assigned to the Pull Request and linked Issue.




Patch:
diff --git a/docs/sphinx/source/whatsnew/v0.12.1.rst b/docs/sphinx/source/whatsnew/v0.12.1.rst
index 88cdfc6..37c458c 100644
--- a/docs/sphinx/source/whatsnew/v0.12.1.rst
+++ b/docs/sphinx/source/whatsnew/v0.12.1.rst
@@ -30,6 +30,8 @@ Enhancements
   :py:func:`~pvlib.iotools.get_nsrdb_psm4_full_disc`,
   :py:func:`~pvlib.iotools.read_nsrdb_psm4`, and
   :py:func:`~pvlib.iotools.parse_nsrdb_psm4`. (:issue:`2326`, :pull:`2378`)
+* :py:mod:`pvlib.bifacial.infinite_sheds` no longer emits "invalid value" warnings
+  when supplying irradiance arrays with nighttime zero values. (:issue:`2450`, :pull:`2451`)
 
 Documentation
 ~~~~~~~~~~~~~
diff --git a/pvlib/bifacial/infinite_sheds.py b/pvlib/bifacial/infinite_sheds.py
index c1a5d91..2418fd7 100644
--- a/pvlib/bifacial/infinite_sheds.py
+++ b/pvlib/bifacial/infinite_sheds.py
@@ -9,21 +9,23 @@ from pvlib.bifacial import utils
 from pvlib.irradiance import beam_component, aoi, haydavies
 
 
-def _poa_ground_shadows(poa_ground, f_gnd_beam, df, vf_gnd_sky):
+def _poa_ground_shadows(ghi, dhi, albedo, f_gnd_beam, vf_gnd_sky):
     """
     Reduce ground-reflected irradiance to the tilted plane (poa_ground) to
     account for shadows on the ground.
 
     Parameters
     ----------
-    poa_ground : numeric
-        Ground reflected irradiance on the tilted surface, assuming full GHI
-        illumination on all of the ground. [W/m^2]
+    ghi : numeric
+        Global horizontal irradiance, with no adjustments. [W/m^2]
+    dhi : numeric
+        Diffuse horizontal irradiance, with no adjustments. [W/m^2]
+    albedo : numeric
+        Ground albedo, the ratio of reflected to incident irradiance of the
+        ground surface. [W/m^2]
     f_gnd_beam : numeric
         Fraction of the distance between rows that is illuminated (unshaded).
         [unitless]
-    df : numeric
-        Diffuse fraction, the ratio of DHI to GHI. [unitless]
     vf_gnd_sky : numeric
         View factor from the ground to the sky, integrated along the distance
         between rows. [unitless]
@@ -35,7 +37,7 @@ def _poa_ground_shadows(poa_ground, f_gnd_beam, df, vf_gnd_sky):
         ground. [W/m^2]
 
     """
-    return poa_ground * (f_gnd_beam*(1 - df) + df*vf_gnd_sky)
+    return albedo * (f_gnd_beam * (ghi - dhi) + vf_gnd_sky * dhi)
 
 
 def _poa_sky_diffuse_pv(dhi, gcr, surface_tilt):
@@ -339,18 +341,11 @@ def get_irradiance_poa(surface_tilt, surface_azimuth, solar_zenith,
     # and restricted views
     # this is a deviation from [1], because the row to ground view factor
     # is accounted for in a different manner
-    ground_diffuse = ghi * albedo
-
-    # diffuse fraction
-    diffuse_fraction = np.clip(dhi / ghi, 0., 1.)
-    # make diffuse fraction 0 when ghi is small
-    diffuse_fraction = np.where(ghi < 0.0001, 0., diffuse_fraction)
-
     # Reduce ground-reflected irradiance because other rows in the array
     # block irradiance from reaching the ground.
     # [2], Eq. 9
-    ground_diffuse = _poa_ground_shadows(
-        ground_diffuse, f_gnd_beam, diffuse_fraction, vf_gnd_sky)
+    ground_diffuse = _poa_ground_shadows(ghi, dhi, albedo, f_gnd_beam,
+                                         vf_gnd_sky)
 
     # Ground-reflected irradiance on the row surface accounting for
     # the view to the ground. This deviates from [1], Eq. 10, 11 and


Test patch:
diff --git a/tests/bifacial/test_infinite_sheds.py b/tests/bifacial/test_infinite_sheds.py
index b689b41..5459b9d 100644
--- a/tests/bifacial/test_infinite_sheds.py
+++ b/tests/bifacial/test_infinite_sheds.py
@@ -42,19 +42,20 @@ def test_system():
 
 
 def test__poa_ground_shadows():
-    poa_ground, f_gnd_beam, df, vf_gnd_sky = (300., 0.5, 0.5, 0.2)
-    result = infinite_sheds._poa_ground_shadows(
-        poa_ground, f_gnd_beam, df, vf_gnd_sky)
-    expected = 300. * (0.5 * 0.5 + 0.5 * 0.2)
+    ghi, dhi, albedo, f_gnd_beam, vf_gnd_sky = (300., 100, 0.3, 0.5, 0.2)
+    result = infinite_sheds._poa_ground_shadows(ghi, dhi, albedo, f_gnd_beam,
+                                                vf_gnd_sky)
+
+    expected = 0.3 * (200 * 0.5 + 100 * 0.2)
     assert np.isclose(result, expected)
     # vector inputs
-    poa_ground = np.array([300., 300.])
-    f_gnd_beam = np.array([0.5, 0.5])
-    df = np.array([0.5, 0.])
-    vf_gnd_sky = np.array([0.2, 0.2])
-    result = infinite_sheds._poa_ground_shadows(
-        poa_ground, f_gnd_beam, df, vf_gnd_sky)
-    expected_vec = np.array([expected, 300. * 0.5])
+    ghi = np.array([ghi, ghi])
+    dhi = np.array([dhi, 0])
+    f_gnd_beam = np.array([f_gnd_beam, f_gnd_beam])
+    vf_gnd_sky = np.array([vf_gnd_sky, vf_gnd_sky])
+    result = infinite_sheds._poa_ground_shadows(ghi, dhi, albedo, f_gnd_beam,
+                                                vf_gnd_sky)
+    expected_vec = np.array([expected, 300. * 0.5 * 0.3])
     assert np.allclose(result, expected_vec)