Repository: pvlib/pvlib-python
Pull request #1103 (2020-12-08 20:25:19)

Add missing modules to init.py
 - ~Closes #xxxx~
 - [x] I am familiar with the [contributing guidelines](https://pvlib-python.readthedocs.io/en/latest/contributing.html)
 - ~Tests added~
 - ~Updates entries to [`docs/sphinx/source/api.rst`](https://github.com/pvlib/pvlib-python/blob/master/docs/sphinx/source/api.rst) for API changes.~
 - [x] Adds description and name entries in the appropriate "what's new" file in [`docs/sphinx/source/whatsnew`](https://github.com/pvlib/pvlib-python/tree/master/docs/sphinx/source/whatsnew) for all changes. Includes link to the GitHub Issue with `` :issue:`num` `` or this Pull Request with `` :pull:`num` ``. Includes contributor name and/or GitHub username (link with `` :ghuser:`user` ``).
 - ~New code is fully documented. Includes [numpydoc](https://numpydoc.readthedocs.io/en/latest/format.html) compliant docstrings, examples, and comments where necessary.~
 - [x] Pull request is nearly complete and ready for detailed review.
 - [x] Maintainer: Appropriate GitHub Labels and Milestone are assigned to the Pull Request and linked Issue.

`__init__.py` is missing several modules.  Some of the omissions don't make a practical difference, maybe because they get imported by other modules.   For `shading` and `scaling`, some import patterns fail:

```
$ python -c 'from pvlib import shading'  # works; this is how the test suite does it
$ python -c 'import pvlib; pvlib.shading'
Traceback (most recent call last):
  File "<string>", line 1, in <module>
AttributeError: module 'pvlib' has no attribute 'shading'
```

I think I got all the missing modules but another pair of eyes never hurts. 

wholmgren: Ah good catch @kanderso-nrel. Putting the imports in alphabetical order would make it easier to catch missing modules. While you're at it, I think this pattern might be more concise:

```python
from pvlib import (  # noqa: F401
    shading,
    snow,
)
```

Patch:
diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst
index 9d79aab..1306ed7 100644
--- a/docs/sphinx/source/api.rst
+++ b/docs/sphinx/source/api.rst
@@ -181,7 +181,7 @@ DNI estimation models
    irradiance.dirint
    irradiance.dirindex
    irradiance.erbs
-   irradiance.liujordan
+   irradiance.campbell_norman
    irradiance.gti_dirint
 
 Clearness index models
@@ -520,7 +520,7 @@ Processing data
    forecast.ForecastModel.cloud_cover_to_ghi_linear
    forecast.ForecastModel.cloud_cover_to_irradiance_clearsky_scaling
    forecast.ForecastModel.cloud_cover_to_transmittance_linear
-   forecast.ForecastModel.cloud_cover_to_irradiance_liujordan
+   forecast.ForecastModel.cloud_cover_to_irradiance_campbell_norman
    forecast.ForecastModel.cloud_cover_to_irradiance
    forecast.ForecastModel.kelvin_to_celsius
    forecast.ForecastModel.isobaric_to_ambient_temperature
diff --git a/docs/sphinx/source/whatsnew/v0.8.1.rst b/docs/sphinx/source/whatsnew/v0.8.1.rst
index f7b4ec1..abd8164 100644
--- a/docs/sphinx/source/whatsnew/v0.8.1.rst
+++ b/docs/sphinx/source/whatsnew/v0.8.1.rst
@@ -9,7 +9,7 @@ Breaking changes
 
 Deprecations
 ~~~~~~~~~~~~
-
+* ``pvlib.irradiance.liujordan`` is deprecated.
 
 Enhancements
 ~~~~~~~~~~~~
@@ -21,6 +21,12 @@ Enhancements
   multiple MPPTs (:issue:`457`, :pull:`1085`)
 * Added optional ``attributes`` parameter to :py:func:`pvlib.iotools.get_psm3`
   and added the option of fetching 5- and 15-minute PSM3 data. (:pull:`1086`)
+* Added :py:func:`pvlib.irradiance.campbell_norman` for estimating DNI, DHI and GHI
+  from extraterrestrial irradiance. This function replaces ``pvlib.irradiance.liujordan``;
+  users of ``pvlib.irradiance.liujordan`` should note that :py:func:`pvlib.irradiance.campbell_norman`
+  expects different parameters.
+* :py:meth:`pvlib.forecast.Forecast.cloud_cover_to_irradiance_campbell_norman`
+  replaces ``pvlib.forecast.Forecast.cloud_cover_to_irradiance_liujordan``.
 
 Bug fixes
 ~~~~~~~~~
@@ -28,6 +34,8 @@ Bug fixes
   inputs. (:issue:`1071`, :pull:`1072`)
 * Raise ``ValueError`` from  :py:meth:`pvlib.modelchain.ModelChain.prepare_inputs`
   when input does not have a 'dhi' column. (:issue:`1092`, :pull:`1093`)
+* Add missing modules (including ``shading`` and ``scaling``) to ``__init__.py``.
+  (:pull:`1103`)
 
 Testing
 ~~~~~~~
diff --git a/pvlib/__init__.py b/pvlib/__init__.py
index c2d645d..55ffa51 100644
--- a/pvlib/__init__.py
+++ b/pvlib/__init__.py
@@ -1,18 +1,26 @@
 from pvlib.version import __version__  # noqa: F401
-from pvlib import tools  # noqa: F401
-from pvlib import atmosphere  # noqa: F401
-from pvlib import clearsky  # noqa: F401
-# from pvlib import forecast  # noqa: F401
-from pvlib import irradiance  # noqa: F401
-from pvlib import location  # noqa: F401
-from pvlib import solarposition  # noqa: F401
-from pvlib import iotools  # noqa: F401
-from pvlib import ivtools  # noqa: F401
-from pvlib import tracking  # noqa: F401
-from pvlib import pvsystem  # noqa: F401
-from pvlib import spa  # noqa: F401
-from pvlib import modelchain  # noqa: F401
-from pvlib import singlediode  # noqa: F401
-from pvlib import bifacial  # noqa: F401
-from pvlib import soiling  # noqa: F401
-from pvlib import snow  # noqa: F401
+
+from pvlib import (  # noqa: F401
+    atmosphere,
+    bifacial,
+    clearsky,
+    # forecast
+    iam,
+    inverter,
+    iotools,
+    irradiance,
+    ivtools,
+    location,
+    modelchain,
+    pvsystem,
+    scaling,
+    shading,
+    singlediode,
+    snow,
+    soiling,
+    solarposition,
+    spa,
+    temperature,
+    tools,
+    tracking,
+)
diff --git a/pvlib/forecast.py b/pvlib/forecast.py
index 24e54e1..4d4500c 100644
--- a/pvlib/forecast.py
+++ b/pvlib/forecast.py
@@ -9,11 +9,14 @@ from requests.exceptions import HTTPError
 from xml.etree.ElementTree import ParseError
 
 from pvlib.location import Location
-from pvlib.irradiance import liujordan, get_extra_radiation, disc
+from pvlib.irradiance import campbell_norman, get_extra_radiation, disc
+from pvlib.irradiance import _liujordan
 from siphon.catalog import TDSCatalog
 from siphon.ncss import NCSS
 
 import warnings
+from pvlib._deprecation import deprecated
+
 
 warnings.warn(
     'The forecast module algorithms and features are highly experimental. '
@@ -526,8 +529,48 @@ class ForecastModel:
 
         return transmittance
 
+    def cloud_cover_to_irradiance_campbell_norman(self, cloud_cover, **kwargs):
+        """
+        Estimates irradiance from cloud cover in the following steps:
+
+        1. Determine transmittance using a function of cloud cover e.g.
+           :py:meth:`~ForecastModel.cloud_cover_to_transmittance_linear`
+        2. Calculate GHI, DNI, DHI using the
+           :py:func:`pvlib.irradiance.campbell_norman` model
+
+        Parameters
+        ----------
+        cloud_cover : Series
+
+        Returns
+        -------
+        irradiance : DataFrame
+            Columns include ghi, dni, dhi
+        """
+        # in principle, get_solarposition could use the forecast
+        # pressure, temp, etc., but the cloud cover forecast is not
+        # accurate enough to justify using these minor corrections
+        solar_position = self.location.get_solarposition(cloud_cover.index)
+        dni_extra = get_extra_radiation(cloud_cover.index)
+
+        transmittance = self.cloud_cover_to_transmittance_linear(cloud_cover,
+                                                                 **kwargs)
+
+        irrads = campbell_norman(solar_position['apparent_zenith'],
+                                 transmittance, dni_extra=dni_extra)
+        irrads = irrads.fillna(0)
+
+        return irrads
+
+    @deprecated(
+        '0.8',
+        alternative='Forecast.cloud_cover_to_irradiance_campbell_norman',
+        name='Forecast.cloud_cover_to_irradiance_liujordan',
+        removal='0.9')
     def cloud_cover_to_irradiance_liujordan(self, cloud_cover, **kwargs):
         """
+        Deprecated. Use cloud_cover_to_irradiance_campbell_norman instead.
+
         Estimates irradiance from cloud cover in the following steps:
 
         1. Determine transmittance using a function of cloud cover e.g.
@@ -554,9 +597,9 @@ class ForecastModel:
         transmittance = self.cloud_cover_to_transmittance_linear(cloud_cover,
                                                                  **kwargs)
 
-        irrads = liujordan(solar_position['apparent_zenith'],
-                           transmittance, airmass['airmass_absolute'],
-                           dni_extra=dni_extra)
+        irrads = _liujordan(solar_position['apparent_zenith'],
+                            transmittance, airmass['airmass_absolute'],
+                            dni_extra=dni_extra)
         irrads = irrads.fillna(0)
 
         return irrads
@@ -571,7 +614,8 @@ class ForecastModel:
         cloud_cover : Series
         how : str, default 'clearsky_scaling'
             Selects the method for conversion. Can be one of
-            clearsky_scaling or liujordan.
+            clearsky_scaling or campbell_norman. Method liujordan is
+            deprecated.
         **kwargs
             Passed to the selected method.
 
@@ -585,6 +629,9 @@ class ForecastModel:
         if how == 'clearsky_scaling':
             irrads = self.cloud_cover_to_irradiance_clearsky_scaling(
                 cloud_cover, **kwargs)
+        elif how == 'campbell_norman':
+            irrads = self.cloud_cover_to_irradiance_campbell_norman(
+                cloud_cover, **kwargs)
         elif how == 'liujordan':
             irrads = self.cloud_cover_to_irradiance_liujordan(
                 cloud_cover, **kwargs)
diff --git a/pvlib/irradiance.py b/pvlib/irradiance.py
index e1d0769..67a327a 100644
--- a/pvlib/irradiance.py
+++ b/pvlib/irradiance.py
@@ -13,6 +13,9 @@ import pandas as pd
 
 from pvlib import atmosphere, solarposition, tools
 
+from pvlib._deprecation import deprecated
+
+
 # see References section of grounddiffuse function
 SURFACE_ALBEDOS = {'urban': 0.18,
                    'grass': 0.20,
@@ -2184,7 +2187,61 @@ def erbs(ghi, zenith, datetime_or_doy, min_cos_zenith=0.065, max_zenith=87):
     return data
 
 
-def liujordan(zenith, transmittance, airmass, dni_extra=1367.0):
+def campbell_norman(zenith, transmittance, pressure=101325.0,
+                    dni_extra=1367.0):
+    '''
+    Determine DNI, DHI, GHI from extraterrestrial flux, transmittance,
+    and atmospheric pressure.
+
+    Parameters
+    ----------
+    zenith: pd.Series
+        True (not refraction-corrected) zenith angles in decimal
+        degrees. If Z is a vector it must be of the same size as all
+        other vector inputs. Z must be >=0 and <=180.
+
+    transmittance: float
+        Atmospheric transmittance between 0 and 1.
+
+    pressure: float, default 101325.0
+        Air pressure
+
+    dni_extra: float, default 1367.0
+        Direct irradiance incident at the top of the atmosphere.
+
+    Returns
+    -------
+    irradiance: DataFrame
+        Modeled direct normal irradiance, direct horizontal irradiance,
+        and global horizontal irradiance in W/m^2
+
+    References
+    ----------
+    .. [1] Campbell, G. S., J. M. Norman (1998) An Introduction to
+       Environmental Biophysics. 2nd Ed. New York: Springer.
+    '''
+
+    tau = transmittance
+
+    airmass = atmosphere.get_relative_airmass(zenith, model='simple')
+    airmass = atmosphere.get_absolute_airmass(airmass, pressure=pressure)
+    dni = dni_extra*tau**airmass
+    cos_zen = tools.cosd(zenith)
+    dhi = 0.3 * (1.0 - tau**airmass) * dni_extra * cos_zen
+    ghi = dhi + dni * cos_zen
+
+    irrads = OrderedDict()
+    irrads['ghi'] = ghi
+    irrads['dni'] = dni
+    irrads['dhi'] = dhi
+
+    if isinstance(ghi, pd.Series):
+        irrads = pd.DataFrame(irrads)
+
+    return irrads
+
+
+def _liujordan(zenith, transmittance, airmass, dni_extra=1367.0):
     '''
     Determine DNI, DHI, GHI from extraterrestrial flux, transmittance,
     and optical air mass number.
@@ -2242,6 +2299,10 @@ def liujordan(zenith, transmittance, airmass, dni_extra=1367.0):
     return irrads
 
 
+liujordan = deprecated('0.8', alternative='campbellnormam',
+                       name='liujordan', removal='0.9')(_liujordan)
+
+
 def _get_perez_coefficients(perezmodel):
     '''
     Find coefficients for the Perez model


Test patch:
diff --git a/pvlib/tests/test_forecast.py b/pvlib/tests/test_forecast.py
index 75b3bad..ff14de7 100644
--- a/pvlib/tests/test_forecast.py
+++ b/pvlib/tests/test_forecast.py
@@ -10,7 +10,7 @@ from conftest import (
     requires_siphon,
     has_siphon,
     skip_windows,
-    requires_recent_cftime,
+    requires_recent_cftime
 )
 from conftest import RERUNS, RERUNS_DELAY
 
@@ -69,7 +69,7 @@ def model(request):
 @pytest.mark.remote_data
 @pytest.mark.flaky(reruns=RERUNS, reruns_delay=RERUNS_DELAY)
 def test_process_data(model):
-    for how in ['liujordan', 'clearsky_scaling']:
+    for how in ['campbell_norman', 'clearsky_scaling']:
         if model.raw_data.empty:
             warnings.warn('Could not test {} process_data with how={} '
                           'because raw_data was empty'.format(model, how))
diff --git a/pvlib/tests/test_irradiance.py b/pvlib/tests/test_irradiance.py
index 0aeec3b..9959526 100644
--- a/pvlib/tests/test_irradiance.py
+++ b/pvlib/tests/test_irradiance.py
@@ -13,7 +13,8 @@ from conftest import assert_frame_equal, assert_series_equal
 
 from pvlib import irradiance
 
-from conftest import requires_ephem, requires_numba
+from conftest import requires_ephem, requires_numba, fail_on_pvlib_version
+from pvlib._deprecation import pvlibDeprecationWarning
 
 
 # fixtures create realistic test input data
@@ -285,13 +286,27 @@ def test_get_sky_diffuse_invalid():
             model='invalid')
 
 
+@fail_on_pvlib_version('0.9')
 def test_liujordan():
     expected = pd.DataFrame(np.array(
         [[863.859736967, 653.123094076, 220.65905025]]),
         columns=['ghi', 'dni', 'dhi'],
         index=[0])
-    out = irradiance.liujordan(
-        pd.Series([10]), pd.Series([0.5]), pd.Series([1.1]), dni_extra=1400)
+    with pytest.warns(pvlibDeprecationWarning):
+        out = irradiance.liujordan(
+            pd.Series([10]), pd.Series([0.5]), pd.Series([1.1]),
+            dni_extra=1400)
+    assert_frame_equal(out, expected)
+
+
+def test_campbell_norman():
+    expected = pd.DataFrame(np.array(
+        [[863.859736967, 653.123094076, 220.65905025]]),
+        columns=['ghi', 'dni', 'dhi'],
+        index=[0])
+    out = irradiance.campbell_norman(
+        pd.Series([10]), pd.Series([0.5]), pd.Series([109764.21013135818]),
+        dni_extra=1400)
     assert_frame_equal(out, expected)