Repository: pvlib/pvlib-python

Issue:
document or support modules_per_string strings_per_inverter with pvwatts in modelchain
Hi, 

I am trying to run Modelchain with pvwatt model but it seems that the `modules_per_string` and `strings_per inverter ` doesn't have any affect on the total output. 

I am not sure why is it so. 
May be ModelChain isn't supporting so. If that's the case how can I achieve the desired result?

Here is my code: 

Thanks in advance
```
# built-in python modules
import os
import inspect

# scientific python add-ons
import numpy as np
import pandas as pd

# plotting stuff
# first line makes the plots appear in the notebook
%matplotlib inline 
import matplotlib.pyplot as plt
import matplotlib as mpl
# seaborn makes your plots look better
try:
    import seaborn as sns
    sns.set(rc={"figure.figsize": (12, 6)})
    sns.set_color_codes()
except ImportError:
    print('We suggest you install seaborn using conda or pip and rerun this cell')

# finally, we import the pvlib library
import pvlib

tmy = pd.read_csv("http://re.jrc.ec.europa.eu/pvgis5/tmy.php?lat=29.74&lon=40.10")

tmy.Date = pd.to_datetime(tmy.Date, format='%Y-%d-%m %H:%M:%S')

tmy.rename(columns={' Ghor':'ghi','Dhor':'dhi','DNI':'dni','Tair':'temp_air',
                        'Ws':'wind_speed'},inplace=True)					
						
tmy.set_index(tmy['Date'],inplace=True)
    #Drop unnecessary column

tmy = tmy.drop('Date', 1)
tmy = tmy.drop('RH', 1)
tmy = tmy.drop('IR', 1)
tmy = tmy.drop(' Wd', 1)
tmy = tmy.drop('Pres', 1)

#module =Jinko_Solar_JKM320P_72_V
#inverter = ABB__PVS980_58_2000kVA_K__N_A_V__CEC_2018_

lat = 29.74
lon = 40.10
altitude = 676
tz = 'Etc/GMT+3'  

loc = pvlib.location.Location(latitude=lat,longitude= lon,tz=tz)

#model = pvwatts 
pvwatts_system = pvlib.pvsystem.PVSystem(module_parameters={'pdc0': 320, 'gamma_pdc': -0.0041},inverter_parameters={'pdc' : 3200000, 'pdc0' : 2024292, 'eta_inv_nom':0.988, 'eta_inv_ref':0.986},surface_tilt = 20, surface_azimuth=0,
                    modules_per_string=30,strings_per_inverter=267, albedo = 0.2)
					
mc = pvlib.modelchain.ModelChain(pvwatts_system, loc, transposition_model ="perez",aoi_model = 'ashrae',spectral_model='no_loss')
print(mc)
mc.run_model(times=tmy.index,weather=tmy)
a = mc.ac
a = pd.Series.to_frame(a)
a = a * 530  # 530 = number of inverters in the system 

a['month'] = a.index
a.month = a.month.dt.month
monthly = a.groupby('month').sum()
```




Discussion:
https://stackoverflow.com/questions/49550656/run-pvlib-modelchain-with-pvwatts-model/50165303#50165303

The work around is to scale your ``module_parameters ['pdc0']``. Pull requests welcome for improving the functionality and/or documentation.
It seems that the system scaling provided by `PVSystem.scale_voltage_current_power()` is a system-level entity that should be included in `PVSystem.sapm()` and `PVSystem.singlediode()` computations, in addition to adding this to `PVSystem.pvwatts_dc()`. Currently, a higher level `ModelChain` function does this (except for pvwatts, as discussed above). If folks agree to this, then a question arises as to if the corresponding wrapped functions in `pvsystem.py` should still only calculate `singlediode()` for a single module/device instead of the whole system. (ATM, I think that they should.)
@cwhanse we need this for SPI. Do you have any concern with adding this

```python
        self.results.dc = self.system.scale_voltage_current_power(
            self.results.dc,
            unwrap=False
        )
```

to 

https://github.com/pvlib/pvlib-python/blob/56971c614e7faea3c24013445f1bf6ffe9943305/pvlib/modelchain.py#L732-L735

?

Or do you think we should go ahead with @markcampanelli's suggestion above? I think @markcampanelli's suggestion is better on the merits but it's a much bigger change and I don't know how to do it in a way that wouldn't cause user code to return unexpected answers.
I don't have a problem patching that into `pvlib.modelchain.ModelChain.pvwatts_dc`. I think it's an oversight that the scaling was left out, since it is included in the `sapm` and `singlediode` methods.
I think we left it out because it's arguably a departure from the pvwatts model in which you're typically specifying the pdc0 of the entire system. But I don't see a problem with the extension within our data model.
want me to open a PR? Or have you got it?
Would be great if you can do it.

Patch:
diff --git a/pvlib/modelchain.py b/pvlib/modelchain.py
--- a/pvlib/modelchain.py
+++ b/pvlib/modelchain.py
@@ -730,8 +730,33 @@ def pvsyst(self):
         return self._singlediode(self.system.calcparams_pvsyst)
 
     def pvwatts_dc(self):
+        """Calculate DC power using the PVWatts model.
+
+        Results are stored in ModelChain.results.dc. DC power is computed
+        from PVSystem.module_parameters['pdc0'] and then scaled by
+        PVSystem.modules_per_string and PVSystem.strings_per_inverter.
+
+        Returns
+        -------
+        self
+
+        See also
+        --------
+        pvlib.pvsystem.PVSystem.pvwatts_dc
+        pvlib.pvsystem.PVSystem.scale_voltage_current_power
+        """
         self.results.dc = self.system.pvwatts_dc(
             self.results.effective_irradiance, self.results.cell_temperature)
