Repository: pvlib/pvlib-python
Pull request #513 (2018-07-27 23:32:02)

make github recognize license, add authors.md, clarify shared copyright


 - [x] Closes  #503
 - [x] I am familiar with the [contributing guidelines](http://pvlib-python.readthedocs.io/en/latest/contributing.html).
 - [x] Fully tested. Added and/or modified tests to ensure correct behavior for all reasonable inputs. Tests (usually) must pass on the TravisCI and Appveyor testing services.
 - [x] Updates entries to `docs/sphinx/source/api.rst` for API changes.
 - [x] Adds description and name entries in the appropriate `docs/sphinx/source/whatsnew` file for all changes.
 - [x] Code quality and style is sufficient. Passes ``git diff upstream/master -u -- "*.py" | flake8 --diff``
 - [x] New code is fully documented. Includes sphinx/numpydoc compliant docstrings and comments in the code where necessary.
 - [x] Pull request is nearly complete and ready for detailed review.

mikofski: Thanks!
cwhanse: I think the pandas AUTHORS.md model is appropriate for pvlib.  I anticipate merging code which has been copyrighted by its authors, and I'd like to identify and preserve that attribution.

Patch:
diff --git a/AUTHORS.md b/AUTHORS.md
new file mode 100644
index 0000000..a4f93c9
--- /dev/null
+++ b/AUTHORS.md
@@ -0,0 +1,32 @@
+About the Copyright Holders
+===========================
+
+*   Copyright (c) 2013, Sandia National Laboratories
+
+    Sandia National Laboratories developed pvlib python based on code in
+    PVLib MATLAB.
+
+*   Copyright (c) 2014-2018, pvlib python Development Team
+
+    The pvlib python Development Team is the collection of developers of the
+    pvlib python project.
+
+Full credits for pvlib python contributors can be found in the documentation.
+
+Our Copyright Policy
+====================
+
+pvlib python uses a shared copyright model. Each contributor maintains copyright
+over their contributions to pvlib python. However, it is important to note that
+these contributions are typically only changes to the repositories. Thus,
+the pvlib python source code, in its entirety, is not the copyright of any single
+person or institution. Instead, it is the collective copyright of the
+entire pvlib python Development Team. If individual contributors want to maintain
+a record of what changes/contributions they have specific copyright on,
+they should indicate their copyright in the commit message of the change
+when they commit the change to the pvlib python repository.
+
+License
+=======
+
+pvlib python is distributed under a 3-clause BSD license.
diff --git a/LICENSE b/LICENSE
index eabfa7e..cc39ec8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,7 +1,6 @@
-Copyright (c) 2013, Sandia National Labs
-All rights reserved.
+BSD 3-Clause License
 
-Copyright (c) 2014-2016, PVLIB Python Developers
+Copyright (c) 2013-2018, Sandia National Laboratories and pvlib python Development Team
 All rights reserved.
 
 Redistribution and use in source and binary forms, with or without modification,
diff --git a/docs/sphinx/source/whatsnew/v0.6.0.rst b/docs/sphinx/source/whatsnew/v0.6.0.rst
index 1aa535c..f3cb70a 100644
--- a/docs/sphinx/source/whatsnew/v0.6.0.rst
+++ b/docs/sphinx/source/whatsnew/v0.6.0.rst
@@ -5,7 +5,11 @@ v0.6.0 (___, 2018)
 
 API Changes
 ~~~~~~~~~~~
-* pvsystem.calcparams_desoto now requires arguments for each module model parameter.
+* pvsystem.calcparams_desoto now requires arguments for each module model
+  parameter. (:issue:`462`)
+* Add losses_parameters attribute to PVSystem objects and remove the kwargs
+  support from PVSystem.pvwatts_losses. Enables custom losses specification
+  in ModelChain calculations. (:issue:`484`)
 * removed irradiance parameter from ModelChain.run_model and ModelChain.prepare_inputs
 
 
@@ -61,6 +65,9 @@ Bug fixes
   (:issue:`493`)
 * Fix ``pvlib.irradiance.klucher`` output is different for Pandas Series vs.
   floats and NumPy arrays. (:issue:`508`)
+* Make GitHub recognize the license, add AUTHORS.md, clarify shared copyright.
+  (:issue:`503`)
+
 
 Documentation
 ~~~~~~~~~~~~~
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
index 43c07de..e44617f 100644
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -100,6 +100,9 @@ class PVSystem(object):
     racking_model : None or string, default 'open_rack_cell_glassback'
         Used for cell and module temperature calculations.
 
+    losses_parameters : None, dict or Series, default None
+        Losses parameters as defined by PVWatts or other.
+
     name : None or string, default None
 
     **kwargs
@@ -120,8 +123,7 @@ class PVSystem(object):
                  modules_per_string=1, strings_per_inverter=1,
                  inverter=None, inverter_parameters=None,
                  racking_model='open_rack_cell_glassback',
-                 name=None,
-                 **kwargs):
+                 losses_parameters=None, name=None, **kwargs):
 
         self.name = name
 
@@ -151,6 +153,11 @@ class PVSystem(object):
         else:
             self.inverter_parameters = inverter_parameters
 
+        if losses_parameters is None:
+            self.losses_parameters = {}
+        else:
+            self.losses_parameters = losses_parameters
+
         self.racking_model = racking_model
 
     def __repr__(self):
@@ -627,15 +634,17 @@ class PVSystem(object):
                           self.module_parameters['gamma_pdc'],
                           **kwargs)
 
