diff --git a/docs/sphinx/source/api.rst b/docs/sphinx/source/api.rst index f043894ee6..5586c4e44e 100644 --- a/docs/sphinx/source/api.rst +++ b/docs/sphinx/source/api.rst @@ -292,6 +292,7 @@ Inverter models (DC to AC conversion) .. autosummary:: :toctree: generated/ + pvsystem.PVSystem.get_ac inverter.sandia inverter.sandia_multi inverter.adr diff --git a/docs/sphinx/source/whatsnew/v0.9.0.rst b/docs/sphinx/source/whatsnew/v0.9.0.rst index 91254807c8..446f4ac25d 100644 --- a/docs/sphinx/source/whatsnew/v0.9.0.rst +++ b/docs/sphinx/source/whatsnew/v0.9.0.rst @@ -87,6 +87,9 @@ Enhancements by ``pvsystem.PVSystem.modules_per_strings`` and ``pvsystem.PVSystem.strings_per_inverter``. Note that both attributes still default to 1. (:pull:`1138`) +* :py:meth:`~pvlib.pvsystem.PVSystem.get_ac` is added to calculate AC power + from DC power. Use parameter 'model' to specify which inverter model to use. + (:pull:`1147`, :issue:`998`) Bug fixes ~~~~~~~~~ diff --git a/pvlib/pvsystem.py b/pvlib/pvsystem.py index afc1c195cf..cb003f2b3f 100644 --- a/pvlib/pvsystem.py +++ b/pvlib/pvsystem.py @@ -852,7 +852,75 @@ def i_from_v(self, resistance_shunt, resistance_series, nNsVth, voltage, return i_from_v(resistance_shunt, resistance_series, nNsVth, voltage, saturation_current, photocurrent) - # inverter now specified by self.inverter_parameters + def get_ac(self, model, p_dc, v_dc=None): + r"""Calculates AC power from p_dc using the inverter model indicated + by model and self.inverter_parameters. + + Parameters + ---------- + model : str + Must be one of 'sandia', 'adr', or 'pvwatts'. + p_dc : numeric, or tuple, list or array of numeric + DC power on each MPPT input of the inverter. Use tuple, list or + array for inverters with multiple MPPT inputs. If type is array, + p_dc must be 2d with axis 0 being the MPPT inputs. [W] + v_dc : numeric, or tuple, list or array of numeric + DC voltage on each MPPT input of the inverter. Required when + model='sandia' or model='adr'. Use tuple, list or + array for inverters with multiple MPPT inputs. If type is array, + v_dc must be 2d with axis 0 being the MPPT inputs. [V] + + Returns + ------- + power_ac : numeric + AC power output for the inverter. [W] + + Raises + ------ + ValueError + If model is not one of 'sandia', 'adr' or 'pvwatts'. + ValueError + If model='adr' and the PVSystem has more than one array. + + See also + -------- + pvlib.inverter.sandia + pvlib.inverter.sandia_multi + pvlib.inverter.adr + pvlib.inverter.pvwatts + pvlib.inverter.pvwatts_multi + """ + model = model.lower() + multiple_arrays = self.num_arrays > 1 + if model == 'sandia': + if multiple_arrays: + p_dc = self._validate_per_array(p_dc) + v_dc = self._validate_per_array(v_dc) + inv_fun = inverter.sandia_multi + else: + inv_fun = inverter.sandia + return inv_fun(v_dc, p_dc, self.inverter_parameters) + elif model == 'pvwatts': + kwargs = _build_kwargs(['eta_inv_nom', 'eta_inv_ref'], + self.inverter_parameters) + if multiple_arrays: + p_dc = self._validate_per_array(p_dc) + inv_fun = inverter.pvwatts_multi + else: + inv_fun = inverter.pvwatts + return inv_fun(p_dc, self.inverter_parameters['pdc0'], **kwargs) + elif model == 'adr': + if multiple_arrays: + raise ValueError( + 'The adr inverter function cannot be used for an inverter', + ' with multiple MPPT inputs') + else: + return inverter.adr(v_dc, p_dc, self.inverter_parameters) + else: + raise ValueError( + model + ' is not a valid AC power model.', + ' model must be one of "sandia", "adr" or "pvwatts"') + def snlinverter(self, v_dc, p_dc): """Uses :py:func:`pvlib.inverter.sandia` to calculate AC power based on ``self.inverter_parameters`` and the input voltage and power. @@ -969,7 +1037,6 @@ def pvwatts_multi(self, p_dc): self.inverter_parameters) return inverter.pvwatts_multi(p_dc, self.inverter_parameters['pdc0'], **kwargs) - @property @_unwrap_single_value def module_parameters(self): diff --git a/pvlib/tests/test_pvsystem.py b/pvlib/tests/test_pvsystem.py index 311820828b..dd6a51abc2 100644 --- a/pvlib/tests/test_pvsystem.py +++ b/pvlib/tests/test_pvsystem.py @@ -1374,6 +1374,21 @@ def test_PVSystem_multi_scale_voltage_current_power(mocker): system.scale_voltage_current_power(None) +def test_PVSystem_get_ac_sandia(cec_inverter_parameters, mocker): + inv_fun = mocker.spy(inverter, 'sandia') + system = pvsystem.PVSystem( + inverter=cec_inverter_parameters['Name'], + inverter_parameters=cec_inverter_parameters, + ) + vdcs = pd.Series(np.linspace(0, 50, 3)) + idcs = pd.Series(np.linspace(0, 11, 3)) + pdcs = idcs * vdcs + pacs = system.get_ac('sandia', vdcs, pdcs) + assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000])) + inv_fun.assert_called_once() + + +# remove after deprecation period for PVSystem.snlinverter def test_PVSystem_snlinverter(cec_inverter_parameters): system = pvsystem.PVSystem( inverter=cec_inverter_parameters['Name'], @@ -1387,6 +1402,31 @@ def test_PVSystem_snlinverter(cec_inverter_parameters): assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000])) +def test_PVSystem_get_ac_sandia_multi(cec_inverter_parameters, mocker): + inv_fun = mocker.spy(inverter, 'sandia_multi') + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(), pvsystem.Array()], + inverter=cec_inverter_parameters['Name'], + inverter_parameters=cec_inverter_parameters, + ) + vdcs = pd.Series(np.linspace(0, 50, 3)) + idcs = pd.Series(np.linspace(0, 11, 3)) / 2 + pdcs = idcs * vdcs + pacs = system.get_ac('sandia', (vdcs, vdcs), (pdcs, pdcs)) + assert_series_equal(pacs, pd.Series([-0.020000, 132.004308, 250.000000])) + inv_fun.assert_called_once() + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.get_ac('sandia', vdcs, (pdcs, pdcs)) + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.get_ac('sandia', vdcs, (pdcs,)) + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.get_ac('sandia', (vdcs, vdcs), (pdcs, pdcs, pdcs)) + + +# remove after deprecation period for PVSystem.sandia_multi def test_PVSystem_sandia_multi(cec_inverter_parameters): system = pvsystem.PVSystem( arrays=[pvsystem.Array(), pvsystem.Array()], @@ -1409,6 +1449,7 @@ def test_PVSystem_sandia_multi(cec_inverter_parameters): system.sandia_multi((vdcs, vdcs), (pdcs, pdcs, pdcs)) +# remove after deprecation period for PVSystem.sandia_multi def test_PVSystem_sandia_multi_single_array(cec_inverter_parameters): system = pvsystem.PVSystem( arrays=[pvsystem.Array()], @@ -1431,6 +1472,84 @@ def test_PVSystem_sandia_multi_single_array(cec_inverter_parameters): system.sandia_multi((vdcs,), (pdcs, pdcs)) +def test_PVSystem_get_ac_pvwatts(pvwatts_system_defaults, mocker): + mocker.spy(inverter, 'pvwatts') + pdc = 50 + out = pvwatts_system_defaults.get_ac('pvwatts', pdc) + inverter.pvwatts.assert_called_once_with( + pdc, **pvwatts_system_defaults.inverter_parameters) + assert out < pdc + + +def test_PVSystem_get_ac_pvwatts_kwargs(pvwatts_system_kwargs, mocker): + mocker.spy(inverter, 'pvwatts') + pdc = 50 + out = pvwatts_system_kwargs.get_ac('pvwatts', pdc) + inverter.pvwatts.assert_called_once_with( + pdc, **pvwatts_system_kwargs.inverter_parameters) + assert out < pdc + + +def test_PVSystem_get_ac_pvwatts_multi( + pvwatts_system_defaults, pvwatts_system_kwargs, mocker): + mocker.spy(inverter, 'pvwatts_multi') + expected = [pd.Series([0.0, 48.123524, 86.400000]), + pd.Series([0.0, 45.893550, 85.500000])] + systems = [pvwatts_system_defaults, pvwatts_system_kwargs] + for base_sys, exp in zip(systems, expected): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(), pvsystem.Array()], + inverter_parameters=base_sys.inverter_parameters, + ) + pdcs = pd.Series([0., 25., 50.]) + pacs = system.get_ac('pvwatts', (pdcs, pdcs)) + assert_series_equal(pacs, exp) + assert inverter.pvwatts_multi.call_count == 2 + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.get_ac('pvwatts', (pdcs,)) + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.get_ac('pvwatts', pdcs) + with pytest.raises(ValueError, + match="Length mismatch for per-array parameter"): + system.get_ac('pvwatts', (pdcs, pdcs, pdcs)) + + +def test_PVSystem_get_ac_adr(adr_inverter_parameters, mocker): + mocker.spy(inverter, 'adr') + system = pvsystem.PVSystem( + inverter_parameters=adr_inverter_parameters, + ) + vdcs = pd.Series([135, 154, 390, 420, 551]) + pdcs = pd.Series([135, 1232, 1170, 420, 551]) + pacs = system.get_ac('adr', pdcs, vdcs) + assert_series_equal(pacs, pd.Series([np.nan, 1161.5745, 1116.4459, + 382.6679, np.nan])) + inverter.adr.assert_called_once_with(vdcs, pdcs, + system.inverter_parameters) + + +def test_PVSystem_get_ac_adr_multi(adr_inverter_parameters): + system = pvsystem.PVSystem( + arrays=[pvsystem.Array(), pvsystem.Array()], + inverter_parameters=adr_inverter_parameters, + ) + pdcs = pd.Series([135, 1232, 1170, 420, 551]) + with pytest.raises(ValueError, + match="The adr inverter function cannot be used"): + system.get_ac(model='adr', p_dc=pdcs) + + +def test_PVSystem_get_ac_invalid(cec_inverter_parameters): + system = pvsystem.PVSystem( + inverter_parameters=cec_inverter_parameters, + ) + pdcs = pd.Series(np.linspace(0, 50, 3)) + with pytest.raises(ValueError, match="is not a valid AC power model"): + system.get_ac(model='not_a_model', p_dc=pdcs) + + def test_PVSystem_creation(): pv_system = pvsystem.PVSystem(module='blah', inverter='blarg') # ensure that parameter attributes are dict-like. GH 294 @@ -1891,6 +2010,7 @@ def test_PVSystem_pvwatts_losses(pvwatts_system_defaults, mocker): assert out < expected +# remove after deprecation period for PVSystem.pvwatts_ac def test_PVSystem_pvwatts_ac(pvwatts_system_defaults, mocker): mocker.spy(inverter, 'pvwatts') pdc = 50 @@ -1900,6 +2020,7 @@ def test_PVSystem_pvwatts_ac(pvwatts_system_defaults, mocker): assert out < pdc +# remove after deprecation period for PVSystem.pvwatts_ac def test_PVSystem_pvwatts_ac_kwargs(pvwatts_system_kwargs, mocker): mocker.spy(inverter, 'pvwatts') pdc = 50 @@ -1909,6 +2030,7 @@ def test_PVSystem_pvwatts_ac_kwargs(pvwatts_system_kwargs, mocker): assert out < pdc +# remove after deprecation period for PVSystem.pvwatts_ac def test_PVSystem_pvwatts_multi(pvwatts_system_defaults, pvwatts_system_kwargs): expected = [pd.Series([0.0, 48.123524, 86.400000]),