Skip to content

Implement PVSystem.get_ac #1147

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 10 commits into from
Jan 27, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/sphinx/source/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,7 @@ Inverter models (DC to AC conversion)
.. autosummary::
:toctree: generated/

pvsystem.PVSystem.get_ac
inverter.sandia
inverter.sandia_multi
inverter.adr
Expand Down
3 changes: 3 additions & 0 deletions docs/sphinx/source/whatsnew/v0.9.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~
Expand Down
71 changes: 69 additions & 2 deletions pvlib/pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

remove this line assuming we move the validation call above the if model block

inv_fun = inverter.pvwatts_multi
else:
inv_fun = inverter.pvwatts
return inv_fun(p_dc, self.inverter_parameters['pdc0'], **kwargs)
elif model == 'adr':
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to handle the case that model == 'adr' and self.num_arrays > 1 with another ValueError.

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.
Expand Down Expand Up @@ -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):
Expand Down
122 changes: 122 additions & 0 deletions pvlib/tests/test_pvsystem.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Copy link
Member

@wholmgren wholmgren Jan 27, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand why this test passed in the CI because vdcs and pdcs are swapped from their correct order. It failed when I ran the tests locally. Fixed in #1150

I think we had a similar issue with the CI in the last year. Maybe there's something wrong in our configuration.

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'],
Expand All @@ -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()],
Expand All @@ -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()],
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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]),
Expand Down