From 6ff02e5295a2c57f7225a4be54a34cbcd10cca91 Mon Sep 17 00:00:00 2001
From: "Adam R. Jensen" <39184289+AdamRJensen@users.noreply.github.com>
Date: Tue, 12 Dec 2023 16:37:04 +0100
Subject: [PATCH 1/6] Add atand function (#1927)

* Add atand function

* Fix

* Fix me again

* Last fix promise

* Update pvlib/tools.py

Co-authored-by: Kevin Anderson <kevin.anderso@gmail.com>

* Apply suggestions from code review

Thanks for these improvements @adriesse

Co-authored-by: Anton Driesse <anton.driesse@pvperformancelabs.com>

---------

Co-authored-by: Kevin Anderson <kevin.anderso@gmail.com>
Co-authored-by: Anton Driesse <anton.driesse@pvperformancelabs.com>
---
 pvlib/tools.py | 33 +++++++++++++++++++++++----------
 1 file changed, 23 insertions(+), 10 deletions(-)

diff --git a/pvlib/tools.py b/pvlib/tools.py
index fe1b79a5f1..adf502a79d 100644
--- a/pvlib/tools.py
+++ b/pvlib/tools.py
@@ -11,7 +11,7 @@
 
 def cosd(angle):
     """
-    Cosine with angle input in degrees
+    Trigonometric cosine with angle input in degrees.
 
     Parameters
     ----------
@@ -23,14 +23,13 @@ def cosd(angle):
     result : float or array-like
         Cosine of the angle
     """
-
     res = np.cos(np.radians(angle))
     return res
 
 
 def sind(angle):
     """
-    Sine with angle input in degrees
+    Trigonometric sine with angle input in degrees.
 
     Parameters
     ----------
@@ -42,14 +41,13 @@ def sind(angle):
     result : float
         Sin of the angle
     """
-
     res = np.sin(np.radians(angle))
     return res
 
 
 def tand(angle):
     """
-    Tan with angle input in degrees
+    Trigonometric tangent with angle input in degrees.
 
     Parameters
     ----------
@@ -61,14 +59,13 @@ def tand(angle):
     result : float
         Tan of the angle
     """
-
     res = np.tan(np.radians(angle))
     return res
 
 
 def asind(number):
     """
-    Inverse Sine returning an angle in degrees
+    Trigonometric inverse sine returning an angle in degrees.
 
     Parameters
     ----------
@@ -80,14 +77,13 @@ def asind(number):
     result : float
         arcsin result
     """
-
     res = np.degrees(np.arcsin(number))
     return res
 
 
 def acosd(number):
     """
-    Inverse Cosine returning an angle in degrees
+    Trigonometric inverse cosine returning an angle in degrees.
 
     Parameters
     ----------
@@ -99,11 +95,28 @@ def acosd(number):
     result : float
         arccos result
     """
-
     res = np.degrees(np.arccos(number))
     return res
 
 
+def atand(number):
+    """
+    Trigonometric inverse tangent returning an angle in degrees.
+
+    Parameters
+    ----------
+    number : float
+        Input number
+
+    Returns
+    -------
+    result : float
+        arctan result
+    """
+    res = np.degrees(np.arctan(number))
+    return res
+
+
 def localize_to_utc(time, location):
     """
     Converts or localizes a time series to UTC.

From 300aedc27f3a78ab9dc850880d1def92055bc079 Mon Sep 17 00:00:00 2001
From: Echedey Luis <80125792+echedey-ls@users.noreply.github.com>
Date: Tue, 12 Dec 2023 16:53:05 +0100
Subject: [PATCH 2/6] Docstring cleanup: "default None", doi (#1828)

* Align reference

* Update irradiance.py

Exclude .*=| from negative lookahead

^(\s{8}|\s{4})(?!try|else|doi|DOI|Warning|Access|Requests|Note)(\w*): (?!.*:)(.+)$

Reason: just this line :full_moon_with_face:
Remaining matches are type annotations in modelchain.py and pvsystem.py

* Remove parenthesis in types

F: (?<spaces>\s{8}|\s{4})(?<name>\w*)\s?: (?<type>.*) \(optional, default=(?<default>.*)\)$
R:

* Remove none in type definition #1574

1st-F: (?<spaces> {8}| {4})(?<name>\w*) : (?<types>.*), default None$
2nd-F: (?<spaces> {8}| {4})(?<name>\w*) : [Nn}one or (?<type>.*), optional$
3rd-F: (?<spaces> {8}| {4})(?<name>\w*) : (?<type>.*) or [nN]one, optional$

R: $1$2 : $3, optional

* Change if none ocurrences (I)

F: If None,
R: If not specified,

* Change if none ocurrences (II)

F: If None
R: If not specified,

* Too long line

* Too long line

* fix doi external links

Regex find: (?:doi|DOI):(?!`)\s?(.*?)(\.\n|\n)
Replace: :doi:`$1`$2

* Apply cwhanse suggestion

Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>

* Apply cwhanse suggestion

Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>

* Apply cwhanse suggestion

Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>

* Apply cwhanse suggestion

Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>

* Modified cwhanse suggestion

Co-Authored-By: Cliff Hansen <5393711+cwhanse@users.noreply.github.com>

* Update modelchain.py

Co-Authored-By: Cliff Hansen <5393711+cwhanse@users.noreply.github.com>

* Some None's missed

F: (?<spaces> {8}| {4})(?<name>\w*) : [Nn]one[ ,]*(?<type>.*), optional$
R: $1$2 : $3, optional
Co-Authored-By: Cliff Hansen <5393711+cwhanse@users.noreply.github.com>

* ``none``'s, some more ``'s to variables and some other nones

Co-Authored-By: Cliff Hansen <5393711+cwhanse@users.noreply.github.com>

* Special ``none`` cases (please review)

Co-Authored-By: Cliff Hansen <5393711+cwhanse@users.noreply.github.com>

* Linter

Co-Authored-By: Cliff Hansen <5393711+cwhanse@users.noreply.github.com>

* Run more regexes

F1: (?<spaces> {8}| {4})(?<name>\w*) ?: (?<type>.*), default: None
F2: (?<spaces> {8}| {4})(?<name>\w*) ?: (?<types>.*), default None$
F3: (?<spaces> {8}| {4})(?<name>\w*) ?: [Nn]one or (?<type>.*), optional$     zero results
F4: (?<spaces> {8}| {4})(?<name>\w*) ?: (?<type>.*) or [nN]one, optional$     zero results

R: $1$2 : $3, optional

* More Nones

F: (?<spaces> {8}| {4})(?<name>\w*) ?: (?<type>.*), default:? None\.?
R: $1$2 : $3, optional

F: (?<spaces> {8}| {4})(?<name>\w*) ?: [Nn]one, (?<type>.*), default:? (?<def>.*)\.?
R: $1$2 : $3, default $4

F: (?<spaces> {8}| {4})(?<name>\w*) ?: [Nn]one or (?<types>.*)
R: $1$2 : $3

F: (?<spaces> {8}| {4})(?<name>\w*) ?: (?<types>.*) or [Nn]one(?<trailing>.*)
R: $1$2 : $3$4

F: (?<spaces> {8}| {4})(?<name>\w*) ?: [Nn]one(?:,|or) (?<types>.*)
R: $1$2 : $3

* Update irradiance.py

* Revert "Special ``none`` cases (please review)"

This reverts commit b8f942bb4396829986158d3995277c42f418e478.

* Apply suggestions from code review

Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>

* Update irradiance.py

* Update pvlib/irradiance.py

Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>

* Update irradiance.py

---------

Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>
Co-authored-by: Cliff Hansen <5393711+cwhanse@users.noreply.github.com>
---
 pvlib/bifacial/utils.py    |  2 +-
 pvlib/clearsky.py          |  8 ++--
 pvlib/iam.py               | 12 +++---
 pvlib/iotools/epw.py       |  4 +-
 pvlib/iotools/pvgis.py     | 50 +++++++++++------------
 pvlib/iotools/sodapro.py   | 10 ++---
 pvlib/iotools/tmy.py       |  6 +--
 pvlib/irradiance.py        | 57 ++++++++++++++-------------
 pvlib/ivtools/sde.py       |  6 +--
 pvlib/ivtools/sdm.py       |  2 +-
 pvlib/ivtools/utils.py     |  6 +--
 pvlib/location.py          | 22 +++++------
 pvlib/modelchain.py        | 50 +++++++++++------------
 pvlib/pvsystem.py          | 81 +++++++++++++++++++-------------------
 pvlib/scaling.py           |  4 +-
 pvlib/shading.py           |  6 +--
 pvlib/soiling.py           |  4 +-
 pvlib/solarposition.py     | 14 +++----
 pvlib/spectrum/mismatch.py |  2 +-
 pvlib/spectrum/spectrl2.py |  2 +-
 pvlib/temperature.py       |  6 +--
 21 files changed, 177 insertions(+), 177 deletions(-)

diff --git a/pvlib/bifacial/utils.py b/pvlib/bifacial/utils.py
index 8d7f5d5a71..9e6e0bcd60 100644
--- a/pvlib/bifacial/utils.py
+++ b/pvlib/bifacial/utils.py
@@ -80,7 +80,7 @@ def _unshaded_ground_fraction(surface_tilt, surface_azimuth, solar_zenith,
     .. [1] Mikofski, M., Darawali, R., Hamer, M., Neubert, A., and Newmiller,
        J. "Bifacial Performance Modeling in Large Arrays". 2019 IEEE 46th
        Photovoltaic Specialists Conference (PVSC), 2019, pp. 1282-1287.
-       doi: 10.1109/PVSC40753.2019.8980572.
+       :doi:`10.1109/PVSC40753.2019.8980572`.
     """
     tan_phi = _solar_projection_tangent(solar_zenith, solar_azimuth,
                                         surface_azimuth)
diff --git a/pvlib/clearsky.py b/pvlib/clearsky.py
index 62318942da..ad779182eb 100644
--- a/pvlib/clearsky.py
+++ b/pvlib/clearsky.py
@@ -159,7 +159,7 @@ def lookup_linke_turbidity(time, latitude, longitude, filepath=None,
 
     longitude : float or int
 
-    filepath : None or string, default None
+    filepath : string, optional
         The path to the ``.h5`` file.
 
     interp_turbidity : bool, default True
@@ -703,9 +703,9 @@ def detect_clearsky(measured, clearsky, times=None, infer_limits=False,
         Time series of measured GHI. [W/m2]
     clearsky : array or Series
         Time series of the expected clearsky GHI. [W/m2]
-    times : DatetimeIndex or None, default None.
-        Times of measured and clearsky values. If None the index of measured
-        will be used.
+    times : DatetimeIndex, optional
+        Times of measured and clearsky values. If not specified, the index of
+        ``measured`` will be used.
     infer_limits : bool, default False
         If True, does not use passed in kwargs (or defaults), but instead
         interpolates these values from Table 1 in [2]_.
diff --git a/pvlib/iam.py b/pvlib/iam.py
index b0b2202ad9..4f32d352ee 100644
--- a/pvlib/iam.py
+++ b/pvlib/iam.py
@@ -124,7 +124,7 @@ def physical(aoi, n=1.526, K=4.0, L=0.002, *, n_ar=None):
 
     n_ar : numeric, optional
         The effective index of refraction of the anti-reflective (AR) coating
-        (unitless). If n_ar is None (default), no AR coating is applied.
+        (unitless). If ``n_ar`` is not supplied, no AR coating is applied.
         A typical value for the effective index of an AR coating is 1.29.
 
     Returns
@@ -338,7 +338,7 @@ def martin_ruiz_diffuse(surface_tilt, a_r=0.16, c1=0.4244, c2=None):
     c2 : float
         Second fitting parameter for the expressions that approximate the
         integral of diffuse irradiance coming from different directions.
-        If c2 is None, it will be calculated according to the linear
+        If c2 is not specified, it will be calculated according to the linear
         relationship given in [3]_.
 
     Returns
@@ -514,7 +514,7 @@ def sapm(aoi, module, upper=None):
         A dict or Series with the SAPM IAM model parameters.
         See the :py:func:`sapm` notes section for more details.
 
-    upper : None or float, default None
+    upper : float, optional
         Upper limit on the results.
 
     Returns
@@ -541,7 +541,7 @@ def sapm(aoi, module, upper=None):
 
     .. [3] B.H. King et al, "Recent Advancements in Outdoor Measurement
        Techniques for Angle of Incidence Effects," 42nd IEEE PVSC (2015).
-       DOI: 10.1109/PVSC.2015.7355849
+       :doi:`10.1109/PVSC.2015.7355849`
 
     See Also
     --------
@@ -607,7 +607,7 @@ def marion_diffuse(model, surface_tilt, **kwargs):
     .. [1] B. Marion "Numerical method for angle-of-incidence correction
        factors for diffuse radiation incident photovoltaic modules",
        Solar Energy, Volume 147, Pages 344-348. 2017.
-       DOI: 10.1016/j.solener.2017.03.027
+       :doi:`10.1016/j.solener.2017.03.027`
 
     Examples
     --------
@@ -694,7 +694,7 @@ def marion_integrate(function, surface_tilt, region, num=None):
     .. [1] B. Marion "Numerical method for angle-of-incidence correction
        factors for diffuse radiation incident photovoltaic modules",
        Solar Energy, Volume 147, Pages 344-348. 2017.
-       DOI: 10.1016/j.solener.2017.03.027
+       :doi:`10.1016/j.solener.2017.03.027`
 
     Examples
     --------
diff --git a/pvlib/iotools/epw.py b/pvlib/iotools/epw.py
index 249dd76056..a777b69911 100644
--- a/pvlib/iotools/epw.py
+++ b/pvlib/iotools/epw.py
@@ -25,7 +25,7 @@ def read_epw(filename, coerce_year=None):
     filename : String
         Can be a relative file path, absolute file path, or url.
 
-    coerce_year : None or int, default None
+    coerce_year : int, optional
         If supplied, the year of the data will be set to this value. This can
         be a useful feature because EPW data is composed of data from
         different years.
@@ -247,7 +247,7 @@ def parse_epw(csvdata, coerce_year=None):
     csvdata : file-like buffer
         a file-like buffer containing data in the EPW format
 
-    coerce_year : None or int, default None
+    coerce_year : int, optional
         If supplied, the year of the data will be set to this value. This can
         be a useful feature because EPW data is composed of data from
         different years.
diff --git a/pvlib/iotools/pvgis.py b/pvlib/iotools/pvgis.py
index 3f1ba01e97..06986bf2e5 100644
--- a/pvlib/iotools/pvgis.py
+++ b/pvlib/iotools/pvgis.py
@@ -63,13 +63,13 @@ def get_pvgis_hourly(latitude, longitude, start=None, end=None,
         In decimal degrees, between -90 and 90, north is positive (ISO 19115)
     longitude: float
         In decimal degrees, between -180 and 180, east is positive (ISO 19115)
-    start: int or datetime like, default: None
+    start : int or datetime like, optional
         First year of the radiation time series. Defaults to first year
         available.
-    end: int or datetime like, default: None
+    end : int or datetime like, optional
         Last year of the radiation time series. Defaults to last year
         available.
-    raddatabase: str, default: None
+    raddatabase : str, optional
         Name of radiation database. Options depend on location, see [3]_.
     components: bool, default: True
         Output solar radiation components (beam, diffuse, and reflected).
@@ -87,14 +87,14 @@ def get_pvgis_hourly(latitude, longitude, start=None, end=None,
            and pvlib<=0.9.5 is offset by 180 degrees.
     usehorizon: bool, default: True
         Include effects of horizon
-    userhorizon: list of float, default: None
+    userhorizon : list of float, optional
         Optional user specified elevation of horizon in degrees, at equally
         spaced azimuth clockwise from north, only valid if ``usehorizon`` is
-        true, if ``usehorizon`` is true but ``userhorizon`` is ``None`` then
+        true, if ``usehorizon`` is true but ``userhorizon`` is not specified then
         PVGIS will calculate the horizon [4]_
     pvcalculation: bool, default: False
         Return estimate of hourly PV production.
-    peakpower: float, default: None
+    peakpower : float, optional
         Nominal power of PV system in kW. Required if pvcalculation=True.
     pvtechchoice: {'crystSi', 'CIS', 'CdTe', 'Unknown'}, default: 'crystSi'
         PV technology.
@@ -309,12 +309,12 @@ def read_pvgis_hourly(filename, pvgis_format=None, map_variables=True):
     ----------
     filename : str, pathlib.Path, or file-like buffer
         Name, path, or buffer of hourly data file downloaded from PVGIS.
-    pvgis_format : str, default None
+    pvgis_format : str, optional
         Format of PVGIS file or buffer. Equivalent to the ``outputformat``
         parameter in the PVGIS API. If ``filename`` is a file and
-        ``pvgis_format`` is ``None`` then the file extension will be used to
-        determine the PVGIS format to parse. If ``filename`` is a buffer, then
-        ``pvgis_format`` is required and must be in ``['csv', 'json']``.
+        ``pvgis_format`` is not specified then the file extension will be used
+        to determine the PVGIS format to parse. If ``filename`` is a buffer,
+        then ``pvgis_format`` is required and must be in ``['csv', 'json']``.
     map_variables: bool, default True
         When true, renames columns of the DataFrame to pvlib variable names
         where applicable. See variable :const:`VARIABLE_MAP`.
@@ -336,11 +336,11 @@ def read_pvgis_hourly(filename, pvgis_format=None, map_variables=True):
     Raises
     ------
     ValueError
-        if ``pvgis_format`` is ``None`` and the file extension is neither
+        if ``pvgis_format`` is not specified and the file extension is neither
         ``.csv`` nor ``.json`` or if ``pvgis_format`` is provided as
         input but isn't in ``['csv', 'json']``
     TypeError
-        if ``pvgis_format`` is ``None`` and ``filename`` is a buffer
+        if ``pvgis_format`` is not specified and ``filename`` is a buffer
 
     See Also
     --------
@@ -409,14 +409,13 @@ def get_pvgis_tmy(latitude, longitude, outputformat='json', usehorizon=True,
         documentation [2]_ for more info.
     usehorizon : bool, default True
         include effects of horizon
-    userhorizon : list of float, default None
-        optional user specified elevation of horizon in degrees, at equally
-        spaced azimuth clockwise from north, only valid if ``usehorizon`` is
-        true, if ``usehorizon`` is true but ``userhorizon`` is ``None`` then
-        PVGIS will calculate the horizon [3]_
-    startyear : int, default None
+    userhorizon : list of float, optional
+        Optional user-specified elevation of horizon in degrees, at equally
+        spaced azimuth clockwise from north. If not specified, PVGIS will
+        calculate the horizon [3]_. If specified, requires ``usehorizon=True``.
+    startyear : int, optional
         first year to calculate TMY
-    endyear : int, default None
+    endyear : int, optional
         last year to calculate TMY, must be at least 10 years from first year
     map_variables: bool, default True
         When true, renames columns of the Dataframe to pvlib variable names
@@ -573,12 +572,13 @@ def read_pvgis_tmy(filename, pvgis_format=None, map_variables=True):
     ----------
     filename : str, pathlib.Path, or file-like buffer
         Name, path, or buffer of file downloaded from PVGIS.
-    pvgis_format : str, default None
+    pvgis_format : str, optional
         Format of PVGIS file or buffer. Equivalent to the ``outputformat``
         parameter in the PVGIS TMY API. If ``filename`` is a file and
-        ``pvgis_format`` is ``None`` then the file extension will be used to
-        determine the PVGIS format to parse. For PVGIS files from the API with
-        ``outputformat='basic'``, please set ``pvgis_format`` to ``'basic'``.
+        ``pvgis_format`` is not specified then the file extension will be used
+        to determine the PVGIS format to parse. For PVGIS files from the API
+        with ``outputformat='basic'``, please set ``pvgis_format`` to
+        ``'basic'``.
         If ``filename`` is a buffer, then ``pvgis_format`` is required and must
         be in ``['csv', 'epw', 'json', 'basic']``.
     map_variables: bool, default True
@@ -600,11 +600,11 @@ def read_pvgis_tmy(filename, pvgis_format=None, map_variables=True):
     Raises
     ------
     ValueError
-        if ``pvgis_format`` is ``None`` and the file extension is neither
+        if ``pvgis_format`` is not specified and the file extension is neither
         ``.csv``, ``.json``, nor ``.epw``, or if ``pvgis_format`` is provided
         as input but isn't in ``['csv', 'epw', 'json', 'basic']``
     TypeError
-        if ``pvgis_format`` is ``None`` and ``filename`` is a buffer
+        if ``pvgis_format`` is not specified and ``filename`` is a buffer
 
     See Also
     --------
diff --git a/pvlib/iotools/sodapro.py b/pvlib/iotools/sodapro.py
index 95d6ffd866..a6c43ad341 100644
--- a/pvlib/iotools/sodapro.py
+++ b/pvlib/iotools/sodapro.py
@@ -75,8 +75,8 @@ def get_cams(latitude, longitude, start, end, email, identifier='mcclear',
     identifier: {'mcclear', 'cams_radiation'}
         Specify whether to retrieve CAMS Radiation or McClear parameters
     altitude: float, optional
-        Altitude in meters. If None, then the altitude is determined from the
-        NASA SRTM database
+        Altitude in meters. If not specified, then the altitude is determined
+        from the NASA SRTM database
     time_step: str, {'1min', '15min', '1h', '1d', '1M'}, default: '1h'
         Time step of the time series, either 1 minute, 15 minute, hourly,
         daily, or monthly.
@@ -88,7 +88,7 @@ def get_cams(latitude, longitude, start, end, email, identifier='mcclear',
     integrated: boolean, default False
         Whether to return radiation parameters as integrated values (Wh/m^2)
         or as average irradiance values (W/m^2) (pvlib preferred units)
-    label: {'right', 'left'}, default: None
+    label : {'right', 'left'}, optional
         Which bin edge label to label time-step with. The default is 'left' for
         all time steps except for '1M' which has a default of 'right'.
     map_variables: bool, default: True
@@ -241,7 +241,7 @@ def parse_cams(fbuf, integrated=False, label=None, map_variables=True):
     integrated: boolean, default False
         Whether to return radiation parameters as integrated values (Wh/m^2)
         or as average irradiance values (W/m^2) (pvlib preferred units)
-    label: {'right', 'left'}, default: None
+    label : {'right', 'left'}, optional
         Which bin edge label to label time-step with. The default is 'left' for
         all time steps except for '1M' which has a default of 'right'.
     map_variables: bool, default: True
@@ -342,7 +342,7 @@ def read_cams(filename, integrated=False, label=None, map_variables=True):
     integrated: boolean, default False
         Whether to return radiation parameters as integrated values (Wh/m^2)
         or as average irradiance values (W/m^2) (pvlib preferred units)
-    label: {'right', 'left}, default: None
+    label : {'right', 'left}, optional
         Which bin edge label to label time-step with. The default is 'left' for
         all time steps except for '1M' which has a default of 'right'.
     map_variables: bool, default: True
diff --git a/pvlib/iotools/tmy.py b/pvlib/iotools/tmy.py
index 90358ed105..fde96ee679 100644
--- a/pvlib/iotools/tmy.py
+++ b/pvlib/iotools/tmy.py
@@ -40,11 +40,11 @@ def read_tmy3(filename, coerce_year=None, map_variables=None, recolumn=None,
     ----------
     filename : str
         A relative file path or absolute file path.
-    coerce_year : None or int, default None
-        If supplied, the year of the index will be set to `coerce_year`, except
+    coerce_year : int, optional
+        If supplied, the year of the index will be set to ``coerce_year``, except
         for the last index value which will be set to the *next* year so that
         the index increases monotonically.
-    map_variables : bool, default None
+    map_variables : bool, optional
         When True, renames columns of the DataFrame to pvlib variable names
         where applicable. See variable :const:`VARIABLE_MAP`.
     recolumn : bool (deprecated, use map_variables instead)
diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py
index efae7f6236..acccf97ba5 100644
--- a/pvlib/irradiance.py
+++ b/pvlib/irradiance.py
@@ -342,13 +342,13 @@ def get_total_irradiance(surface_tilt, surface_azimuth,
         Global horizontal irradiance. [W/m2]
     dhi : numeric
         Diffuse horizontal irradiance. [W/m2]
-    dni_extra : None or numeric, default None
+    dni_extra : numeric, optional
         Extraterrestrial direct normal irradiance. [W/m2]
-    airmass : None or numeric, default None
+    airmass : numeric, optional
         Relative airmass (not adjusted for pressure). [unitless]
     albedo : numeric, default 0.25
         Ground surface albedo. [unitless]
-    surface_type : None or str, default None
+    surface_type : str, optional
         Surface type. See :py:func:`~pvlib.irradiance.get_ground_diffuse` for
         the list of accepted values.
     model : str, default 'isotropic'
@@ -421,9 +421,9 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
         Global horizontal irradiance. [W/m2]
     dhi : numeric
         Diffuse horizontal irradiance. [W/m2]
-    dni_extra : None or numeric, default None
+    dni_extra : numeric, optional
         Extraterrestrial direct normal irradiance. [W/m2]
-    airmass : None or numeric, default None
+    airmass : numeric, optional
         Relative airmass (not adjusted for pressure). [unitless]
     model : str, default 'isotropic'
         Irradiance model. Can be one of ``'isotropic'``, ``'klucher'``,
@@ -441,7 +441,7 @@ def get_sky_diffuse(surface_tilt, surface_azimuth,
     ------
     ValueError
         If model is one of ``'haydavies'``, ``'reindl'``, or ``'perez'`` and
-        ``dni_extra`` is ``None``.
+        ``dni_extra`` is not specified.
 
     Notes
     -----
@@ -574,10 +574,11 @@ def get_ground_diffuse(surface_tilt, ghi, albedo=.25, surface_type=None):
         the reflection coefficient. Must be >=0 and <=1. Will be
         overridden if surface_type is supplied.
 
-    surface_type: None or string, default None
-        If not None, overrides albedo. String can be one of 'urban',
-        'grass', 'fresh grass', 'snow', 'fresh snow', 'asphalt', 'concrete',
-        'aluminum', 'copper', 'fresh steel', 'dirty steel', 'sea'.
+    surface_type : string, optional
+        If supplied, overrides ``albedo``. ``surface_type`` can be one of
+        'urban', 'grass', 'fresh grass', 'snow', 'fresh snow', 'asphalt',
+        'concrete', 'aluminum', 'copper', 'fresh steel', 'dirty steel',
+        'sea'.
 
     Returns
     -------
@@ -790,17 +791,17 @@ def haydavies(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
     dni_extra : numeric
         Extraterrestrial normal irradiance in W/m^2.
 
-    solar_zenith : None or numeric, default None
+    solar_zenith : numeric, optional
         Solar apparent (refraction-corrected) zenith angles in decimal
         degrees. Must supply ``solar_zenith`` and ``solar_azimuth`` or
         supply ``projection_ratio``.
 
-    solar_azimuth : None or numeric, default None
+    solar_azimuth : numeric, optional
         Solar azimuth angles in decimal degrees. Must supply
         ``solar_zenith`` and ``solar_azimuth`` or supply
         ``projection_ratio``.
 
-    projection_ratio : None or numeric, default None
+    projection_ratio : numeric, optional
         Ratio of angle of incidence projection to solar zenith angle
         projection. Must supply ``solar_zenith`` and ``solar_azimuth``
         or supply ``projection_ratio``.
@@ -1081,7 +1082,7 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
         inputs. AM must be >=0 (careful using the 1/sec(z) model of AM
         generation)
 
-    model : string (optional, default='allsitescomposite1990')
+    model : string, default 'allsitescomposite1990'
         A string which selects the desired set of Perez coefficients. If
         model is not provided as an input, the default, '1990' will be
         used. All possible model selections are:
@@ -1099,7 +1100,7 @@ def perez(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
         * 'capecanaveral1988'
         * 'albany1988'
 
-    return_components: bool (optional, default=False)
+    return_components : bool, default False
         Flag used to decide whether to return the calculated diffuse components
         or not.
 
@@ -1341,8 +1342,8 @@ def perez_driesse(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
         and <=360. The azimuth convention is defined as degrees east of
         north (e.g. North = 0, East = 90, West = 270).
 
-    airmass : numeric (optional, default None)
-        Relative (not pressure-corrected) airmass values. If airmass is a
+    airmass : numeric, optional
+        Relative (not pressure-corrected) airmass values. If ``airmass`` is a
         DataFrame it must be of the same size as all other DataFrame
         inputs. The kastenyoung1989 airmass calculation is used internally
         and is also recommended when pre-calculating airmass because
@@ -1620,9 +1621,9 @@ def disc(ghi, solar_zenith, datetime_or_doy, pressure=101325,
         Day of year or array of days of year e.g.
         pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex.
 
-    pressure : None or numeric, default 101325
-        Site pressure in Pascal. If None, relative airmass is used
-        instead of absolute (pressure-corrected) airmass.
+    pressure : numeric or None, default 101325
+        Site pressure in Pascal. Uses absolute (pressure-corrected) airmass
+        by default. Set to ``None`` to use relative airmass.
 
     min_cos_zenith : numeric, default 0.065
         Minimum value of cos(zenith) to allow when calculating global
@@ -1776,7 +1777,7 @@ def dirint(ghi, solar_zenith, times, pressure=101325., use_delta_kt_prime=True,
         GHI points is 1.5 hours or greater. If use_delta_kt_prime=True,
         input data must be Series.
 
-    temp_dew : None, float, or array-like, default None
+    temp_dew : float, or array-like, optional
         Surface dew point temperatures, in degrees C. Values of temp_dew
         may be numeric or NaN. Any single time period point with a
         temp_dew=NaN does not have dew point improvements applied. If
@@ -2025,7 +2026,7 @@ def dirindex(ghi, ghi_clearsky, dni_clearsky, zenith, times, pressure=101325.,
         GHI points is 1.5 hours or greater. If use_delta_kt_prime=True,
         input data must be Series.
 
-    temp_dew : None, float, or array-like, default None
+    temp_dew : float, or array-like, optional
         Surface dew point temperatures, in degrees C. Values of temp_dew
         may be numeric or NaN. Any single time period point with a
         temp_dew=NaN does not have dew point improvements applied. If
@@ -2133,7 +2134,7 @@ def gti_dirint(poa_global, aoi, solar_zenith, solar_azimuth, times,
         GHI points is 1.5 hours or greater. If use_delta_kt_prime=True,
         input data must be Series.
 
-    temp_dew : None, float, or array-like, default None
+    temp_dew : float, or array-like, optional
         Surface dew point temperatures, in degrees C. Values of temp_dew
         may be numeric or NaN. Any single time period point with a
         temp_dew=NaN does not have dew point improvements applied. If
@@ -2529,11 +2530,11 @@ def erbs_driesse(ghi, zenith, datetime_or_doy=None, dni_extra=None,
         Global horizontal irradiance in W/m^2.
     zenith: numeric
         True (not refraction-corrected) zenith angles in decimal degrees.
-    datetime_or_doy : int, float, array, pd.DatetimeIndex, default None
+    datetime_or_doy : int, float, array or pd.DatetimeIndex, optional
         Day of year or array of days of year e.g.
         pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex.
         Either datetime_or_doy or dni_extra must be provided.
-    dni_extra : numeric, default None
+    dni_extra : numeric, optional
         Extraterrestrial normal irradiance.
         dni_extra can be provided if available to avoid recalculating it
         inside this function.  In this case datetime_or_doy is not required.
@@ -2652,7 +2653,7 @@ def orgill_hollands(ghi, zenith, datetime_or_doy, dni_extra=None,
     datetime_or_doy : int, float, array, pd.DatetimeIndex
         Day of year or array of days of year e.g.
         pd.DatetimeIndex.dayofyear, or pd.DatetimeIndex.
-    dni_extra : None or numeric, default None
+    dni_extra : numeric, optional
         Extraterrestrial direct normal irradiance. [W/m2]
     min_cos_zenith : numeric, default 0.065
         Minimum value of cos(zenith) to allow when calculating global
@@ -2945,7 +2946,7 @@ def _get_perez_coefficients(perezmodel):
     Parameters
     ----------
 
-    perezmodel : string (optional, default='allsitescomposite1990')
+    perezmodel : string, default 'allsitescomposite1990'
 
           a character string which selects the desired set of Perez
           coefficients. If model is not provided as an input, the default,
@@ -3465,7 +3466,7 @@ def dni(ghi, dhi, zenith, clearsky_dni=None, clearsky_tolerance=1.1,
         True (not refraction-corrected) zenith angles in decimal
         degrees. Angles must be >=0 and <=180.
 
-    clearsky_dni : None or Series, default None
+    clearsky_dni : Series, optional
         Clearsky direct normal irradiance.
 
     clearsky_tolerance : float, default 1.1
diff --git a/pvlib/ivtools/sde.py b/pvlib/ivtools/sde.py
index 786a777210..236e91e390 100644
--- a/pvlib/ivtools/sde.py
+++ b/pvlib/ivtools/sde.py
@@ -25,15 +25,15 @@ def fit_sandia_simple(voltage, current, v_oc=None, i_sc=None, v_mp_i_mp=None,
         1D array of `float` type containing current at each point on the IV
         curve, from ``i_sc`` to 0 inclusive. [A]
 
-    v_oc : float, default None
+    v_oc : float, optional
         Open circuit voltage. If not provided, ``v_oc`` is taken as the
         last point in the ``voltage`` array. [V]
 
-    i_sc : float, default None
+    i_sc : float, optional
         Short circuit current. If not provided, ``i_sc`` is taken as the
         first point in the ``current`` array. [A]
 
-    v_mp_i_mp : tuple of float, default None
+    v_mp_i_mp : tuple of float, optional
         Voltage, current at maximum power point. If not provided, the maximum
         power point is found at the maximum of ``voltage`` \times ``current``.
         [V], [A]
diff --git a/pvlib/ivtools/sdm.py b/pvlib/ivtools/sdm.py
index 1f36471a30..7d5a1cdd79 100644
--- a/pvlib/ivtools/sdm.py
+++ b/pvlib/ivtools/sdm.py
@@ -166,7 +166,7 @@ def fit_desoto(v_mp, i_mp, v_oc, i_sc, alpha_sc, beta_voc, cells_in_series,
         Reference temperature condition [C]
     irrad_ref: float, default 1000
         Reference irradiance condition [W/m2]
-    root_kwargs: dictionary, default None
+    root_kwargs : dictionary, optional
         Dictionary of arguments to pass onto scipy.optimize.root()
 
     Returns
diff --git a/pvlib/ivtools/utils.py b/pvlib/ivtools/utils.py
index bdbc42da6c..cde50655dc 100644
--- a/pvlib/ivtools/utils.py
+++ b/pvlib/ivtools/utils.py
@@ -49,7 +49,7 @@ def _numdiff(x, f):
     ----------
     .. [1] M. K. Bowen, R. Smith, "Derivative formulae and errors for
        non-uniformly spaced points", Proceedings of the Royal Society A, vol.
-       461 pp 1975 - 1997, July 2005. DOI: 10.1098/rpsa.2004.1430
+       461 pp 1975 - 1997, July 2005. :doi:`10.1098/rpsa.2004.1430`
     .. [2] PVLib MATLAB https://github.com/sandialabs/MATLAB_PV_LIB
     """
 
@@ -136,9 +136,9 @@ def rectify_iv_curve(voltage, current, decimals=None):
     ----------
     voltage : numeric [V]
     current : numeric [A]
-    decimals : int or None, default None
+    decimals : int, optional
         number of decimal places to which voltage is rounded to remove
-        duplicated points. If None, no rounding is done.
+        duplicated points. If not specified, no rounding is done.
 
     Returns
     -------
diff --git a/pvlib/location.py b/pvlib/location.py
index 46d8477237..0a1f56c6ba 100644
--- a/pvlib/location.py
+++ b/pvlib/location.py
@@ -47,7 +47,7 @@ class Location:
     altitude : float, default 0.
         Altitude from sea level in meters.
 
-    name : None or string, default None.
+    name : string, optional
         Sets the name attribute of the Location object.
 
     See also
@@ -94,7 +94,7 @@ def from_tmy(cls, tmy_metadata, tmy_data=None, **kwargs):
         ----------
         tmy_metadata : dict
             Returned from tmy.readtmy2 or tmy.readtmy3
-        tmy_data : None or DataFrame, default None
+        tmy_data : DataFrame, optional
             Optionally attach the TMY data to this object.
 
         Returns
@@ -138,7 +138,7 @@ def from_epw(cls, metadata, data=None, **kwargs):
         ----------
         metadata : dict
             Returned from epw.read_epw
-        data : None or DataFrame, default None
+        data : DataFrame, optional
             Optionally attach the epw data to this object.
 
         Returns
@@ -173,10 +173,10 @@ def get_solarposition(self, times, pressure=None, temperature=12,
         ----------
         times : pandas.DatetimeIndex
             Must be localized or UTC will be assumed.
-        pressure : None, float, or array-like, default None
-            If None, pressure will be calculated using
+        pressure : float, or array-like, optional
+            If not specified, ``pressure`` is calculated using
             :py:func:`pvlib.atmosphere.alt2pres` and ``self.altitude``.
-        temperature : None, float, or array-like, default 12
+        temperature : float or array-like, default 12
 
         kwargs
             passed to :py:func:`pvlib.solarposition.get_solarposition`
@@ -209,11 +209,11 @@ def get_clearsky(self, times, model='ineichen', solar_position=None,
         model: str, default 'ineichen'
             The clear sky model to use. Must be one of
             'ineichen', 'haurwitz', 'simplified_solis'.
-        solar_position : None or DataFrame, default None
+        solar_position : DataFrame, optional
             DataFrame with columns 'apparent_zenith', 'zenith',
             'apparent_elevation'.
-        dni_extra: None or numeric, default None
-            If None, will be calculated from times.
+        dni_extra : numeric, optional
+            If not specified, will be calculated from times.
 
         kwargs
             Extra parameters passed to the relevant functions. Climatological
@@ -279,9 +279,9 @@ def get_airmass(self, times=None, solar_position=None,
 
         Parameters
         ----------
-        times : None or DatetimeIndex, default None
+        times : DatetimeIndex, optional
             Only used if solar_position is not provided.
-        solar_position : None or DataFrame, default None
+        solar_position : DataFrame, optional
             DataFrame with columns 'apparent_zenith', 'zenith'.
         model : str, default 'kastenyoung1989'
             Relative airmass model. See
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
index c88f8a7641..cc42d59077 100644
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -108,24 +108,24 @@ def basic_chain(times, latitude, longitude,
         as degrees east of north
         (North=0, South=180, East=90, West=270).
 
-    module_parameters : None, dict or Series
+    module_parameters : dict or Series
         Module parameters as defined by the SAPM. See pvsystem.sapm for
         details.
 
-    temperature_model_parameters : None, dict or Series.
+    temperature_model_parameters : dict or Series
         Temperature model parameters as defined by the SAPM.
         See temperature.sapm_cell for details.
 
-    inverter_parameters : None, dict or Series
+    inverter_parameters : dict or Series
         Inverter parameters as defined by the CEC. See
         :py:func:`inverter.sandia` for details.
 
-    irradiance : None or DataFrame, default None
-        If None, calculates clear sky data.
+    irradiance : DataFrame, optional
+        If not specified, calculates clear sky data.
         Columns must be 'dni', 'ghi', 'dhi'.
 
-    weather : None or DataFrame, default None
-        If None, assumes air temperature is 20 C and
+    weather : DataFrame, optional
+        If not specified, assumes air temperature is 20 C and
         wind speed is 0 m/s.
         Columns must be 'wind_speed', 'temp_air'.
 
@@ -138,13 +138,13 @@ def basic_chain(times, latitude, longitude,
     airmass_model : str, default 'kastenyoung1989'
         Passed to atmosphere.relativeairmass.
 
-    altitude : None or float, default None
-        If None, computed from pressure. Assumed to be 0 m
-        if pressure is also None.
+    altitude : float, optional
+        If not specified, computed from ``pressure``. Assumed to be 0 m
+        if ``pressure`` is also unspecified.
 
-    pressure : None or float, default None
-        If None, computed from altitude. Assumed to be 101325 Pa
-        if altitude is also None.
+    pressure : float, optional
+        If not specified, computed from ``altitude``. Assumed to be 101325 Pa
+        if ``altitude`` is also unspecified.
 
     **kwargs
         Arbitrary keyword arguments.
@@ -471,35 +471,35 @@ class ModelChain:
     airmass_model : str, default 'kastenyoung1989'
         Passed to location.get_airmass.
 
-    dc_model: None, str, or function, default None
-        If None, the model will be inferred from the parameters that
+    dc_model : str, or function, optional
+        If not specified, the model will be inferred from the parameters that
         are common to all of system.arrays[i].module_parameters.
         Valid strings are 'sapm', 'desoto', 'cec', 'pvsyst', 'pvwatts'.
         The ModelChain instance will be passed as the first argument
         to a user-defined function.
 
-    ac_model: None, str, or function, default None
-        If None, the model will be inferred from the parameters that
+    ac_model : str, or function, optional
+        If not specified, the model will be inferred from the parameters that
         are common to all of system.inverter_parameters.
         Valid strings are 'sandia', 'adr', 'pvwatts'. The
         ModelChain instance will be passed as the first argument to a
         user-defined function.
 
-    aoi_model: None, str, or function, default None
-        If None, the model will be inferred from the parameters that
+    aoi_model : str, or function, optional
+        If not specified, the model will be inferred from the parameters that
         are common to all of system.arrays[i].module_parameters.
         Valid strings are 'physical', 'ashrae', 'sapm', 'martin_ruiz',
         'interp' and 'no_loss'. The ModelChain instance will be passed as the
         first argument to a user-defined function.
 
-    spectral_model: None, str, or function, default None
-        If None, the model will be inferred from the parameters that
+    spectral_model : str, or function, optional
+        If not specified, the model will be inferred from the parameters that
         are common to all of system.arrays[i].module_parameters.
         Valid strings are 'sapm', 'first_solar', 'no_loss'.
         The ModelChain instance will be passed as the first argument to
         a user-defined function.
 
-    temperature_model: None, str or function, default None
+    temperature_model : str or function, optional
         Valid strings are: 'sapm', 'pvsyst', 'faiman', 'fuentes', 'noct_sam'.
         The ModelChain instance will be passed as the first argument to a
         user-defined function.
@@ -513,7 +513,7 @@ class ModelChain:
         Valid strings are 'pvwatts', 'no_loss'. The ModelChain instance
         will be passed as the first argument to a user-defined function.
 
-    name: None or str, default None
+    name : str, optional
         Name of ModelChain instance.
     """
 
@@ -574,7 +574,7 @@ def with_pvwatts(cls, system, location,
         airmass_model : str, default 'kastenyoung1989'
             Passed to location.get_airmass.
 
-        name: None or str, default None
+        name : str, optional
             Name of ModelChain instance.
 
         **kwargs
@@ -672,7 +672,7 @@ def with_sapm(cls, system, location,
         airmass_model : str, default 'kastenyoung1989'
             Passed to location.get_airmass.
 
-        name: None or str, default None
+        name : str, optional
             Name of ModelChain instance.
 
         **kwargs
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
index f0c1cd2cda..7b2f662b13 100644
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -129,29 +129,29 @@ class PVSystem:
         Azimuth angle of the module surface.
         North=0, East=90, South=180, West=270.
 
-    albedo : None or float, default None
-        Ground surface albedo. If ``None``, then ``surface_type`` is used
+    albedo : float, optional
+        Ground surface albedo. If not supplied, then ``surface_type`` is used
         to look up a value in ``irradiance.SURFACE_ALBEDOS``.
-        If ``surface_type`` is also None then a ground surface albedo
+        If ``surface_type`` is also not supplied then a ground surface albedo
         of 0.25 is used.
 
-    surface_type : None or string, default None
+    surface_type : string, optional
         The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` for
         valid values.
 
-    module : None or string, default None
+    module : string, optional
         The model name of the modules.
         May be used to look up the module_parameters dictionary
         via some other method.
 
-    module_type : None or string, default 'glass_polymer'
+    module_type : string, default 'glass_polymer'
          Describes the module's construction. Valid strings are 'glass_polymer'
          and 'glass_glass'. Used for cell and module temperature calculations.
 
-    module_parameters : None, dict or Series, default None
+    module_parameters : dict or Series, optional
         Module parameters as defined by the SAPM, CEC, or other.
 
-    temperature_model_parameters : None, dict or Series, default None.
+    temperature_model_parameters : dict or Series, optional
         Temperature model parameters as required by one of the models in
         pvlib.temperature (excluding poa_global, temp_air and wind_speed).
 
@@ -161,22 +161,22 @@ class PVSystem:
     strings_per_inverter: int or float, default 1
         See system topology discussion above.
 
-    inverter : None or string, default None
+    inverter : string, optional
         The model name of the inverters.
         May be used to look up the inverter_parameters dictionary
         via some other method.
 
-    inverter_parameters : None, dict or Series, default None
+    inverter_parameters : dict or Series, optional
         Inverter parameters as defined by the SAPM, CEC, or other.
 
-    racking_model : None or string, default 'open_rack'
+    racking_model : string, default 'open_rack'
         Valid strings are 'open_rack', 'close_mount', and 'insulated_back'.
         Used to identify a parameter set for the SAPM cell temperature model.
 
-    losses_parameters : None, dict or Series, default None
+    losses_parameters : dict or Series, optional
         Losses parameters as defined by PVWatts or other.
 
-    name : None or string, default None
+    name : string, optional
 
     **kwargs
         Arbitrary keyword arguments.
@@ -328,12 +328,11 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
             Global horizontal irradiance. [W/m2]
         dhi : float or Series or tuple of float or Series
             Diffuse horizontal irradiance. [W/m2]
-        dni_extra : None, float, Series or tuple of float or Series,\
-            default None
+        dni_extra : float, Series or tuple of float or Series, optional
             Extraterrestrial direct normal irradiance. [W/m2]
-        airmass : None, float or Series, default None
+        airmass : float or Series, optional
             Airmass. [unitless]
-        albedo : None, float or Series, default None
+        albedo : float or Series, optional
             Ground surface albedo. [unitless]
         model : String, default 'haydavies'
             Irradiance model.
@@ -906,29 +905,29 @@ class Array:
         single axis tracker. Mounting is used to determine module orientation.
         If not provided, a FixedMount with zero tilt is used.
 
-    albedo : None or float, default None
-        Ground surface albedo. If ``None``, then ``surface_type`` is used
+    albedo : float, optional
+        Ground surface albedo. If not supplied, then ``surface_type`` is used
         to look up a value in ``irradiance.SURFACE_ALBEDOS``.
-        If ``surface_type`` is also None then a ground surface albedo
+        If ``surface_type`` is also not supplied then a ground surface albedo
         of 0.25 is used.
 
-    surface_type : None or string, default None
+    surface_type : string, optional
         The ground surface type. See ``irradiance.SURFACE_ALBEDOS`` for valid
         values.
 
-    module : None or string, default None
+    module : string, optional
         The model name of the modules.
         May be used to look up the module_parameters dictionary
         via some other method.
 
-    module_type : None or string, default None
+    module_type : string, optional
          Describes the module's construction. Valid strings are 'glass_polymer'
          and 'glass_glass'. Used for cell and module temperature calculations.
 
-    module_parameters : None, dict or Series, default None
+    module_parameters : dict or Series, optional
         Parameters for the module model, e.g., SAPM, CEC, or other.
 
-    temperature_model_parameters : None, dict or Series, default None.
+    temperature_model_parameters : dict or Series, optional
         Parameters for the module temperature model, e.g., SAPM, Pvsyst, or
         other.
 
@@ -938,10 +937,10 @@ class Array:
     strings: int, default 1
         Number of parallel strings in the array.
 
-    array_losses_parameters: None, dict or Series, default None.
+    array_losses_parameters : dict or Series, optional
         Supported keys are 'dc_ohmic_percent'.
 
-    name: None or str, default None
+    name : str, optional
         Name of Array instance.
     """
 
@@ -1095,11 +1094,11 @@ def get_irradiance(self, solar_zenith, solar_azimuth, dni, ghi, dhi,
             Global horizontal irradiance
         dhi : float or Series
             Diffuse horizontal irradiance. [W/m2]
-        dni_extra : None, float or Series, default None
+        dni_extra : float or Series, optional
             Extraterrestrial direct normal irradiance. [W/m2]
-        airmass : None, float or Series, default None
+        airmass : float or Series, optional
             Airmass. [unitless]
-        albedo : None, float or Series, default None
+        albedo : float or Series, optional
             Ground surface albedo. [unitless]
         model : String, default 'haydavies'
             Irradiance model.
@@ -1536,10 +1535,10 @@ def calcparams_desoto(effective_irradiance, temp_cell,
         the SAM CEC module database, dEgdT=-0.0002677 is implicit for all cell
         types in the parameter estimation algorithm used by NREL.
 
-    irrad_ref : float (optional, default=1000)
+    irrad_ref : float, default 1000
         Reference irradiance in W/m^2.
 
-    temp_ref : float (optional, default=25)
+    temp_ref : float, default 25
         Reference cell temperature in C.
 
     Returns
@@ -1752,10 +1751,10 @@ def calcparams_cec(effective_irradiance, temp_cell,
         the SAM CEC module database, dEgdT=-0.0002677 is implicit for all cell
         types in the parameter estimation algorithm used by NREL.
 
-    irrad_ref : float (optional, default=1000)
+    irrad_ref : float, default 1000
         Reference irradiance in W/m^2.
 
-    temp_ref : float (optional, default=25)
+    temp_ref : float, default 25
         Reference cell temperature in C.
 
     Returns
@@ -1869,10 +1868,10 @@ def calcparams_pvsyst(effective_irradiance, temp_cell,
         The energy bandgap at reference temperature in units of eV.
         1.121 eV for crystalline silicon. EgRef must be >0.
 
-    irrad_ref : float (optional, default=1000)
+    irrad_ref : float, default 1000
         Reference irradiance in W/m^2.
 
-    temp_ref : float (optional, default=25)
+    temp_ref : float, default 25
         Reference cell temperature in C.
 
     Returns
@@ -1974,7 +1973,7 @@ def retrieve_sam(name=None, path=None):
 
     Parameters
     ----------
-    name : None or string, default None
+    name : string, optional
         Name can be one of:
 
         * 'CECMod' - returns the CEC module database
@@ -1985,7 +1984,7 @@ def retrieve_sam(name=None, path=None):
         * 'SandiaMod' - returns the Sandia Module database
         * 'ADRInverter' - returns the ADR Inverter database
 
-    path : None or string, default None
+    path : string, optional
         Path to the SAM file. May also be a URL.
 
     Returns
@@ -2395,9 +2394,9 @@ def singlediode(photocurrent, saturation_current, resistance_series,
         junction in Kelvin, and :math:`q` is the charge of an electron
         (coulombs). ``0 < nNsVth``.  [V]
 
-    ivcurve_pnts : None or int, default None
-        Number of points in the desired IV curve. If None or 0, no points on
-        the IV curves will be produced.
+    ivcurve_pnts : int, optional
+        Number of points in the desired IV curve. If not specified or 0, no
+        points on the IV curves will be produced.
 
         .. deprecated:: 0.10.0
            Use :py:func:`pvlib.pvsystem.v_from_i` and
diff --git a/pvlib/scaling.py b/pvlib/scaling.py
index dca2ca4935..7fbc0a46d1 100644
--- a/pvlib/scaling.py
+++ b/pvlib/scaling.py
@@ -30,7 +30,7 @@ def wvm(clearsky_index, positions, cloud_speed, dt=None):
     cloud_speed : numeric
         Speed of cloud movement in meters per second [m/s].
 
-    dt : float, default None
+    dt : float, optional
         The time series time delta. By default, is inferred from the
         clearsky_index. Must be specified for a time series that doesn't
         include an index. Units of seconds [s].
@@ -216,7 +216,7 @@ def _compute_wavelet(clearsky_index, dt=None):
     clearsky_index : numeric or pandas.Series
         Clear Sky Index time series that will be smoothed.
 
-    dt : float, default None
+    dt : float, optional
         The time series time delta. By default, is inferred from the
         clearsky_index. Must be specified for a time series that doesn't
         include an index. Units of seconds [s].
diff --git a/pvlib/shading.py b/pvlib/shading.py
index 1533d2a013..c0a7a91f18 100644
--- a/pvlib/shading.py
+++ b/pvlib/shading.py
@@ -85,7 +85,7 @@ def masking_angle(surface_tilt, gcr, slant_height):
     ----------
     .. [1] D. Passias and B. Källbäck, "Shading effects in rows of solar cell
        panels", Solar Cells, Volume 11, Pages 281-291.  1984.
-       DOI: 10.1016/0379-6787(84)90017-6
+       :doi:`10.1016/0379-6787(84)90017-6`
     .. [2] Gilman, P. et al., (2018). "SAM Photovoltaic Model Technical
        Reference Update", NREL Technical Report NREL/TP-6A20-67399.
        Available at https://www.nrel.gov/docs/fy18osti/67399.pdf
@@ -167,7 +167,7 @@ def masking_angle_passias(surface_tilt, gcr):
     ----------
     .. [1] D. Passias and B. Källbäck, "Shading effects in rows of solar cell
        panels", Solar Cells, Volume 11, Pages 281-291.  1984.
-       DOI: 10.1016/0379-6787(84)90017-6
+       :doi:`10.1016/0379-6787(84)90017-6`
     """
     # wrap it in an array so that division by zero is handled well
     beta = np.radians(np.array(surface_tilt))
@@ -226,7 +226,7 @@ def sky_diffuse_passias(masking_angle):
     ----------
     .. [1] D. Passias and B. Källbäck, "Shading effects in rows of solar cell
        panels", Solar Cells, Volume 11, Pages 281-291.  1984.
-       DOI: 10.1016/0379-6787(84)90017-6
+       :doi:`10.1016/0379-6787(84)90017-6`
     .. [2] Gilman, P. et al., (2018). "SAM Photovoltaic Model Technical
        Reference Update", NREL Technical Report NREL/TP-6A20-67399.
        Available at https://www.nrel.gov/docs/fy18osti/67399.pdf
diff --git a/pvlib/soiling.py b/pvlib/soiling.py
index bbad4862f4..bcdaf97b58 100644
--- a/pvlib/soiling.py
+++ b/pvlib/soiling.py
@@ -58,7 +58,7 @@ def hsu(rainfall, cleaning_threshold, surface_tilt, pm2_5, pm10,
     -----------
     .. [1] M. Coello and L. Boyle, "Simple Model For Predicting Time Series
        Soiling of Photovoltaic Panels," in IEEE Journal of Photovoltaics.
-       doi: 10.1109/JPHOTOV.2019.2919628
+       :doi:`10.1109/JPHOTOV.2019.2919628`
     .. [2] Atmospheric Chemistry and Physics: From Air Pollution to Climate
        Change. J. Seinfeld and S. Pandis. Wiley and Sons 2001.
 
@@ -128,7 +128,7 @@ def kimber(rainfall, cleaning_threshold=6, soiling_loss_rate=0.0015,
     max_soiling : float, default 0.3
         Maximum fraction of energy lost due to soiling. Soiling will build up
         until this value. [unitless]
-    manual_wash_dates : sequence or None, default None
+    manual_wash_dates : sequence, optional
         List or tuple of dates as Python ``datetime.date`` when the panels were
         washed manually. Note there is no grace period after a manual wash, so
         soiling begins to build up immediately.
diff --git a/pvlib/solarposition.py b/pvlib/solarposition.py
index cdcacd7ec6..38ffa9e51c 100644
--- a/pvlib/solarposition.py
+++ b/pvlib/solarposition.py
@@ -51,13 +51,13 @@ def get_solarposition(time, latitude, longitude,
         Longitude in decimal degrees. Positive east of prime meridian,
         negative to west.
 
-    altitude : None or float, default None
-        If None, computed from pressure. Assumed to be 0 m
-        if pressure is also None.
+    altitude : float, optional
+        If not specified, computed from ``pressure``. Assumed to be 0 m
+        if ``pressure`` is not supplied.
 
-    pressure : None or float, default None
-        If None, computed from altitude. Assumed to be 101325 Pa
-        if altitude is also None.
+    pressure : float, optional
+        If not specified, computed from ``altitude``. Assumed to be 101325 Pa
+        if ``altitude`` is not supplied.
 
     method : string, default 'nrel_numpy'
         'nrel_numpy' uses an implementation of the NREL SPA algorithm
@@ -312,7 +312,7 @@ def spa_python(time, latitude, longitude,
         *Note: delta_t = None will break code using nrel_numba,
         this will be fixed in a future version.*
         The USNO has historical and forecasted delta_t [3]_.
-    atmos_refrac : None or float, optional, default None
+    atmos_refrac : float, optional
         The approximate atmospheric refraction (in degrees)
         at sunrise and sunset.
     how : str, optional, default 'numpy'
diff --git a/pvlib/spectrum/mismatch.py b/pvlib/spectrum/mismatch.py
index 10f8db3564..e51cdf8625 100644
--- a/pvlib/spectrum/mismatch.py
+++ b/pvlib/spectrum/mismatch.py
@@ -115,7 +115,7 @@ def get_am15g(wavelength=None):
     References
     ----------
     .. [1] ASTM "G173-03 Standard Tables for Reference Solar Spectral
-        Irradiances: Direct Normal and Hemispherical on 37° Tilted Surface."
+       Irradiances: Direct Normal and Hemispherical on 37° Tilted Surface."
     '''
     # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Aug. 2022
 
diff --git a/pvlib/spectrum/spectrl2.py b/pvlib/spectrum/spectrl2.py
index f679825014..1c1f102fec 100644
--- a/pvlib/spectrum/spectrl2.py
+++ b/pvlib/spectrum/spectrl2.py
@@ -270,7 +270,7 @@ def spectrl2(apparent_zenith, aoi, surface_tilt, ground_albedo,
     .. [1] Bird, R, and Riordan, C., 1984, "Simple solar spectral model for
        direct and diffuse irradiance on horizontal and tilted planes at the
        earth's surface for cloudless atmospheres", NREL Technical Report
-       TR-215-2436 doi:10.2172/5986936.
+       TR-215-2436 :doi:`10.2172/5986936`.
     .. [2] Bird Simple Spectral Model: spectrl2_2.c.
        https://www.nrel.gov/grid/solar-resource/spectral.html
     """
diff --git a/pvlib/temperature.py b/pvlib/temperature.py
index 63cf4baf79..6901b4e5e0 100644
--- a/pvlib/temperature.py
+++ b/pvlib/temperature.py
@@ -721,7 +721,7 @@ def fuentes(poa_global, temp_air, wind_speed, noct_installed, module_height=5,
            http://prod.sandia.gov/techlib/access-control.cgi/1985/850330.pdf
     .. [2] Dobos, A. P., 2014, "PVWatts Version 5 Manual", NREL/TP-6A20-62641,
            National Renewable Energy Laboratory, Golden CO.
-           doi:10.2172/1158421.
+           :doi:`10.2172/1158421`.
     """
     # ported from the FORTRAN77 code provided in Appendix A of Fuentes 1987;
     # nearly all variable names are kept the same for ease of comparison.
@@ -872,8 +872,8 @@ def noct_sam(poa_global, temp_air, wind_speed, noct, module_efficiency,
         :math:`\eta_{m} = \frac{V_{mp} I_{mp}}{A \times 1000 W/m^2}`
         where A is module area [m^2].
 
-    effective_irradiance : numeric, default None.
-        The irradiance that is converted to photocurrent. If None,
+    effective_irradiance : numeric, optional
+        The irradiance that is converted to photocurrent. If not specified,
         assumed equal to poa_global. [W/m^2]
 
     transmittance_absorptance : numeric, default 0.9

From c18a00413fde8021055f8709bbc67ed9789bf9f8 Mon Sep 17 00:00:00 2001
From: Anton Driesse <anton.driesse@pvperformancelabs.com>
Date: Tue, 12 Dec 2023 16:03:46 +0000
Subject: [PATCH 3/6] Implement reverse transposition using Perez-Driesse
 forward transposition (#1907)

* Add reverse transposition function and two helpers.

* Add missing import.

* Add to docs under transposition until a better place is found/made.

* Minor doc string fixes.

* Add full_output option similar to newton().

* First example for reverse transposition.

* Second example for reverse transposition.

* Some improvements to the two examples.

* Placate flake8.

* Add tests for reverse transpostion.

* Refine examples and fix test.

* Update whatsnew.

* Refine examples.

* Try to get rid of matplotlib warning in example.

* Remove unused import.

* Update pvlib/irradiance.py

Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>

* Improve examples based on reviews.

* Settle conflict.

* Try again.

* Remove one space.

* Final(?) changes.

* Update reference in erbs_driesse().

* Fix links in examples.

* Update docs/examples/irradiance-transposition/plot_rtranpose_limitations.py

Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com>

* Address review comments.

* Final renames.

---------

Co-authored-by: Cliff Hansen <cwhanse@sandia.gov>
Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com>
---
 .../plot_rtranpose_limitations.py             | 181 ++++++++++++++++++
 .../plot_rtranpose_year.py                    | 152 +++++++++++++++
 .../reference/irradiance/transposition.rst    |   1 +
 docs/sphinx/source/whatsnew/v0.10.3.rst       |   5 +
 pvlib/irradiance.py                           | 176 ++++++++++++++++-
 pvlib/tests/test_irradiance.py                |  44 +++++
 6 files changed, 554 insertions(+), 5 deletions(-)
 create mode 100644 docs/examples/irradiance-transposition/plot_rtranpose_limitations.py
 create mode 100644 docs/examples/irradiance-transposition/plot_rtranpose_year.py

diff --git a/docs/examples/irradiance-transposition/plot_rtranpose_limitations.py b/docs/examples/irradiance-transposition/plot_rtranpose_limitations.py
new file mode 100644
index 0000000000..8df8339ad4
--- /dev/null
+++ b/docs/examples/irradiance-transposition/plot_rtranpose_limitations.py
@@ -0,0 +1,181 @@
+"""
+Reverse transposition limitations
+====================================
+
+Unfortunately, sometimes there is not a unique solution.
+
+Author: Anton Driesse
+
+"""
+
+# %%
+#
+# Introduction
+# ------------
+# When irradiance is measured on a tilted plane, it is useful to be able to
+# estimate the GHI that produces the POA irradiance.
+# The estimation requires inverting a GHI-to-POA irradiance model,
+# which involves two parts:
+# a decomposition of GHI into direct and diffuse components,
+# and a transposition model that calculates the direct and diffuse irradiance
+# on the tilted plane.
+# Recovering GHI from POA irradiance is termed "reverse transposition."
+#
+# Unfortunately, for a given POA irradiance value, sometimes there is not a
+# unique solution for GHI.
+# Different GHI values can produce different combinations of direct and
+# diffuse irradiance that sum to the same POA irradiance value.
+#
+# In this example we look at a single point in time and consider a full range
+# of possible GHI and POA global values as shown in figures 3 and 4 of [1]_.
+# Then we use :py:meth:`pvlib.irradiance.ghi_from_poa_driesse_2023` to estimate
+# the original GHI from POA global.
+#
+# References
+# ----------
+# .. [1] Driesse, A., Jensen, A., Perez, R., 2024. A Continuous form of the
+#     Perez diffuse sky model for forward and reverse transposition.
+#     Solar Energy vol. 267. :doi:`10.1016/j.solener.2023.112093`
+#
+
+import numpy as np
+
+import matplotlib
+import matplotlib.pyplot as plt
+
+from pvlib.irradiance import (erbs_driesse,
+                              get_total_irradiance,
+                              ghi_from_poa_driesse_2023,
+                              )
+
+matplotlib.rcParams['axes.grid'] = True
+
+# %%
+#
+# Define the conditions that were used for figure 3 in [1]_.
+#
+
+dni_extra = 1366.1
+albedo = 0.25
+surface_tilt = 40
+surface_azimuth = 180
+
+solar_azimuth = 82
+solar_zenith = 75
+
+# %%
+#
+# Define a range of possible GHI values and calculate the corresponding
+# POA global.  First estimate DNI and DHI using the Erbs-Driesse model, then
+# transpose using the Perez-Driesse model.
+#
+
+ghi = np.linspace(0, 500, 100+1)
+
+erbsout = erbs_driesse(ghi, solar_zenith, dni_extra=dni_extra)
+
+dni = erbsout['dni']
+dhi = erbsout['dhi']
+
+irrads = get_total_irradiance(surface_tilt, surface_azimuth,
+                              solar_zenith, solar_azimuth,
+                              dni, ghi, dhi,
+                              dni_extra,
+                              model='perez-driesse')
+
+poa_global = irrads['poa_global']
+
+# %%
+#
+# Suppose you measure that POA global is 200 W/m2. What would GHI be?
+#
+
+poa_test = 200
+
+ghi_hat = ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth,
+                                    solar_zenith, solar_azimuth,
+                                    poa_test,
+                                    dni_extra,
+                                    full_output=False)
+
+print('Estimated GHI: %.2f W/m².' % ghi_hat)
+
+# %%
+#
+# Show this result on the graph of all possible combinations of GHI and POA.
+#
+
+plt.figure()
+plt.plot(ghi, poa_global, 'k-')
+plt.axvline(ghi_hat, color='g', lw=1)
+plt.axhline(poa_test, color='g', lw=1)
+plt.plot(ghi_hat, poa_test, 'gs')
+plt.annotate('GHI=%.2f' % (ghi_hat),
+             xy=(ghi_hat-2, 200+2),
+             xytext=(ghi_hat-20, 200+20),
+             ha='right',
+             arrowprops={'arrowstyle': 'simple'})
+plt.xlim(0, 500)
+plt.ylim(0, 250)
+plt.xlabel('GHI [W/m²]')
+plt.ylabel('POA [W/m²]')
+plt.show()
+
+# %%
+#
+# Now change the solar azimuth to match the conditions for figure 4 in [1]_.
+#
+
+solar_azimuth = 76
+
+# %%
+#
+# Again, estimate DNI and DHI using the Erbs-Driesse model, then
+# transpose using the Perez-Driesse model.
+#
+
+erbsout = erbs_driesse(ghi, solar_zenith, dni_extra=dni_extra)
+
+dni = erbsout['dni']
+dhi = erbsout['dhi']
+
+irrads = get_total_irradiance(surface_tilt, surface_azimuth,
+                              solar_zenith, solar_azimuth,
+                              dni, ghi, dhi,
+                              dni_extra,
+                              model='perez-driesse')
+
+poa_global = irrads['poa_global']
+
+# %%
+#
+# Now reverse transpose all the POA values and observe that the original
+# GHI cannot always be found.  There is a range of POA values that
+# maps to three possible GHI values, and there is not enough information
+# to choose one of them.  Sometimes we get lucky and the right one comes
+# out, other times not.
+#
+
+result = ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth,
+                                   solar_zenith, solar_azimuth,
+                                   poa_global,
+                                   dni_extra,
+                                   full_output=True,
+                                   )
+
+ghi_hat, conv, niter = result
+correct = np.isclose(ghi, ghi_hat, atol=0.01)
+
+plt.figure()
+plt.plot(np.where(correct, ghi, np.nan), np.where(correct, poa_global, np.nan),
+         'g.', label='correct GHI found')
+plt.plot(ghi[~correct], poa_global[~correct], 'r.', label='unreachable GHI')
+plt.plot(ghi[~conv], poa_global[~conv], 'm.', label='out of range (kt > 1.25)')
+plt.axhspan(88, 103, color='y', alpha=0.25, label='problem region')
+
+plt.xlim(0, 500)
+plt.ylim(0, 250)
+plt.xlabel('GHI [W/m²]')
+plt.ylabel('POA [W/m²]')
+plt.legend()
+plt.show()
diff --git a/docs/examples/irradiance-transposition/plot_rtranpose_year.py b/docs/examples/irradiance-transposition/plot_rtranpose_year.py
new file mode 100644
index 0000000000..c0b9860bba
--- /dev/null
+++ b/docs/examples/irradiance-transposition/plot_rtranpose_year.py
@@ -0,0 +1,152 @@
+"""
+Reverse transposition using one year of hourly data
+===================================================
+
+With a brief look at accuracy and speed.
+
+Author: Anton Driesse
+
+"""
+# %%
+#
+# Introduction
+# ------------
+# When irradiance is measured on a tilted plane, it is useful to be able to
+# estimate the GHI that produces the POA irradiance.
+# The estimation requires inverting a GHI-to-POA irradiance model,
+# which involves two parts:
+# a decomposition of GHI into direct and diffuse components,
+# and a transposition model that calculates the direct and diffuse
+# irradiance on the tilted plane.
+# Recovering GHI from POA irradiance is termed "reverse transposition."
+#
+# In this example we start with a TMY file and calculate POA global irradiance.
+# Then we use :py:meth:`pvlib.irradiance.ghi_from_poa_driesse_2023` to estimate
+# the original GHI from POA global.  Details of the method found in [1]_.
+#
+# Another method for reverse tranposition called GTI-DIRINT is also
+# available in pvlib python (:py:meth:`pvlib.irradiance.gti_dirint`).
+# More information is available in [2]_.
+#
+# References
+# ----------
+# .. [1] Driesse, A., Jensen, A., Perez, R., 2024. A Continuous form of the
+#     Perez diffuse sky model for forward and reverse transposition.
+#     Solar Energy vol. 267. :doi:`10.1016/j.solener.2023.112093`
+#
+# .. [2] B. Marion, A model for deriving the direct normal and
+#        diffuse horizontal irradiance from the global tilted
+#        irradiance, Solar Energy 122, 1037-1046.
+#        :doi:`10.1016/j.solener.2015.10.024`
+
+import os
+import time
+import pandas as pd
+
+import matplotlib.pyplot as plt
+
+import pvlib
+from pvlib import iotools, location
+from pvlib.irradiance import (get_extra_radiation,
+                              get_total_irradiance,
+                              ghi_from_poa_driesse_2023,
+                              aoi,
+                              )
+
+# %%
+#
+# Read a TMY3 file containing weather data and select needed columns.
+#
+
+PVLIB_DIR = pvlib.__path__[0]
+DATA_FILE = os.path.join(PVLIB_DIR, 'data', '723170TYA.CSV')
+
+tmy, metadata = iotools.read_tmy3(DATA_FILE, coerce_year=1990,
+                                  map_variables=True)
+
+df = pd.DataFrame({'ghi': tmy['ghi'], 'dhi': tmy['dhi'], 'dni': tmy['dni'],
+                   'temp_air': tmy['temp_air'],
+                   'wind_speed': tmy['wind_speed'],
+                   })
+
+# %%
+#
+# Shift the timestamps to the middle of the hour and calculate sun positions.
+#
+
+df.index = df.index - pd.Timedelta(minutes=30)
+
+loc = location.Location.from_tmy(metadata)
+solpos = loc.get_solarposition(df.index)
+
+# %%
+#
+# Estimate global irradiance on a fixed-tilt array (forward transposition).
+# The array is tilted 30 degrees and oriented 30 degrees east of south.
+#
+
+TILT = 30
+ORIENT = 150
+
+df['dni_extra'] = get_extra_radiation(df.index)
+
+total_irrad = get_total_irradiance(TILT, ORIENT,
+                                   solpos.apparent_zenith,
+                                   solpos.azimuth,
+                                   df.dni, df.ghi, df.dhi,
+                                   dni_extra=df.dni_extra,
+                                   model='perez-driesse')
+
+df['poa_global'] = total_irrad.poa_global
+df['aoi'] = aoi(TILT, ORIENT, solpos.apparent_zenith, solpos.azimuth)
+
+# %%
+#
+# Now estimate ghi from poa_global using reverse transposition.
+# The algorithm uses a simple bisection search, which is quite slow
+# because scipy doesn't offer a vectorized version (yet).
+# For this reason we'll process a random sample of 1000 timestamps
+# rather than the whole year.
+#
+
+df = df[df.ghi > 0].sample(n=1000)
+solpos = solpos.reindex(df.index)
+
+start = time.process_time()
+
+df['ghi_rev'] = ghi_from_poa_driesse_2023(TILT, ORIENT,
+                                          solpos.apparent_zenith,
+                                          solpos.azimuth,
+                                          df.poa_global,
+                                          dni_extra=df.dni_extra)
+finish = time.process_time()
+
+print('Elapsed time for reverse transposition: %.1f s' % (finish - start))
+
+# %%
+#
+# This graph shows the reverse transposed values vs. the original values.
+# The markers are color-coded by angle-of-incidence to show that
+# errors occur primarily with incidence angle approaching 90° and beyond.
+#
+# Note that the results look particularly good because the POA values
+# were calculated using the same models as used in reverse transposition.
+# This isn't cheating though.  It's a way of ensuring that the errors
+# we see are really due to the reverse transposition algorithm.
+# Expect to see larger errors with real-word POA measurements
+# because errors from forward and reverse transposition will both be present.
+#
+
+df = df.sort_values('aoi')
+
+plt.figure()
+plt.gca().grid(True, alpha=.5)
+pc = plt.scatter(df['ghi'], df['ghi_rev'], c=df['aoi'], s=15,
+                 cmap='jet', vmin=60, vmax=120)
+plt.colorbar(label='AOI [°]')
+pc.set_alpha(0.5)
+
+plt.xlabel('GHI original [W/m²]')
+plt.ylabel('GHI from POA [W/m²]')
+
+plt.show()
diff --git a/docs/sphinx/source/reference/irradiance/transposition.rst b/docs/sphinx/source/reference/irradiance/transposition.rst
index 7b3624e692..22136f0c58 100644
--- a/docs/sphinx/source/reference/irradiance/transposition.rst
+++ b/docs/sphinx/source/reference/irradiance/transposition.rst
@@ -15,3 +15,4 @@ Transposition models
    irradiance.klucher
    irradiance.reindl
    irradiance.king
+   irradiance.ghi_from_poa_driesse_2023
diff --git a/docs/sphinx/source/whatsnew/v0.10.3.rst b/docs/sphinx/source/whatsnew/v0.10.3.rst
index 007eb8d34b..63f7b90b84 100644
--- a/docs/sphinx/source/whatsnew/v0.10.3.rst
+++ b/docs/sphinx/source/whatsnew/v0.10.3.rst
@@ -9,6 +9,9 @@ Enhancements
 ~~~~~~~~~~~~
 * Added the continuous Perez-Driesse transposition model.
   :py:func:`pvlib.irradiance.perez_driesse` (:issue:`1841`, :pull:`1876`)
+* Added a reverse transposition algorithm using the Perez-Driesse model.
+  :py:func:`pvlib.irradiance.ghi_from_poa_driesse_2023`
+  (:issue:`1901`, :pull:`1907`)
 * :py:func:`pvlib.bifacial.infinite_sheds.get_irradiance` and
   :py:func:`pvlib.bifacial.infinite_sheds.get_irradiance_poa` now include
   shaded fraction in returned variables. (:pull:`1871`)
@@ -27,8 +30,10 @@ Documentation
 ~~~~~~~~~~~~~
 * Create :ref:`weatherdata` User's Guide page. (:pull:`1754`)
 * Fixed a plotting issue in the IV curve gallery example (:pull:`1895`)
+* Added two examples to demonstrate reverse transposition (:pull:`1907`)
 * Fixed `detect_clearsky` example in `clearsky.rst` (:issue:`1914`)
 
+
 Requirements
 ~~~~~~~~~~~~
 * Minimum version of scipy advanced from 1.4.0 to 1.5.0. (:issue:`1918`, :pull:`1919`)
diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py
index acccf97ba5..174a88e6a0 100644
--- a/pvlib/irradiance.py
+++ b/pvlib/irradiance.py
@@ -11,6 +11,7 @@
 import numpy as np
 import pandas as pd
 from scipy.interpolate import splev
+from scipy.optimize import bisect
 
 from pvlib import atmosphere, solarposition, tools
 import pvlib  # used to avoid dni name collision in complete_irradiance
@@ -1381,9 +1382,9 @@ def perez_driesse(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
 
     References
     ----------
-    .. [1] A. Driesse, A. Jensen, R. Perez, A Continuous Form of the Perez
-        Diffuse Sky Model for Forward and Reverse Transposition, accepted
-        for publication in the Solar Energy Journal.
+    .. [1] Driesse, A., Jensen, A., Perez, R., 2024. A Continuous form of the
+        Perez diffuse sky model for forward and reverse transposition.
+        Solar Energy vol. 267. :doi:`10.1016/j.solener.2023.112093`
 
     .. [2] Perez, R., Ineichen, P., Seals, R., Michalsky, J., Stewart, R.,
        1990. Modeling daylight availability and irradiance components from
@@ -1445,6 +1446,170 @@ def perez_driesse(surface_tilt, surface_azimuth, dhi, dni, dni_extra,
         return sky_diffuse
 
 
+def _poa_from_ghi(surface_tilt, surface_azimuth,
+                  solar_zenith, solar_azimuth,
+                  ghi,
+                  dni_extra, airmass, albedo):
+    '''
+    Transposition function that includes decomposition of GHI using the
+    continuous Erbs-Driesse model.
+
+    Helper function for ghi_from_poa_driesse_2023.
+    '''
+    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Nov., 2023
+
+    erbsout = erbs_driesse(ghi, solar_zenith, dni_extra=dni_extra)
+
+    dni = erbsout['dni']
+    dhi = erbsout['dhi']
+
+    irrads = get_total_irradiance(surface_tilt, surface_azimuth,
+                                  solar_zenith, solar_azimuth,
+                                  dni, ghi, dhi,
+                                  dni_extra, airmass, albedo,
+                                  model='perez-driesse')
+
+    return irrads['poa_global']
+
+
+def _ghi_from_poa(surface_tilt, surface_azimuth,
+                  solar_zenith, solar_azimuth,
+                  poa_global,
+                  dni_extra, airmass, albedo,
+                  xtol=0.01):
+    '''
+    Reverse transposition function that uses the scalar bisection from scipy.
+
+    Helper function for ghi_from_poa_driesse_2023.
+    '''
+    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Nov., 2023
+
+    # propagate nans and zeros quickly
+    if np.isnan(poa_global):
+        return np.nan, False, 0
+    if poa_global <= 0:
+        return 0.0, True, 0
+
+    # function whose root needs to be found
+    def poa_error(ghi):
+        poa_hat = _poa_from_ghi(surface_tilt, surface_azimuth,
+                                solar_zenith, solar_azimuth,
+                                ghi,
+                                dni_extra, airmass, albedo)
+        return poa_hat - poa_global
+
+    # calculate an upper bound for ghi using clearness index 1.25
+    ghi_clear = dni_extra * tools.cosd(solar_zenith)
+    ghi_high = np.maximum(10, 1.25 * ghi_clear)
+
+    try:
+        result = bisect(poa_error,
+                        a=0,
+                        b=ghi_high,
+                        xtol=xtol,
+                        maxiter=25,
+                        full_output=True,
+                        disp=False,
+                        )
+    except ValueError:
+        # this occurs when poa_error has the same sign at both end points
+        ghi = np.nan
+        conv = False
+        niter = -1
+    else:
+        ghi = result[0]
+        conv = result[1].converged
+        niter = result[1].iterations
+
+    return ghi, conv, niter
+
+
+def ghi_from_poa_driesse_2023(surface_tilt, surface_azimuth,
+                              solar_zenith, solar_azimuth,
+                              poa_global,
+                              dni_extra=None, airmass=None, albedo=0.25,
+                              xtol=0.01,
+                              full_output=False):
+    '''
+    Estimate global horizontal irradiance (GHI) from global plane-of-array
+    (POA) irradiance.  This reverse transposition algorithm uses a bisection
+    search together with the continuous Perez-Driesse transposition and
+    continuous Erbs-Driesse decomposition models, as described in [1]_.
+
+    Parameters
+    ----------
+    surface_tilt : numeric
+        Panel tilt from horizontal. [degree]
+    surface_azimuth : numeric
+        Panel azimuth from north. [degree]
+    solar_zenith : numeric
+        Solar zenith angle. [degree]
+    solar_azimuth : numeric
+        Solar azimuth angle. [degree]
+    poa_global : numeric
+        Plane-of-array global irradiance, aka global tilted irradiance. [W/m^2]
+    dni_extra : None or numeric, default None
+        Extraterrestrial direct normal irradiance. [W/m^2]
+    airmass : None or numeric, default None
+        Relative airmass (not adjusted for pressure). [unitless]
+    albedo : numeric, default 0.25
+        Ground surface albedo. [unitless]
+    xtol : numeric, default 0.01
+        Convergence criterion.  The estimated GHI will be within xtol of the
+        true value. [W/m^2]
+    full_output : boolean, default False
+        If full_output is False, only ghi is returned, otherwise the return
+        value is (ghi, converged, niter). (see Returns section for details).
+
+    Returns
+    -------
+    ghi : numeric
+        Estimated GHI. [W/m^2]
+    converged : boolean, optional
+        Present if full_output=True. Indicates which elements converged
+        successfully.
+    niter : integer, optional
+        Present if full_output=True. Indicates how many bisection iterations
+        were done.
+
+    Notes
+    -----
+    Since :py:func:`scipy.optimize.bisect` is not vectorized, high-resolution
+    time series can be quite slow to process.
+
+    References
+    ----------
+    .. [1] Driesse, A., Jensen, A., Perez, R., 2024. A Continuous form of the
+        Perez diffuse sky model for forward and reverse transposition.
+        Solar Energy vol. 267. :doi:`10.1016/j.solener.2023.112093`
+
+    See also
+    --------
+    perez_driesse
+    erbs_driesse
+    gti_dirint
+    '''
+    # Contributed by Anton Driesse (@adriesse), PV Performance Labs. Nov., 2023
+
+    ghi_from_poa_array = np.vectorize(_ghi_from_poa)
+
+    ghi, conv, niter = ghi_from_poa_array(surface_tilt, surface_azimuth,
+                                          solar_zenith, solar_azimuth,
+                                          poa_global,
+                                          dni_extra, airmass, albedo,
+                                          xtol=0.01)
+
+    if isinstance(poa_global, pd.Series):
+        ghi = pd.Series(ghi, poa_global.index)
+        conv = pd.Series(conv, poa_global.index)
+        niter = pd.Series(niter, poa_global.index)
+
+    if full_output:
+        return ghi, conv, niter
+    else:
+        return ghi
+
+
 def clearsky_index(ghi, clearsky_ghi, max_clearsky_index=2.0):
     """
     Calculate the clearsky index.
@@ -2568,8 +2733,9 @@ def erbs_driesse(ghi, zenith, datetime_or_doy=None, dni_extra=None,
 
     References
     ----------
-    .. [1] A. Driesse, A. Jensen, R. Perez, A Continuous Form of the Perez
-        Diffuse Sky Model for Forward and Reverse Transposition, forthcoming.
+    .. [1] Driesse, A., Jensen, A., Perez, R., 2024. A Continuous form of the
+        Perez diffuse sky model for forward and reverse transposition.
+        Solar Energy vol. 267. :doi:`10.1016/j.solener.2023.112093`
 
     .. [2] D. G. Erbs, S. A. Klein and J. A. Duffie, Estimation of the
        diffuse radiation fraction for hourly, daily and monthly-average
diff --git a/pvlib/tests/test_irradiance.py b/pvlib/tests/test_irradiance.py
index ba66f4dc36..55fef490ba 100644
--- a/pvlib/tests/test_irradiance.py
+++ b/pvlib/tests/test_irradiance.py
@@ -782,6 +782,50 @@ def test_dirint_min_cos_zenith_max_zenith():
     assert_series_equal(out, expected, check_less_precise=True)
 
 
+def test_ghi_from_poa_driesse():
+    # inputs copied from test_gti_dirint
+    times = pd.DatetimeIndex(
+        ['2014-06-24T06-0700', '2014-06-24T09-0700', '2014-06-24T12-0700'])
+    poa_global = np.array([20, 300, 1000])
+    zenith = np.array([80, 45, 20])
+    azimuth = np.array([90, 135, 180])
+    surface_tilt = 30
+    surface_azimuth = 180
+
+    # test core function
+    output = irradiance.ghi_from_poa_driesse_2023(
+        surface_tilt, surface_azimuth, zenith, azimuth,
+        poa_global, dni_extra=1366.1)
+
+    expected = [22.089, 304.088, 931.143]
+    assert_allclose(expected, output, atol=0.001)
+
+    # test series output
+    poa_global = pd.Series([20, 300, 1000], index=times)
+
+    output = irradiance.ghi_from_poa_driesse_2023(
+        surface_tilt, surface_azimuth, zenith, azimuth,
+        poa_global, dni_extra=1366.1)
+
+    assert isinstance(output, pd.Series)
+
+    # test full_output option and special cases
+    poa_global = np.array([0, 1500, np.nan])
+
+    ghi, conv, niter = irradiance.ghi_from_poa_driesse_2023(
+        surface_tilt, surface_azimuth, zenith, azimuth,
+        poa_global, dni_extra=1366.1, full_output=True)
+
+    expected = [0, np.nan, np.nan]
+    assert_allclose(expected, ghi, atol=0.001)
+
+    expected = [True, False, False]
+    assert_allclose(expected, conv)
+
+    expected = [0, -1, 0]
+    assert_allclose(expected, niter)
+
+
 def test_gti_dirint():
     times = pd.DatetimeIndex(
         ['2014-06-24T06-0700', '2014-06-24T09-0700', '2014-06-24T12-0700'])

From f430a745528d5d645024bb6b4b6d82ee350043ad Mon Sep 17 00:00:00 2001
From: GillesFischerV <147727952+GillesFischerV@users.noreply.github.com>
Date: Tue, 12 Dec 2023 20:29:34 +0100
Subject: [PATCH 4/6] Fix CAMS message error handler (#1905)

* Fix CAMS message error handler - Issue#1799

* Fix CAMS message error handler - Issue#1799

* fix Python Flake8 Linter error

* Fix associated UT and add contribution in WhatsNews

* Fix line code length in test_sodapro.py

* Fix typo in contributor ghuser

* Update geographical coverage description

* correction in maximum longitude available for CAMS Radiation

* revert of last change

* Add doc string changes from adriesse

Co-authored-by: Anton Driesse <anton.driesse@pvperformancelabs.com>

* Flake8 correction

---------

Co-authored-by: Adam R. Jensen <39184289+AdamRJensen@users.noreply.github.com>
Co-authored-by: Anton Driesse <anton.driesse@pvperformancelabs.com>
---
 docs/sphinx/source/whatsnew/v0.10.3.rst |  5 ++++-
 pvlib/iotools/sodapro.py                | 21 ++++++++++++++-------
 pvlib/tests/iotools/test_sodapro.py     |  9 +++++----
 3 files changed, 23 insertions(+), 12 deletions(-)

diff --git a/docs/sphinx/source/whatsnew/v0.10.3.rst b/docs/sphinx/source/whatsnew/v0.10.3.rst
index 63f7b90b84..2a466cdb4b 100644
--- a/docs/sphinx/source/whatsnew/v0.10.3.rst
+++ b/docs/sphinx/source/whatsnew/v0.10.3.rst
@@ -18,6 +18,8 @@ Enhancements
 
 Bug fixes
 ~~~~~~~~~
+* Fixed CAMS error message handler in
+  :py:func:`pvlib.iotools.get_cams` (:issue:`1799`, :pull:`1905`)
 * Fix mapping of the dew point column to ``temp_dew`` when ``map_variables``
   is True in :py:func:`pvlib.iotools.get_psm3`. (:pull:`1920`)
 
@@ -45,7 +47,8 @@ Contributors
 * Miguel Sánchez de León Peque (:ghuser:`Peque`)
 * Will Hobbs (:ghuser:`williamhobbs`)
 * Anton Driesse (:ghuser:`adriesse`)
+* Gilles Fischer (:ghuser:`GillesFischerV`)
+* Adam R. Jensen (:ghusuer:`AdamRJensen`)
 * :ghuser:`matsuobasho`
 * Harry Jack (:ghuser:`harry-solcast`)
-* Adam R. Jensen (:ghuser:`AdamRJensen`)
 * Kevin Anderson (:ghuser:`kandersolar`)
diff --git a/pvlib/iotools/sodapro.py b/pvlib/iotools/sodapro.py
index a6c43ad341..b9922af4b8 100644
--- a/pvlib/iotools/sodapro.py
+++ b/pvlib/iotools/sodapro.py
@@ -57,8 +57,10 @@ def get_cams(latitude, longitude, start, end, email, identifier='mcclear',
     Access: free, but requires registration, see [2]_
 
     Requests: max. 100 per day
+
     Geographical coverage: worldwide for CAMS McClear and approximately -66° to
-    66° in both latitude and longitude for CAMS Radiation.
+    66° in latitude and -66° to 180° in longitude for CAMS Radiation. See [3]_
+    for a map of the geographical coverage.
 
     Parameters
     ----------
@@ -157,6 +159,9 @@ def get_cams(latitude, longitude, start, end, email, identifier='mcclear',
        <https://atmosphere.copernicus.eu/solar-radiation>`_
     .. [2] `CAMS Radiation Automatic Access (SoDa)
        <https://www.soda-pro.com/help/cams-services/cams-radiation-service/automatic-access>`_
+    .. [3] A. R. Jensen et al., pvlib iotools — Open-source Python functions
+       for seamless access to solar irradiance data. Solar Energy. 2023. Vol
+       266, pp. 112092. :doi:`10.1016/j.solener.2023.112092`
     """
     try:
         time_step_str = TIME_STEPS_MAP[time_step]
@@ -215,14 +220,16 @@ def get_cams(latitude, longitude, start, end, email, identifier='mcclear',
     res = requests.get(base_url + '?DataInputs=' + data_inputs, params=params,
                        timeout=timeout)
 
-    # Invalid requests returns an XML error message and the HTTP staus code 200
-    # as if the request was successful. Therefore, errors cannot be handled
-    # automatic (e.g. res.raise_for_status()) and errors are handled manually
-    if res.headers['Content-Type'] == 'application/xml':
+    # Response from CAMS follows the status and reason format of PyWPS4
+    # If an error occurs on server side, it will return error 400 - bad request
+    # Additional information is available in the response text, so it is added
+    # to the error displayed to facilitate users effort to fix their request
+    if not res.ok:
         errors = res.text.split('ows:ExceptionText')[1][1:-2]
-        raise requests.HTTPError(errors, response=res)
+        res.reason = "%s: <%s>" % (res.reason, errors)
+        res.raise_for_status()
     # Successful requests returns a csv data file
-    elif res.headers['Content-Type'] == 'application/csv':
+    else:
         fbuf = io.StringIO(res.content.decode('utf-8'))
         data, metadata = parse_cams(fbuf, integrated=integrated, label=label,
                                     map_variables=map_variables)
diff --git a/pvlib/tests/iotools/test_sodapro.py b/pvlib/tests/iotools/test_sodapro.py
index ff17691a98..4729cf2c64 100644
--- a/pvlib/tests/iotools/test_sodapro.py
+++ b/pvlib/tests/iotools/test_sodapro.py
@@ -248,7 +248,7 @@ def test_get_cams_bad_request(requests_mock):
     requests inputs. Also tests if the specified server url gets used"""
 
     # Subset of an xml file returned for errornous requests
-    mock_response_bad = """<?xml version="1.0" encoding="utf-8"?>
+    mock_response_bad_text = """<?xml version="1.0" encoding="utf-8"?>
     <ows:Exception exceptionCode="NoApplicableCode" locator="None">
     <ows:ExceptionText>Failed to execute WPS process [get_mcclear]:
         Please, register yourself at www.soda-pro.com
@@ -256,12 +256,13 @@ def test_get_cams_bad_request(requests_mock):
 
     url_cams_bad_request = 'https://pro.soda-is.com/service/wps?DataInputs=latitude=55.7906;longitude=12.5251;altitude=-999;date_begin=2020-01-01;date_end=2020-05-04;time_ref=TST;summarization=PT01H;username=test%2540test.com;verbose=false&Service=WPS&Request=Execute&Identifier=get_mcclear&version=1.0.0&RawDataOutput=irradiation'  # noqa: E501
 
-    requests_mock.get(url_cams_bad_request, text=mock_response_bad,
-                      headers={'Content-Type': 'application/xml'})
+    requests_mock.get(url_cams_bad_request, status_code=400,
+                      text=mock_response_bad_text)
 
     # Test if HTTPError is raised if incorrect input is specified
     # In the below example a non-registrered email is specified
-    with pytest.raises(requests.HTTPError, match='Failed to execute WPS'):
+    with pytest.raises(requests.exceptions.HTTPError,
+                       match='Failed to execute WPS process'):
         _ = sodapro.get_cams(
             start=pd.Timestamp('2020-01-01'),
             end=pd.Timestamp('2020-05-04'),

From 304fbb5e8fa3db29d065b792c5a9c5cd56a5842f Mon Sep 17 00:00:00 2001
From: Kevin Anderson <kevin.anderso@gmail.com>
Date: Tue, 12 Dec 2023 14:30:57 -0500
Subject: [PATCH 5/6] Add python 3.12 to CI (#1886)

* Create requirements-py3.12.yml

* add 3.12 to GH Actions test workflows

* whatsnew

* nix solarfactors

* fix yaml syntax

* increase min scipy to 1.5; update comments
---
 .github/workflows/pytest-remote-data.yml |  2 +-
 .github/workflows/pytest.yml             |  2 +-
 ci/requirements-py3.12.yml               | 28 ++++++++++++++++++++++++
 docs/sphinx/source/whatsnew/v0.10.3.rst  |  2 +-
 4 files changed, 31 insertions(+), 3 deletions(-)
 create mode 100644 ci/requirements-py3.12.yml

diff --git a/.github/workflows/pytest-remote-data.yml b/.github/workflows/pytest-remote-data.yml
index 7e32486918..0603f632da 100644
--- a/.github/workflows/pytest-remote-data.yml
+++ b/.github/workflows/pytest-remote-data.yml
@@ -56,7 +56,7 @@ jobs:
     strategy:
       fail-fast: false  # don't cancel other matrix jobs when one fails
       matrix:
-        python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
+        python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"]
         suffix: ['']  # the alternative to "-min"
         include:
           - python-version: 3.7
diff --git a/.github/workflows/pytest.yml b/.github/workflows/pytest.yml
index 5185734e72..0b618d9f92 100644
--- a/.github/workflows/pytest.yml
+++ b/.github/workflows/pytest.yml
@@ -12,7 +12,7 @@ jobs:
       fail-fast: false  # don't cancel other matrix jobs when one fails
       matrix:
         os: [ubuntu-latest, macos-latest, windows-latest]
-        python-version: [3.7, 3.8, 3.9, "3.10", "3.11"]
+        python-version: [3.7, 3.8, 3.9, "3.10", "3.11", "3.12"]
         environment-type: [conda, bare]
         suffix: ['']  # placeholder as an alternative to "-min"
         include:
diff --git a/ci/requirements-py3.12.yml b/ci/requirements-py3.12.yml
new file mode 100644
index 0000000000..69632b3ac1
--- /dev/null
+++ b/ci/requirements-py3.12.yml
@@ -0,0 +1,28 @@
+name: test_env
+channels:
+    - defaults
+    - conda-forge
+dependencies:
+    - coveralls
+    - cython
+    - ephem
+    - h5py
+    # - numba  # not available for 3.12 as of 2023-12-12
+    - numpy >= 1.16.0
+    - pandas >= 0.25.0
+    - pip
+    - pytest
+    - pytest-cov
+    - pytest-mock
+    - requests-mock
+    - pytest-timeout
+    - pytest-rerunfailures
+    - pytest-remotedata
+    - python=3.12
+    - pytz
+    - requests
+    - scipy >= 1.5.0
+    - statsmodels
+    # - pip:
+        # - nrel-pysam>=2.0  # not available for 3.12 as of 2023-12-12
+        # - solarfactors  # required shapely<2 isn't available for 3.12
diff --git a/docs/sphinx/source/whatsnew/v0.10.3.rst b/docs/sphinx/source/whatsnew/v0.10.3.rst
index 2a466cdb4b..c8cfaaf86b 100644
--- a/docs/sphinx/source/whatsnew/v0.10.3.rst
+++ b/docs/sphinx/source/whatsnew/v0.10.3.rst
@@ -26,7 +26,7 @@ Bug fixes
 Testing
 ~~~~~~~
 * Replace use of deprecated ``pkg_resources``. (:issue:`1881`, :pull:`1882`)
-
+* Added Python 3.12 to test suite. (:pull:`1886`)
 
 Documentation
 ~~~~~~~~~~~~~

From ae848177c7cafae78e4a28a7f9871c29ac484f92 Mon Sep 17 00:00:00 2001
From: Cliff Hansen <cwhanse@sandia.gov>
Date: Thu, 14 Dec 2023 11:06:07 -0700
Subject: [PATCH 6/6] Fix and document clearsky_model for ModelChain (#1924)

* add note about clearsky_model

* whatsnew

* pass clearsky_model to get_clearsky

* add bug fix note

* test for correct argument

* test for correct argument

* test for correct argument
---
 docs/sphinx/source/whatsnew/v0.10.3.rst | 4 ++++
 pvlib/modelchain.py                     | 6 ++++--
 pvlib/tests/test_modelchain.py          | 6 +++++-
 3 files changed, 13 insertions(+), 3 deletions(-)

diff --git a/docs/sphinx/source/whatsnew/v0.10.3.rst b/docs/sphinx/source/whatsnew/v0.10.3.rst
index c8cfaaf86b..4d0cc774c8 100644
--- a/docs/sphinx/source/whatsnew/v0.10.3.rst
+++ b/docs/sphinx/source/whatsnew/v0.10.3.rst
@@ -22,6 +22,8 @@ Bug fixes
   :py:func:`pvlib.iotools.get_cams` (:issue:`1799`, :pull:`1905`)
 * Fix mapping of the dew point column to ``temp_dew`` when ``map_variables``
   is True in :py:func:`pvlib.iotools.get_psm3`. (:pull:`1920`)
+* Fix :py:class:`pvlib.modelchain.ModelChain` to use attribute `clearsky_model`
+  (:pull:`1924`)
 
 Testing
 ~~~~~~~
@@ -34,6 +36,7 @@ Documentation
 * Fixed a plotting issue in the IV curve gallery example (:pull:`1895`)
 * Added two examples to demonstrate reverse transposition (:pull:`1907`)
 * Fixed `detect_clearsky` example in `clearsky.rst` (:issue:`1914`)
+* Clarified purpose of `ModelChain.clearsky_model` (:pull:`1924`)
 
 
 Requirements
@@ -52,3 +55,4 @@ Contributors
 * :ghuser:`matsuobasho`
 * Harry Jack (:ghuser:`harry-solcast`)
 * Kevin Anderson (:ghuser:`kandersolar`)
+* Cliff Hansen (:ghuser:`cwhanse`)
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
index cc42d59077..296356971b 100644
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -460,7 +460,8 @@ class ModelChain:
         the physical location at which to evaluate the model.
 
     clearsky_model : str, default 'ineichen'
-        Passed to location.get_clearsky.
+        Passed to location.get_clearsky. Only used when DNI is not found in
+        the weather inputs.
 
     transposition_model : str, default 'haydavies'
         Passed to system.get_irradiance.
@@ -1354,7 +1355,8 @@ def _complete_irradiance(self, weather):
                    "https://github.com/pvlib/pvlib-python \n")
         if {'ghi', 'dhi'} <= icolumns and 'dni' not in icolumns:
             clearsky = self.location.get_clearsky(
-                weather.index, solar_position=self.results.solar_position)
+                weather.index, model=self.clearsky_model,
+                solar_position=self.results.solar_position)
             complete_irrad_df = pvlib.irradiance.complete_irradiance(
                 solar_zenith=self.results.solar_position.zenith,
                 ghi=weather.ghi,
diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py
index c59a02e9c4..0632d34212 100644
--- a/pvlib/tests/test_modelchain.py
+++ b/pvlib/tests/test_modelchain.py
@@ -1847,7 +1847,7 @@ def test_complete_irradiance_clean_run(sapm_dc_snl_ac_system, location):
                         pd.Series([9, 5], index=times, name='ghi'))
 
 
-def test_complete_irradiance(sapm_dc_snl_ac_system, location):
+def test_complete_irradiance(sapm_dc_snl_ac_system, location, mocker):
     """Check calculations"""
     mc = ModelChain(sapm_dc_snl_ac_system, location)
     times = pd.date_range('2010-07-05 7:00:00-0700', periods=2, freq='H')
@@ -1867,7 +1867,11 @@ def test_complete_irradiance(sapm_dc_snl_ac_system, location):
                         pd.Series([372.103976116, 497.087579068],
                                   index=times, name='ghi'))
 
+    # check that clearsky_model is used correctly
+    m_ineichen = mocker.spy(location, 'get_clearsky')
     mc.complete_irradiance(i[['dhi', 'ghi']])
+    assert m_ineichen.call_count == 1
+    assert m_ineichen.call_args[1]['model'] == 'ineichen'
     assert_series_equal(mc.results.weather['dni'],
                         pd.Series([49.756966, 62.153947],
                                   index=times, name='dni'))