Skip to content
4 changes: 4 additions & 0 deletions docs/sphinx/source/whatsnew/v0.9.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ Enhancements
automatically switch to using ``'effective_irradiance'`` (if available) for
cell temperature models, when ``'poa_global'`` is not provided in input
weather or calculated from input weather data.
* :py:meth:`~pvlib.modelchain.ModelChain.pvwatts_dc` now scales the DC power
by ``pvsystem.PVSystem.modules_per_strings`` and
``pvsystem.PVSystem.strings_per_inverter``. Note that both attributes still
default to 1. (:pull:`1138`)

Bug fixes
~~~~~~~~~
Expand Down
25 changes: 25 additions & 0 deletions pvlib/modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 20 additions & 19 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand All @@ -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.):
Expand All @@ -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
-------
Expand Down
19 changes: 19 additions & 0 deletions pvlib/tests/test_modelchain.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down