+        if isinstance(self.results.dc, tuple):
+            temp = tuple(
+                pd.DataFrame(s, columns=['p_mp']) for s in self.results.dc)
+        else:
+            temp = pd.DataFrame(self.results.dc, columns=['p_mp'])
+        scaled = self.system.scale_voltage_current_power(temp)
+        if isinstance(scaled, tuple):
+            self.results.dc = tuple(s['p_mp'] for s in scaled)
+        else:
+            self.results.dc = scaled['p_mp']
         return self
 
     @property
diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py
--- a/pvlib/pvsystem.py
+++ b/pvlib/pvsystem.py
@@ -891,7 +891,7 @@ def scale_voltage_current_power(self, data):
         Parameters
         ----------
         data: DataFrame or tuple of DataFrame
-            Must contain columns `'v_mp', 'v_oc', 'i_mp' ,'i_x', 'i_xx',
+            May contain columns `'v_mp', 'v_oc', 'i_mp' ,'i_x', 'i_xx',
             'i_sc', 'p_mp'`.
 
         Returns
@@ -2626,13 +2626,13 @@ def i_from_v(resistance_shunt, resistance_series, nNsVth, voltage,
 
 def scale_voltage_current_power(data, voltage=1, current=1):
     """
-    Scales the voltage, current, and power of the DataFrames
-    returned by :py:func:`singlediode` and :py:func:`sapm`.
+    Scales the voltage, current, and power in data by the voltage
+    and current factors.
 
     Parameters
     ----------
     data: DataFrame
-        Must contain columns `'v_mp', 'v_oc', 'i_mp' ,'i_x', 'i_xx',
+        May contain columns `'v_mp', 'v_oc', 'i_mp' ,'i_x', 'i_xx',
         'i_sc', 'p_mp'`.
     voltage: numeric, default 1
         The amount by which to multiply the voltages.
@@ -2648,14 +2648,15 @@ def scale_voltage_current_power(data, voltage=1, current=1):
 
     # as written, only works with a DataFrame
     # could make it work with a dict, but it would be more verbose
-    data = data.copy()
-    voltages = ['v_mp', 'v_oc']
-    currents = ['i_mp', 'i_x', 'i_xx', 'i_sc']
-    data[voltages] *= voltage
-    data[currents] *= current
-    data['p_mp'] *= voltage * current
-
-    return data
+    voltage_keys = ['v_mp', 'v_oc']
+    current_keys = ['i_mp', 'i_x', 'i_xx', 'i_sc']
+    power_keys = ['p_mp']
+    voltage_df = data.filter(voltage_keys, axis=1) * voltage
+    current_df = data.filter(current_keys, axis=1) * current
+    power_df = data.filter(power_keys, axis=1) * voltage * current
+    df = pd.concat([voltage_df, current_df, power_df], axis=1)
+    df_sorted = df[data.columns]  # retain original column order
+    return df_sorted
 
 
 def pvwatts_dc(g_poa_effective, temp_cell, pdc0, gamma_pdc, temp_ref=25.):
@@ -2675,20 +2676,20 @@ def pvwatts_dc(g_poa_effective, temp_cell, pdc0, gamma_pdc, temp_ref=25.):
     Parameters
     ----------
     g_poa_effective: numeric
-        Irradiance transmitted to the PV cells in units of W/m**2. To be
+        Irradiance transmitted to the PV cells. To be
         fully consistent with PVWatts, the user must have already
         applied angle of incidence losses, but not soiling, spectral,
-        etc.
+        etc. [W/m^2]
     temp_cell: numeric
-        Cell temperature in degrees C.
+        Cell temperature [C].
     pdc0: numeric
-        Power of the modules at 1000 W/m2 and cell reference temperature.
+        Power of the modules at 1000 W/m^2 and cell reference temperature. [W]
     gamma_pdc: numeric
-        The temperature coefficient in units of 1/C. Typically -0.002 to
-        -0.005 per degree C.
+        The temperature coefficient of power. Typically -0.002 to
+        -0.005 per degree C. [1/C]
     temp_ref: numeric, default 25.0
         Cell reference temperature. PVWatts defines it to be 25 C and
-        is included here for flexibility.
+        is included here for flexibility. [C]
 
     Returns
     -------


Test patch:
diff --git a/pvlib/tests/test_modelchain.py b/pvlib/tests/test_modelchain.py
--- a/pvlib/tests/test_modelchain.py
+++ b/pvlib/tests/test_modelchain.py
@@ -1180,6 +1180,25 @@ def test_dc_model_user_func(pvwatts_dc_pvwatts_ac_system, location, weather,
     assert not mc.results.ac.empty
 
 
+def test_pvwatts_dc_multiple_strings(pvwatts_dc_pvwatts_ac_system, location,
+                                     weather, mocker):
+    system = pvwatts_dc_pvwatts_ac_system
+    m = mocker.spy(system, 'scale_voltage_current_power')
+    mc1 = ModelChain(system, location,
+                     aoi_model='no_loss', spectral_model='no_loss')
+    mc1.run_model(weather)
+    assert m.call_count == 1
+    system.arrays[0].modules_per_string = 2
+    mc2 = ModelChain(system, location,
+                     aoi_model='no_loss', spectral_model='no_loss')
+    mc2.run_model(weather)
+    assert isinstance(mc2.results.ac, (pd.Series, pd.DataFrame))
+    assert not mc2.results.ac.empty
+    expected = pd.Series(data=[2., np.nan], index=mc2.results.dc.index,
+                         name='p_mp')
+    assert_series_equal(mc2.results.dc / mc1.results.dc, expected)
+
+
 def acdc(mc):
     mc.results.ac = mc.results.dc