Repository: pvlib/pvlib-python
Pull request #1351 (2021-12-08 15:07:45)

Organize example gallery into sections
 - ~Closes #xxxx~
 - [x] I am familiar with the [contributing guidelines](https://pvlib-python.readthedocs.io/en/latest/contributing.html)
 - ~Tests added~
 - ~Updates entries to [`docs/sphinx/source/api.rst`](https://github.com/pvlib/pvlib-python/blob/master/docs/sphinx/source/api.rst) for API changes.~
 - ~Adds description and name entries in the appropriate "what's new" file in [`docs/sphinx/source/whatsnew`](https://github.com/pvlib/pvlib-python/tree/master/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](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 and Milestone are assigned to the Pull Request and linked Issue.

Following up on this discussion: https://github.com/pvlib/pvlib-python/discussions/1279

This PR moves the gallery `.py` files into subdirectories so that they appear in sections on the rendered gallery page.  I haven't done it here, but it is possible to specify the ordering of the sections ([sphinx-gallery docs](https://sphinx-gallery.github.io/stable/configuration.html#sorting-gallery-subsections)).  Right now some sections only have one example; I hope that the gallery continues to grow and the sections will flesh out over time. 

PR build here: https://pvlib-python--1351.org.readthedocs.build/en/1351/gallery/index.html


Patch:
diff --git a/azure-pipelines.yml b/azure-pipelines.yml
index d4e7841..dc048dd 100644
--- a/azure-pipelines.yml
+++ b/azure-pipelines.yml
@@ -15,7 +15,7 @@ jobs:
 - template: ci/azure/posix.yml
   parameters:
     name: Test_bare_macOS
-    vmImage: macOS-10.14
+    vmImage: macOS-10.15
 
 
 - template: ci/azure/conda_linux.yml
@@ -27,7 +27,7 @@ jobs:
 - template: ci/azure/conda_windows.yml
   parameters:
     name: Test_conda_windows
-    vmImage: vs2017-win2016
+    vmImage: windows-latest
 
 
 - job: 'Publish'
diff --git a/docs/examples/irradiance-transposition/README.rst b/docs/examples/irradiance-transposition/README.rst
new file mode 100644
index 0000000..a1939ed
--- /dev/null
+++ b/docs/examples/irradiance-transposition/README.rst
@@ -0,0 +1,3 @@
+Irradiance Transposition
+------------------------
+
diff --git a/docs/examples/plot_ghi_transposition.py b/docs/examples/irradiance-transposition/plot_ghi_transposition.py
similarity index 100%
rename from docs/examples/plot_ghi_transposition.py
rename to docs/examples/irradiance-transposition/plot_ghi_transposition.py
diff --git a/docs/examples/plot_interval_transposition_error.py b/docs/examples/irradiance-transposition/plot_interval_transposition_error.py
similarity index 100%
rename from docs/examples/plot_interval_transposition_error.py
rename to docs/examples/irradiance-transposition/plot_interval_transposition_error.py
diff --git a/docs/examples/plot_mixed_orientation.py b/docs/examples/irradiance-transposition/plot_mixed_orientation.py
similarity index 100%
rename from docs/examples/plot_mixed_orientation.py
rename to docs/examples/irradiance-transposition/plot_mixed_orientation.py
diff --git a/docs/examples/plot_seasonal_tilt.py b/docs/examples/irradiance-transposition/plot_seasonal_tilt.py
similarity index 100%
rename from docs/examples/plot_seasonal_tilt.py
rename to docs/examples/irradiance-transposition/plot_seasonal_tilt.py
diff --git a/docs/examples/plot_transposition_gain.py b/docs/examples/irradiance-transposition/plot_transposition_gain.py
similarity index 100%
rename from docs/examples/plot_transposition_gain.py
rename to docs/examples/irradiance-transposition/plot_transposition_gain.py
diff --git a/docs/examples/iv-modeling/README.rst b/docs/examples/iv-modeling/README.rst
new file mode 100644
index 0000000..05f7d72
--- /dev/null
+++ b/docs/examples/iv-modeling/README.rst
@@ -0,0 +1,3 @@
+I-V Modeling
+------------
+
diff --git a/docs/examples/plot_singlediode.py b/docs/examples/iv-modeling/plot_singlediode.py
similarity index 100%
rename from docs/examples/plot_singlediode.py
rename to docs/examples/iv-modeling/plot_singlediode.py
diff --git a/docs/examples/reflections/README.rst b/docs/examples/reflections/README.rst
new file mode 100644
index 0000000..30b16e4
--- /dev/null
+++ b/docs/examples/reflections/README.rst
@@ -0,0 +1,3 @@
+Reflections
+-----------
+
diff --git a/docs/examples/plot_diffuse_aoi_correction.py b/docs/examples/reflections/plot_diffuse_aoi_correction.py
similarity index 100%
rename from docs/examples/plot_diffuse_aoi_correction.py
rename to docs/examples/reflections/plot_diffuse_aoi_correction.py
diff --git a/docs/examples/shading/README.rst b/docs/examples/shading/README.rst
new file mode 100644
index 0000000..f13da4f
--- /dev/null
+++ b/docs/examples/shading/README.rst
@@ -0,0 +1,2 @@
+Shading
+-------
diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/shading/plot_partial_module_shading_simple.py
similarity index 100%
rename from docs/examples/plot_partial_module_shading_simple.py
rename to docs/examples/shading/plot_partial_module_shading_simple.py
diff --git a/docs/examples/plot_passias_diffuse_shading.py b/docs/examples/shading/plot_passias_diffuse_shading.py
similarity index 100%
rename from docs/examples/plot_passias_diffuse_shading.py
rename to docs/examples/shading/plot_passias_diffuse_shading.py
diff --git a/docs/examples/soiling/README.rst b/docs/examples/soiling/README.rst
new file mode 100644
index 0000000..4df1dbb
--- /dev/null
+++ b/docs/examples/soiling/README.rst
@@ -0,0 +1,2 @@
+Soiling
+-------
diff --git a/docs/examples/plot_fig3A_hsu_soiling_example.py b/docs/examples/soiling/plot_fig3A_hsu_soiling_example.py
similarity index 97%
rename from docs/examples/plot_fig3A_hsu_soiling_example.py
rename to docs/examples/soiling/plot_fig3A_hsu_soiling_example.py
index 9107791..9103fa0 100644
--- a/docs/examples/plot_fig3A_hsu_soiling_example.py
+++ b/docs/examples/soiling/plot_fig3A_hsu_soiling_example.py
@@ -71,7 +71,7 @@ fig.show()
 # %%
 # Here is the original figure from [1]_ for comparison:
 #
-# .. image:: ../_images/Coello_Boyle_2019_Fig3.png
+# .. image:: ../../_images/Coello_Boyle_2019_Fig3.png
 #   :alt: Figure 3A from the paper showing a simulated soiling signal.
 #
 # Note that this figure shows additional timeseries not calculated here:
diff --git a/docs/examples/plot_greensboro_kimber_soiling.py b/docs/examples/soiling/plot_greensboro_kimber_soiling.py
similarity index 100%
rename from docs/examples/plot_greensboro_kimber_soiling.py
rename to docs/examples/soiling/plot_greensboro_kimber_soiling.py
diff --git a/docs/examples/solar-position/README.rst b/docs/examples/solar-position/README.rst
new file mode 100644
index 0000000..31c7652
--- /dev/null
+++ b/docs/examples/solar-position/README.rst
@@ -0,0 +1,3 @@
+Solar Position
+--------------
+
diff --git a/docs/examples/plot_sunpath_diagrams.py b/docs/examples/solar-position/plot_sunpath_diagrams.py
similarity index 100%
rename from docs/examples/plot_sunpath_diagrams.py
rename to docs/examples/solar-position/plot_sunpath_diagrams.py
diff --git a/docs/examples/solar-tracking/README.rst b/docs/examples/solar-tracking/README.rst
new file mode 100644
index 0000000..b9f06dc
--- /dev/null
+++ b/docs/examples/solar-tracking/README.rst
@@ -0,0 +1,2 @@
+Solar Tracking
+--------------
diff --git a/docs/examples/plot_discontinuous_tracking.py b/docs/examples/solar-tracking/plot_discontinuous_tracking.py
similarity index 100%
rename from docs/examples/plot_discontinuous_tracking.py
rename to docs/examples/solar-tracking/plot_discontinuous_tracking.py
diff --git a/docs/examples/plot_dual_axis_tracking.py b/docs/examples/solar-tracking/plot_dual_axis_tracking.py
similarity index 100%
rename from docs/examples/plot_dual_axis_tracking.py
rename to docs/examples/solar-tracking/plot_dual_axis_tracking.py
diff --git a/docs/examples/plot_single_axis_tracking.py b/docs/examples/solar-tracking/plot_single_axis_tracking.py
similarity index 100%
rename from docs/examples/plot_single_axis_tracking.py
rename to docs/examples/solar-tracking/plot_single_axis_tracking.py
diff --git a/docs/examples/plot_single_axis_tracking_on_sloped_terrain.py b/docs/examples/solar-tracking/plot_single_axis_tracking_on_sloped_terrain.py
similarity index 97%
rename from docs/examples/plot_single_axis_tracking_on_sloped_terrain.py
rename to docs/examples/solar-tracking/plot_single_axis_tracking_on_sloped_terrain.py
index 5fc5f47..66a30c6 100644
--- a/docs/examples/plot_single_axis_tracking_on_sloped_terrain.py
+++ b/docs/examples/solar-tracking/plot_single_axis_tracking_on_sloped_terrain.py
@@ -31,7 +31,7 @@ Modeling backtracking for single-axis tracker arrays on sloped terrain.
 # two choices for the axis azimuth angle, and by convention (at least in the
 # northern hemisphere), the more southward angle is chosen:
 #
-# .. image:: ../_images/tracker_azimuth_angle_convention.png
+# .. image:: ../../_images/tracker_azimuth_angle_convention.png
 #   :alt: Image showing the azimuth convention for single-axis tracker arrays.
 #   :width: 500
 #   :align: center
@@ -48,7 +48,7 @@ Modeling backtracking for single-axis tracker arrays on sloped terrain.
 # Point the right hand thumb along the axis in the direction of the axis
 # azimuth and the fingers curl in the direction of positive rotation angle:
 #
-# .. image:: ../_images/tracker_rotation_angle_convention.png
+# .. image:: ../../_images/tracker_rotation_angle_convention.png
 #   :alt: Image showing the rotation sign convention for single-axis trackers.
 #   :width: 500
 #   :align: center
@@ -66,7 +66,7 @@ Modeling backtracking for single-axis tracker arrays on sloped terrain.
 # with the axis azimuth coming out of the page, an east-facing, downward slope
 # is a negative rotation from horizontal:
 #
-# .. image:: ../_images/ground_slope_angle_convention.png
+# .. image:: ../../_images/ground_slope_angle_convention.png
 #   :alt: Image showing the ground slope sign convention.
 #   :width: 500
 #   :align: center
diff --git a/docs/examples/spectrum/README.rst b/docs/examples/spectrum/README.rst
new file mode 100644
index 0000000..3b6c564
--- /dev/null
+++ b/docs/examples/spectrum/README.rst
@@ -0,0 +1,2 @@
+Spectrum
+--------
diff --git a/docs/examples/plot_spectrl2_fig51A.py b/docs/examples/spectrum/plot_spectrl2_fig51A.py
similarity index 100%
rename from docs/examples/plot_spectrl2_fig51A.py
rename to docs/examples/spectrum/plot_spectrl2_fig51A.py
diff --git a/docs/sphinx/source/_static/version-alert.js b/docs/sphinx/source/_static/version-alert.js
new file mode 100644
index 0000000..e8179a6
--- /dev/null
+++ b/docs/sphinx/source/_static/version-alert.js
@@ -0,0 +1,59 @@
+"use strict";
+
+// Copyright (c) Anymail Contributors.
+// All rights reserved.
+// 
+// Redistribution and use in source and binary forms, with or without modification,
+// are permitted provided that the following conditions are met:
+// 
+//    1. Redistributions of source code must retain the above copyright notice,
+//       this list of conditions and the following disclaimer.
+//
+//    2. Redistributions in binary form must reproduce the above copyright
+//       notice, this list of conditions and the following disclaimer in the
+//       documentation and/or other materials provided with the distribution.
+//
+//    3. Neither the name of the copyright holder nor the names of its contributors
+//       may be used to endorse or promote products derived from this software
+//       without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+// ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+// WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+// ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+// LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+// ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+// SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// Source:
+// https://github.com/anymail/django-anymail/blob/4c443f5515d1d5269a95cb54cf75057c56a3b150/docs/_static/version-alert.js
+// following instructions here:
+// https://michaelgoerz.net/notes/showing-a-warning-for-the-latest-documentation-on-readthedocs.html
+
+function warnOnLatestVersion() {
+
+  // The warning text and link is really specific to RTD hosting,
+  // so we can just check their global to determine version:
+  if (!window.READTHEDOCS_DATA || window.READTHEDOCS_DATA.version !== "latest") {
+    return;  // not latest, or not on RTD
+  }
+
+  var warning = document.createElement('div');
+  warning.setAttribute('class', 'admonition danger');
+  warning.innerHTML = "<p class='first admonition-title'>Note</p> " +
+    "<p class='last'> " +
+    "This document is for an <strong>unreleased development version</strong>. " +
+    "Documentation is available for the <a href='/en/stable/'>current stable release</a>, " +
+    "or for older versions through the &ldquo;v:&rdquo; menu at bottom right." +
+    "</p>";
+  warning.querySelector('a').href = window.location.pathname.replace('/latest', '/stable');
+
+  // modified from original to work better w/ pydata sphinx theme
+  var parent = document.querySelector('main') || document.body;
+  parent.insertBefore(warning, parent.firstChild);
+}
+
+document.addEventListener('DOMContentLoaded', warnOnLatestVersion);
diff --git a/docs/sphinx/source/conf.py b/docs/sphinx/source/conf.py
index e63d561..ddadd9a 100644
--- a/docs/sphinx/source/conf.py
+++ b/docs/sphinx/source/conf.py
@@ -241,6 +241,8 @@ def setup(app):
     # Override footnote callout CSS to be normal text instead of superscript
     # In-line links to references as numbers in brackets.
     app.add_css_file("reference_format.css")
+    # Add a warning banner at the top of the page if viewing the "latest" docs
+    app.add_javascript("version-alert.js")
 
 # -- Options for LaTeX output ---------------------------------------------
 
diff --git a/docs/sphinx/source/whatsnew/v0.9.1.rst b/docs/sphinx/source/whatsnew/v0.9.1.rst
index fdd7f71..d3736f8 100644
--- a/docs/sphinx/source/whatsnew/v0.9.1.rst
+++ b/docs/sphinx/source/whatsnew/v0.9.1.rst
@@ -1,6 +1,6 @@
 .. _whatsnew_0910:
 
-v0.9.1 (December 1, 2021)
+v0.9.1 (TBD)
 --------------------------
 
 Breaking changes
@@ -16,6 +16,11 @@ Bug fixes
 ~~~~~~~~~
 * Address round-off effects in :py:func:`pvlib.ivtools.utils._schumaker_qspline`
   (:issue:`1311`, :pull:`1315`)
+* Fixed a bug in :py:func:`pvlib.spectrum.spectrl2` where negative spectral irradiance
+  values were returned when the sun is behind the plane of array (:issue:`1348`, :pull:`1349`)
+* Fixed bug in :py:func:`pvlib.iotools.get_pvgis_hourly` where the ``optimal_surface_tilt``
+  argument was not being passed to the ``optimalinclination`` request parameter (:pull:`1356`)
+
 
 Testing
 ~~~~~~~
@@ -31,3 +36,6 @@ Contributors
 * Cliff Hansen (:ghuser:`cwhanse`)
 * :ghuser:`Antoine-0`
 * :ghuser:`Carlosbogo`
+* Christian Weickhmann (:ghuser:`cweickhmann`)
+* Kevin Anderson (:ghuser:`kanderso-nrel`)
+* Adam R. Jensen (:ghuser:`AdamRJensen`)
diff --git a/pvlib/iotools/pvgis.py b/pvlib/iotools/pvgis.py
index 3bb2f97..44c5415 100644
--- a/pvlib/iotools/pvgis.py
+++ b/pvlib/iotools/pvgis.py
@@ -190,7 +190,7 @@ def get_pvgis_hourly(latitude, longitude, start=None, end=None,
               'trackingtype': trackingtype, 'components': int(components),
               'usehorizon': int(usehorizon),
               'optimalangles': int(optimalangles),
-              'optimalinclination': int(optimalangles), 'loss': loss}
+              'optimalinclination': int(optimal_surface_tilt), 'loss': loss}
     # pvgis only takes 0 for False, and 1 for True, not strings
     if userhorizon is not None:
         params['userhorizon'] = ','.join(str(x) for x in userhorizon)
diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py
index f1cc128..c5c3b1c 100644
--- a/pvlib/irradiance.py
+++ b/pvlib/irradiance.py
@@ -795,6 +795,12 @@ def haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
     sky_diffuse : numeric
         The sky diffuse component of the solar radiation.
 
+    Notes
+    ------
+    When supplying ``projection_ratio``, consider constraining its values
+    when zenith angle approaches 90 degrees or angle of incidence
+    projection is negative. See code for details.
+
     References
     -----------
     .. [1] Loutzenhiser P.G. et. al. "Empirical validation of models to
diff --git a/pvlib/spectrum/spectrl2.py b/pvlib/spectrum/spectrl2.py
index 15f006b..f679825 100644
--- a/pvlib/spectrum/spectrl2.py
+++ b/pvlib/spectrum/spectrl2.py
@@ -260,6 +260,11 @@ def spectrl2(apparent_zenith, aoi, surface_tilt, ground_albedo,
     2-5                 kasten1966 kasten1966 kastenyoung1989
     =================== ========== ========== ===============
 
+    This implementation also deviates from the reference by including a
+    check for angles of incidence greater than 90 degrees; without this,
+    the model might return negative spectral irradiance values when the
+    sun is behind the plane of array.
+
     References
     ----------
     .. [1] Bird, R, and Riordan, C., 1984, "Simple solar spectral model for
@@ -357,10 +362,16 @@ def spectrl2(apparent_zenith, aoi, surface_tilt, ground_albedo,
     Is = (Ir + Ia + Ig) * Cs  # Eq 3-1
 
     # calculate spectral irradiance on a tilted surface, Eq 3-18
-    Ibeam = Id * cosd(aoi)
-
-    # don't need surface_azimuth if we provide projection_ratio
-    projection_ratio = cosd(aoi) / cosZ
+    # Note: clipping cosd(aoi) to >=0 is not in the reference, but is necessary
+    # to prevent nonsense values when the sun is behind the plane of array.
+    # The same constraint is applied in irradiance.haydavies when not
+    # supplying `projection_ratio`.
+    aoi_projection_nn = np.maximum(cosd(aoi), 0)  # GH 1348
+    Ibeam = Id * aoi_projection_nn
+
+    # don't need surface_azimuth if we provide projection_ratio.
+    # Also constrain cos zenith to avoid blowup, as in irradiance.haydavies
+    projection_ratio = aoi_projection_nn / np.maximum(cosZ, 0.01745)
     Isky = pvlib.irradiance.haydavies(surface_tilt=surface_tilt,
                                       surface_azimuth=None,
                                       dhi=Is,


Test patch:
diff --git a/pvlib/tests/iotools/test_pvgis.py b/pvlib/tests/iotools/test_pvgis.py
index 5a097d2..a7b0cbb 100644
--- a/pvlib/tests/iotools/test_pvgis.py
+++ b/pvlib/tests/iotools/test_pvgis.py
@@ -273,7 +273,7 @@ def test_get_pvgis_hourly_bad_outputformat(requests_mock):
         get_pvgis_hourly(latitude=45, longitude=8, outputformat='basic')
 
 
-url_additional_inputs = 'https://re.jrc.ec.europa.eu/api/seriescalc?lat=55.6814&lon=12.5758&outputformat=csv&angle=0&aspect=0&pvcalculation=1&pvtechchoice=crystSi&mountingplace=free&trackingtype=0&components=1&usehorizon=1&optimalangles=1&optimalinclination=1&loss=2&userhorizon=10%2C15%2C20%2C10&peakpower=5'  # noqa: E501
+url_additional_inputs = 'https://re.jrc.ec.europa.eu/api/seriescalc?lat=55.6814&lon=12.5758&outputformat=csv&angle=0&aspect=0&pvcalculation=1&pvtechchoice=crystSi&mountingplace=free&trackingtype=0&components=1&usehorizon=1&optimalangles=1&optimalinclination=0&loss=2&userhorizon=10%2C15%2C20%2C10&peakpower=5'  # noqa: E501
 
 
 def test_get_pvgis_hourly_additional_inputs(requests_mock):
diff --git a/pvlib/tests/test_spectrum.py b/pvlib/tests/test_spectrum.py
index 657fa26..cfcd50d 100644
--- a/pvlib/tests/test_spectrum.py
+++ b/pvlib/tests/test_spectrum.py
@@ -92,3 +92,17 @@ def test_dayofyear_missing(spectrl2_data):
     kwargs.pop('dayofyear')
     with pytest.raises(ValueError, match='dayofyear must be specified'):
         _ = spectrum.spectrl2(**kwargs)
+
+
+def test_aoi_gt_90(spectrl2_data):
+    # test that returned irradiance values are non-negative when aoi > 90
+    # see GH #1348
+    kwargs, _ = spectrl2_data
+    kwargs['apparent_zenith'] = 70
+    kwargs['aoi'] = 130
+    kwargs['surface_tilt'] = 60
+
+    spectra = spectrum.spectrl2(**kwargs)
+    for key in ['poa_direct', 'poa_global']:
+        message = f'{key} contains negative values for aoi>90'
+        assert np.all(spectra[key] >= 0), message