Repository: pvlib/pvanalytics

Issue:
Consider switching sphinx themes
Our docs currently use the `alabaster` theme, resulting in a website which I (very subjectively) find a little difficult to read and navigate.  Compare with docs built with other themes:

- pvanalytics (alabaster): https://pvanalytics.readthedocs.io/en/stable/whatsnew/0.1.2.html
- pvlib (pydata-sphinx-theme): https://pvlib-python.readthedocs.io/en/latest/whatsnew.html
- rdtools (rtd-sphinx-theme): https://rdtools.readthedocs.io/en/stable/changelog.html
- twoaxistracking (sphinx_book_theme): https://twoaxistracking.readthedocs.io/en/stable/

I'd like to switch to another theme, but I don't have a strong opinion on what new theme to choose.  Some considerations:

Themes affect not just font and styling but also navigation.  For example, the pydata-sphinx-theme separates out top-level navigation to a top navbar instead of putting the entire website tree in the left sidebar.  See examples of many themes here: https://sphinx-themes.org/#themes

The three `pvlib/*` projects with sphinx docs currently use three different themes.  Using the same theme across multiple projects might be helpful in that experience using a theme in one project would transfer to another.  It may also help with "branding" the pvlib family, if that's something we are interested in.  One downside of using the same theme for all projects is that it might be harder to know which project you're looking at if all of our docs look the same.

Discussion:

Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index aa13cad..5ab7f6c 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -7,7 +7,7 @@ You are free to strikethrough any checklist items that do not apply or to add ad
 - [ ] Added new API functions to `docs/api.rst`.
 - [ ] Non-API functions clearly documented with docstrings or comments as necessary.
 - [ ] Adds description and name entries in the appropriate "what's new" file 