-    def pvwatts_losses(self, **kwargs):
+    def pvwatts_losses(self):
         """
         Calculates DC power losses according the PVwatts model using
-        :py:func:`pvwatts_losses`. No attributes are used in this
-        calculation, but all keyword arguments will be passed to the
-        function.
+        :py:func:`pvwatts_losses` and ``self.losses_parameters``.`
 
         See :py:func:`pvwatts_losses` for details.
         """
+        kwargs = _build_kwargs(['soiling', 'shading', 'snow', 'mismatch',
+                                'wiring', 'connections', 'lid',
+                                'nameplate_rating', 'age', 'availability'],
+                               self.losses_parameters)
         return pvwatts_losses(**kwargs)
 
     def pvwatts_ac(self, pdc):


Test patch:
diff --git a/pvlib/test/test_modelchain.py b/pvlib/test/test_modelchain.py
index 225063d..9c908e3 100644
--- a/pvlib/test/test_modelchain.py
+++ b/pvlib/test/test_modelchain.py
@@ -1,3 +1,5 @@
+import sys
+
 import numpy as np
 import pandas as pd
 from numpy import nan
@@ -78,11 +80,19 @@ def pvwatts_dc_pvwatts_ac_system(sam_data):
     return system
 
 
-@pytest.fixture()
+@pytest.fixture
 def location():
     return Location(32.2, -111, altitude=700)
 
 
+@pytest.fixture
+def weather():
+    times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
+    weather = pd.DataFrame({'ghi': [500, 0], 'dni': [800, 0], 'dhi': [100, 0]},
+                           index=times)
+    return weather
+
+
 def test_ModelChain_creation(system, location):
     mc = ModelChain(system, location)
 
@@ -292,7 +302,7 @@ def test_spectral_models(system, location, spectral_model):
                            columns=['precipitable_water'])
     mc = ModelChain(system, location, dc_model='sapm',
                     aoi_model='no_loss', spectral_model=spectral_model)
-    spectral_modifier = mc.run_model(times=times, 
+    spectral_modifier = mc.run_model(times=times,
                                      weather=weather).spectral_modifier
     assert isinstance(spectral_modifier, (pd.Series, float, int))
 
@@ -302,22 +312,44 @@ def constant_losses(mc):
     mc.ac *= mc.losses
 
 
-@requires_scipy
-@pytest.mark.parametrize('losses_model, expected', [
-    ('pvwatts', [163.280464174, 0]),
-    ('no_loss', [190.028186986, 0]),
-    (constant_losses, [171.025368287, 0])
-])
-def test_losses_models(pvwatts_dc_pvwatts_ac_system, location, losses_model,
-                       expected):
+def test_losses_models_pvwatts(pvwatts_dc_pvwatts_ac_system, location, weather,
+                               mocker):
+    age = 1
+    pvwatts_dc_pvwatts_ac_system.losses_parameters = dict(age=age)
+    m = mocker.spy(pvsystem, 'pvwatts_losses')
     mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model='pvwatts',
                     aoi_model='no_loss', spectral_model='no_loss',
-                    losses_model=losses_model)
-    times = pd.date_range('20160101 1200-0700', periods=2, freq='6H')
-    ac = mc.run_model(times).ac
+                    losses_model='pvwatts')
+    mc.run_model(weather.index, weather=weather)
+    assert m.call_count == 1
+    m.assert_called_with(age=age)
+    assert isinstance(mc.ac, (pd.Series, pd.DataFrame))
+    assert not mc.ac.empty
 
-    expected = pd.Series(np.array(expected), index=times)
-    assert_series_equal(ac, expected, check_less_precise=2)
+
+def test_losses_models_ext_def(pvwatts_dc_pvwatts_ac_system, location, weather,
+                               mocker):
+    m = mocker.spy(sys.modules[__name__], 'constant_losses')
+    mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model='pvwatts',
+                    aoi_model='no_loss', spectral_model='no_loss',
+                    losses_model=constant_losses)
+    mc.run_model(weather.index, weather=weather)
+    assert m.call_count == 1
+    assert isinstance(mc.ac, (pd.Series, pd.DataFrame))
+    assert mc.losses == 0.9
+    assert not mc.ac.empty
+
+
+def test_losses_models_no_loss(pvwatts_dc_pvwatts_ac_system, location, weather,
+                               mocker):
+    m = mocker.spy(pvsystem, 'pvwatts_losses')
+    mc = ModelChain(pvwatts_dc_pvwatts_ac_system, location, dc_model='pvwatts',
+                    aoi_model='no_loss', spectral_model='no_loss',
+                    losses_model='no_loss')
+    assert mc.losses_model == mc.no_extra_losses
+    mc.run_model(weather.index, weather=weather)
+    assert m.call_count == 0
+    assert mc.losses == 1
 
 
 @pytest.mark.parametrize('model', [
diff --git a/pvlib/test/test_pvsystem.py b/pvlib/test/test_pvsystem.py
index 729a654..91b90dd 100644
--- a/pvlib/test/test_pvsystem.py
+++ b/pvlib/test/test_pvsystem.py
@@ -1358,9 +1358,10 @@ def test_PVSystem_pvwatts_dc_kwargs(mocker):
 def test_PVSystem_pvwatts_losses(mocker):
     mocker.spy(pvsystem, 'pvwatts_losses')
     system = make_pvwatts_system_defaults()
-    expected = 15
     age = 1
-    out = system.pvwatts_losses(age=age)
+    system.losses_parameters = dict(age=age)
+    expected = 15
+    out = system.pvwatts_losses()
     pvsystem.pvwatts_losses.assert_called_once_with(age=age)
     assert out < expected