-      in [`docs/whatsnew`](https://github.com/pvlib/pvanalytics/tree/master/docs/whatsnew) 
+      in [`docs/whatsnew`](https://github.com/pvlib/pvanalytics/tree/main/docs/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` ``).
diff --git a/README.md b/README.md
index c96433d..be7aca3 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
 ![lint and test](https://github.com/pvlib/pvanalytics/workflows/lint%20and%20test/badge.svg)
-[![Coverage Status](https://coveralls.io/repos/github/pvlib/pvanalytics/badge.svg?branch=master)](https://coveralls.io/github/pvlib/pvanalytics?branch=master)
+[![Coverage Status](https://coveralls.io/repos/github/pvlib/pvanalytics/badge.svg?branch=main)](https://coveralls.io/github/pvlib/pvanalytics?branch=main)
 [![DOI](https://zenodo.org/badge/DOI/10.5281/zenodo.6110569.svg)](https://doi.org/10.5281/zenodo.6110569)
 
 
diff --git a/docs/conf.py b/docs/conf.py
index b26cc69..e2bed41 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -53,7 +53,23 @@ exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store', 'examples']
 # The theme to use for HTML and HTML Help pages.  See the documentation for
 # a list of builtin themes.
 #
-html_theme = 'alabaster'
+html_theme = 'pydata_sphinx_theme'
+
+html_theme_options = {
+    "github_url": "https://github.com/pvlib/pvanalytics",
+    "icon_links": [
+        {
+            "name": "PyPI",
+            "url": "https://pypi.org/project/pvanalytics/",
+            "icon": "fab fa-python",
+        },
+    ],
+    # "use_edit_page_button": True,
+    "show_toc_level": 1,
+    "footer_items": ["copyright", "sphinx-version", "sidebar-ethical-ads"],
+    "left_sidebar_end": [],
+}
+
 
 # Add any paths that contain custom static files (such as style sheets) here,
 # relative to this directory. They are copied after the builtin static files,
@@ -84,7 +100,6 @@ intersphinx_mapping = {
 # settings for sphinx-gallery
 sphinx_gallery_conf = {
     'examples_dirs': ['examples'],  # location of gallery scripts
-    'nested_sections': False,
     'gallery_dirs': ['generated/gallery'],  # location of generated output
     # sphinx-gallery only shows plots from plot_*.py files by default:
     'filename_pattern': re.escape(os.sep),
diff --git a/docs/examples/weather/weather-limits.py b/docs/examples/weather/weather-limits.py
index 73fd143..1d24ba3 100644
--- a/docs/examples/weather/weather-limits.py
+++ b/docs/examples/weather/weather-limits.py
@@ -1,6 +1,6 @@
 """
 Weather Limits
-===========
+==============
 
 Identifying weather values that are within limits.
 """
diff --git a/docs/index.rst b/docs/index.rst
index 3951824..305dcb3 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -78,22 +78,13 @@ It supports:
 
 Additionally, PVAnalytics relies on several other packages in the open
 source scientific python ecosystem.  For details on dependencies and versions,
-see our `setup.py <https://github.com/pvlib/pvanalytics/blob/master/setup.py>`_.
+see our `setup.py <https://github.com/pvlib/pvanalytics/blob/main/setup.py>`_.
 
-Contents
-========
 
 .. toctree::
-   :maxdepth: 3
+   :hidden:
    :caption: Contents:
 
    api
    generated/gallery/index
    whatsnew/index
-
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`search`
diff --git a/docs/whatsnew/0.1.4.rst b/docs/whatsnew/0.1.4.rst
index 3c938aa..3ffa89f 100644
--- a/docs/whatsnew/0.1.4.rst
+++ b/docs/whatsnew/0.1.4.rst
@@ -5,10 +5,18 @@
 
 Enhancements
 ~~~~~~~~~~~~
+* Updated function :py:func:`~pvanalytics.system.infer_orientation_fit_pvwatts`
+  to more closely align with the PVWatts v5 methodology. This includes incorporating
+  relative airmass and extraterrestrial irradiance into the Perez total irradiance model,
+  accounting for array incidence loss (IAM), and including losses in the PVWatts
+  inverter model. Additionally, added optional arguments for bounding the azimuth range in
+  during least squares optimization. (:issue:`147`, :pull:`180`)
+
 
 Bug Fixes
 ~~~~~~~~~
-
+* ``pvanalytics.__version__`` now correctly reports the version string instead
+  of raising ``AttributeError``. (:pull:`181`)
 
 Requirements
 ~~~~~~~~~~~~
@@ -17,8 +25,10 @@ Requirements
 
 Documentation
 ~~~~~~~~~~~~~
+* Online docs now use ``pydata-sphinx-theme`` instead of the built-in
+  ``alabaster`` theme. (:issue:`176`, :pull:`178`)
 
 
 Contributors
 ~~~~~~~~~~~~
-
+* Kirsten Perry (:ghuser:`kperrynrel`)
diff --git a/pvanalytics/__init__.py b/pvanalytics/__init__.py
index 809eeb2..677d578 100644
--- a/pvanalytics/__init__.py
+++ b/pvanalytics/__init__.py
@@ -1,3 +1,15 @@
+try:
+    from importlib.metadata import PackageNotFoundError, version
+except ImportError:
+    # for python < 3.8 (remove when dropping 3.7 support)
+    from importlib_metadata import PackageNotFoundError, version
+
+try:
+    __version__ = version(__package__)
+except PackageNotFoundError:
+    __version__ = "0+unknown"
+
+
 from pvanalytics import features   # noqa: F401
 from pvanalytics import metrics    # noqa: F401
 from pvanalytics import quality    # noqa: F401
diff --git a/pvanalytics/quality/irradiance.py b/pvanalytics/quality/irradiance.py
index e977a5d..508ff45 100644
--- a/pvanalytics/quality/irradiance.py
+++ b/pvanalytics/quality/irradiance.py
@@ -565,8 +565,7 @@ def calculate_component_sum_series(solar_zenith,
         If a float or int value is passed (np.nan, 0 , -.5, etc.), then
         nighttime values are filled using the fill_night_value parameter.
         If 'equation' is used, nighttime periods are filled using the
-        component sum equation with DNI=0:
-            GHI = 0 + DHI
+        component sum equation with DNI=0: ``GHI = 0 + DHI``
         If None, then the nighttime values are based on the component sum
         equation.
 
diff --git a/pvanalytics/system.py b/pvanalytics/system.py
index 8de3cd6..3cc96fa 100644
--- a/pvanalytics/system.py
+++ b/pvanalytics/system.py
@@ -412,7 +412,9 @@ def _power_residuals_from_clearsky(system_params,
                                    temperature,
                                    wind_speed,
                                    temperature_coefficient,
-                                   temperature_model_parameters):
+                                   temperature_model_parameters,
+                                   relative_airmass,
+                                   dni_extra):
     """Return the residuals between a system with parameters given in
     `system_params` and the data in `power_ac`.
 
@@ -444,29 +446,44 @@ def _power_residuals_from_clearsky(system_params,
         Temperature coefficient of DC power. [1/C]
     temperature_model_parameters : dict
         Parameters for the cell temperature model.
+    relative_airmass: Series
+        Relative airmass at the same times as data in `power_ac`.
+        Required for the running the Perez model in
+        :py:func:`pvlib.irradiance.get_total_irradiance`
+    dni_extra: Series
+        Extraterrestrial radiation at the same times as data in `power_ac`.
+        Required for the running the Perez model in
+        :py:func:`pvlib.irradiance.get_total_irradiance`
 
     Returns
     -------
     Series
         Difference between `power_ac` and the PVWatts output with the
         given parameters.
-
-    Notes
-    ------
-    Uses the defaults in :py:func:`pvlib.irradiance.get_total_irradiance` to
-    calculated plane-of-array irradiance, i.e., the isotropic model for sky
-    diffuse irradiance, and the Perez irradiance transposition model.
     """
     tilt = system_params[0]
     azimuth = system_params[1]
     dc_capacity = system_params[2]
     dc_inverter_limit = system_params[3]
+    # Get total irradiance
     poa = pvlib.irradiance.get_total_irradiance(
         tilt, azimuth,
         solar_zenith,
         solar_azimuth,
-        dni, ghi, dhi
+        dni, ghi, dhi,
+        dni_extra=dni_extra,
+        airmass=relative_airmass,
+        albedo=0.2,
+        model='perez'
     )
+    # Get AOI
+    aoi = pvlib.irradiance.aoi(tilt, azimuth,
+                               solar_zenith,
+                               solar_azimuth)
+    # Run IAM model
+    iam = pvlib.iam.physical(aoi, n=1.5)
+    # Apply IAM to direct POA component only
+    poa_transmitted = poa['poa_direct'] * iam + poa['poa_diffuse']
     temp_cell = pvlib.temperature.sapm_cell(
         poa['poa_global'],
         temperature,
@@ -474,12 +491,15 @@ def _power_residuals_from_clearsky(system_params,
         **temperature_model_parameters
     )
     pdc = pvlib.pvsystem.pvwatts_dc(
-        poa['poa_global'],
+        poa_transmitted,
         temp_cell,
         dc_capacity,
         temperature_coefficient
     )
-    return power_ac - pvlib.inverter.pvwatts(pdc, dc_inverter_limit)
+    # PVWatts losses
+    losses = pvlib.pvsystem.pvwatts_losses() / 100
+    return power_ac - pvlib.inverter.pvwatts(pdc * (1-losses),
+                                             dc_inverter_limit)
 
 
 def _rsquared(data, residuals):
@@ -492,9 +512,13 @@ def _rsquared(data, residuals):
 def infer_orientation_fit_pvwatts(power_ac, ghi, dhi, dni,
                                   solar_zenith, solar_azimuth,
                                   temperature=25, wind_speed=0,
-                                  temperature_coefficient=-0.004,
-                                  temperature_model_parameters=None):
-    """Get the tilt and azimuth that give PVWatts output that most closely
+                                  temperature_coefficient=-0.0047,
+                                  temperature_model_parameters=None,
+                                  azimuth_min=0,
+                                  azimuth_max=360,
+                                  tilt_min=0,
+                                  tilt_max=90):
+    """Get the tilt and azimuth that give PVWatts v5 output that most closely
     fits the data in `power_ac`.
 
     Input data `power_ac`, `ghi`, `dhi`, `dni` should reflect clear-sky
@@ -547,6 +571,18 @@ def infer_orientation_fit_pvwatts(power_ac, ghi, dhi, dni,
         ``pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][
         'open_rack_glass_glass']`` is used. See
         :py:func:`pvlib.temperature.sapm_cell` for more information.
+    azimuth_min: Float, default 0
+        Minimum possible azimuth (bounds) for the least squares search problem.
+        [degrees]
+    azimuth_max: Float, default 360
+        Maximum possible azimuth (bounds) for the least squares search problem.
+        [degrees]
+    tilt_min: Float, default 0
+        Minimum possible tilt (bounds) for the least squares search problem.
+        [degrees]
+    tilt_max: Float, default 90
+        Maximum possible tilt (bounds) for the least squares search problem.
+        [degrees]
 
     Returns
     -------
@@ -561,6 +597,18 @@ def infer_orientation_fit_pvwatts(power_ac, ghi, dhi, dni,
     ------
     ValueError
         If any input passed as a Series contains undefined values (i.e. NaNs).
+
+    Notes
+    -----
+    To prevent significant slowdown, this function uses the SAPM
+    thermal model (:py:func:`~pvlib.temperature.sapm_cell`) instead of
+    the model specified in the PVWatts v5 reference [1]_
+    (:py:func:`~pvlib.temperature.fuentes`).
+
+    References
+    ----------
+    .. [1] Aron Dobos, "PVWatts Version 5 Manual", NREL/TP-6A20-62641 (2014).
+           :doi:`10.2172/1158421`
     """
     if power_ac.hasnans:
         raise ValueError("power_ac must not contain undefined values")
@@ -577,11 +625,19 @@ def infer_orientation_fit_pvwatts(power_ac, ghi, dhi, dni,
     initial_azimuth = 180
     initial_dc_capacity = power_ac.max()
     initial_dc_limit = power_ac.max() * 1.5
+    # Get relative airmass for Perez model
+    relative_airmass = pvlib.atmosphere.get_relative_airmass(solar_zenith)
+    # Get extraterrestrial irradiance for Perez model
+    dni_extra = pvlib.irradiance.get_extra_radiation(power_ac.index)
+    # Optimize for azimuth, tilt, DC capacity, and DC limit using the
+    # :py:func:`scipy.optimize.least_squares` function
     fit_result = scipy.optimize.least_squares(
         _power_residuals_from_clearsky,
         [initial_tilt, initial_azimuth, initial_dc_capacity, initial_dc_limit],
-        bounds=([0, 0, power_ac.max()*0.5, power_ac.max()*0.5],
-                [90, 360, power_ac.max()*2, power_ac.max()*3]),
+        bounds=([tilt_min, azimuth_min,
+                 power_ac.max()*0.5, power_ac.max()*0.5],
+                [tilt_max, azimuth_max,
+                 power_ac.max()*2, power_ac.max()*3]),
         kwargs={
             'ghi': ghi,
             'dhi': dhi,
@@ -592,7 +648,9 @@ def infer_orientation_fit_pvwatts(power_ac, ghi, dhi, dni,
             'temperature': temperature,
             'temperature_coefficient': temperature_coefficient,
             'wind_speed': wind_speed,
-            'temperature_model_parameters': temperature_model_parameters
+            'temperature_model_parameters': temperature_model_parameters,
+            'relative_airmass': relative_airmass,
+            'dni_extra': dni_extra
         }
     )
     r_squared = _rsquared(power_ac, fit_result.fun)
diff --git a/setup.py b/setup.py
index 6e95269..b2d74a5 100644
--- a/setup.py
+++ b/setup.py
@@ -37,11 +37,15 @@ INSTALL_REQUIRES = [
     'pvlib >= 0.9.4',
     'scipy >= 1.4.0',
     'statsmodels >= 0.9.0',
-    'scikit-image >= 0.16.0'
+    'scikit-image >= 0.16.0',
+    'importlib-metadata; python_version < "3.8"',
 ]
 
 DOCS_REQUIRE = [
-    'sphinx == 4.3.1', 'sphinx-gallery', 'matplotlib',
+    'sphinx == 4.5.0',
+    'pydata-sphinx-theme == 0.8.1',
+    'sphinx-gallery',
+    'matplotlib',
 ]
 
 EXTRAS_REQUIRE = {


Test patch:
diff --git a/pvanalytics/tests/test_pvanalytics.py b/pvanalytics/tests/test_pvanalytics.py
new file mode 100644
index 0000000..26e8104
--- /dev/null
+++ b/pvanalytics/tests/test_pvanalytics.py
@@ -0,0 +1,11 @@
+
+from pkg_resources import parse_version
+import pvanalytics
+
+
+def test___version__():
+    # check that the version string is determined correctly.
+    # if the version string is messed up for some reason, it should be
+    # '0+unknown', which is not greater than '0.0.1'.
+    version = parse_version(pvanalytics.__version__)
+    assert version > parse_version('0.0.1')