From a5f58af47a8a5d4ea59777f31ca79977905099d3 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 25 Aug 2020 15:32:14 -0600 Subject: [PATCH 01/14] Identify surface orientation from AC power and clearsky irradiance Implements method for identifying tilt and azimuth given ac power, clearsky irradiance, and solar position. --- pvanalytics/system.py | 158 ++++++++++++++++++ pvanalytics/tests/conftest.py | 20 ++- .../tests/features/test_orientation.py | 20 +-- pvanalytics/tests/test_system.py | 97 ++++++++++- 4 files changed, 268 insertions(+), 27 deletions(-) diff --git a/pvanalytics/system.py b/pvanalytics/system.py index b3e2ccf3..efdfa10c 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -2,8 +2,10 @@ import enum import warnings import numpy as np +import scipy import pandas as pd import pvlib +from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS from pvanalytics.util import _fit, _group @@ -401,3 +403,159 @@ def infer_orientation_daily_peak(power_or_poa, sunny, tilts, best_azimuth = azimuth best_tilt = tilt return best_azimuth, best_tilt + + +def _power_residuals_from_clearsky(system_params, + ghi, dhi, dni, + solar_zenith, solar_azimuth, + power_ac, temperature, + wind_speed, + temperature_coefficient, + temperature_model_parameters): + # Return the residuals between a system with parameters given in `params` + # and the data in `power_ac`. + # + # Parameters + # ---------- + # system_params : array-like + # array of four floats: tilt, azimuth, DC capacity, and inverter + # DC input limit. + # ghi : Series + # Clear sky GHI + # dhi : Series + # Clear sky DHI + # dni : Series + # Clear sky DNI + # solar_zenith : Series + # Solar zenith at the same times as data in `power_ac` + # solar_azimuth : Series + # Solar azimuth at the same times as data in `power_ac` + # power_ac : Series + # Measured AC power under clear sky conditions. + # temperature : float or Series + # Air temperature at which to model the hypothetical system. If a + # float then a constant temperature is used. If a Series, must have + # the same index as `power_ac`. [C] + # wind_speed : float or Series + # Wind speed. If a float then a constant wind speed is used. If a + # Series, must have the same index as `power_ac`. [m/s] + # temperature_model_parameters : dict + # Parameters fot the cell temperature model. + # + # Returns + # ------- + # Series + # Difference between `power_ac` and the PVWatts output with the + # given parameters. + tilt = system_params[0] + azimuth = system_params[1] + dc_capacity = system_params[2] + dc_limit = system_params[3] + poa = pvlib.irradiance.get_total_irradiance( + tilt, azimuth, + solar_zenith, + solar_azimuth, + dni, ghi, dhi + ) + temp_cell = pvlib.temperature.sapm_cell( + poa['poa_global'], + temperature, + wind_speed, + **temperature_model_parameters + ) + pdc = pvlib.pvsystem.pvwatts_dc( + poa['poa_global'], + temp_cell, + dc_capacity, + temperature_coefficient + ) + return power_ac - pvlib.inverter.pvwatts(pdc, dc_limit) + + +def _rsquared(data, residuals): + # Calculate the coefficient of determination from `residuals` + model = data + residuals + _, _, r, _, _ = scipy.stats.linregress(model, data) + return r*r + + +def orientation_fit_pvwatts(power_ac, ghi, dhi, dni, + solar_zenith, solar_azimuth, + temperature=25, wind_speed=0, + temperature_coefficient=-0.002, + temperature_model_parameters=None): + """Get the tilt and azimuth that give pvwatts output that most closely + fits the data in `power_ac`. + + Uses non-linear least squares to optimize over four free variables + to find the values that result in the best fit between power modeled + using PVWatts and `power_ac`. The four free variables are + - surface tilt + - surface azimuth + - the DC capacity of the system + - the DC input limit of the inverter. + + Parameters + ---------- + power_ac : Series + AC power from the system in clear sky conditions. + ghi : Series + Clear sky GHI at the same times as `power_ac` + dhi : Series + Clear sky DHI. + dni : Series + Clear sky DNI. + solar_zenith : Series + Solar zenith. [degrees] + solar_azimuth : Series + Solar azimuth. [degrees] + temperature : float or Series, default 25 + Air temperature at which to model the hypothetical system. If a + float then a constant temperature is used. If a Series, must have + the same index as `power_ac`. [C] + wind_speed : float or Series, default 0 + Wind speed. If a float then a constant wind speed is used. If a + Series, must have the same index as `power_ac`. [m/s] + temperature_model_parameters : dict, optional + Parameters fot the cell temperature model. If not specified + ``pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ + 'open_rack_glass_glass'] is used. See + :py:func`pvlib.temperature.sapm_cell` for more information. + + Returns + ------- + surface_tilt : float + Tilt of the array. [degrees] + surface_azimuth : float + Azimuth of the array. [degrees] + r_squared : float + :math:`r^2` value for the fit at the returned orientation. + + """ + if temperature_model_parameters is None: + temperature_model_parameters = \ + TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'] + initial_tilt = 45 + initial_azimuth = 180 + initial_dc_capacity = power_ac.max() + initial_dc_limit = power_ac.max() * 1.5 + fit_result = scipy.optimize.least_squares( + _power_residuals_from_clearsky, + [initial_tilt, initial_azimuth, initial_dc_capacity, initial_dc_limit], + bounds=([0, 0, power_ac.max()*0.5, power_ac.max()*0.5], + [90, 360, power_ac.max()*2, power_ac.max()*3]), + kwargs={ + 'ghi': ghi, + 'dhi': dhi, + 'dni': dni, + 'solar_zenith': solar_zenith, + 'solar_azimuth': solar_azimuth, + 'power_ac': power_ac, + 'temperature': temperature, + 'temperature_coefficient': temperature_coefficient, + 'wind_speed': wind_speed, + 'temperature_model_parameters': temperature_model_parameters + } + ) + r_squared = _rsquared(power_ac, fit_result.fun) + return fit_result.x[0], fit_result.x[1], r_squared diff --git a/pvanalytics/tests/conftest.py b/pvanalytics/tests/conftest.py index c13c5ba0..fe2841e9 100644 --- a/pvanalytics/tests/conftest.py +++ b/pvanalytics/tests/conftest.py @@ -2,7 +2,8 @@ import pytest import numpy as np import pandas as pd -from pvlib import location +from pvlib import location, pvsystem +from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS def pytest_addoption(parser): @@ -99,3 +100,20 @@ def clearsky_year(one_year_hourly, albuquerque): def solarposition_year(one_year_hourly, albuquerque): """One year of solar position data in albuquerque""" return albuquerque.get_solarposition(one_year_hourly) + + +@pytest.fixture(scope='module') +def system_parameters(): + """System parameters for generating simulated power data.""" + sandia_modules = pvsystem.retrieve_sam('SandiaMod') + sapm_inverters = pvsystem.retrieve_sam('cecinverter') + module = sandia_modules['Canadian_Solar_CS5P_220M___2009_'] + inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_'] + temperature_model_parameters = ( + TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'] + ) + return { + 'module_parameters': module, + 'inverter_parameters': inverter, + 'temperature_model_parameters': temperature_model_parameters + } diff --git a/pvanalytics/tests/features/test_orientation.py b/pvanalytics/tests/features/test_orientation.py index b85d19a3..dd80564d 100644 --- a/pvanalytics/tests/features/test_orientation.py +++ b/pvanalytics/tests/features/test_orientation.py @@ -1,28 +1,10 @@ import pytest from pandas.util.testing import assert_series_equal import pandas as pd -from pvlib import pvsystem, tracking, modelchain, irradiance -from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS +from pvlib import tracking, modelchain, irradiance from pvanalytics.features import orientation -@pytest.fixture(scope='module') -def system_parameters(): - """System parameters for generating simulated power data.""" - sandia_modules = pvsystem.retrieve_sam('SandiaMod') - sapm_inverters = pvsystem.retrieve_sam('cecinverter') - module = sandia_modules['Canadian_Solar_CS5P_220M___2009_'] - inverter = sapm_inverters['ABB__MICRO_0_25_I_OUTD_US_208__208V_'] - temperature_model_parameters = ( - TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'] - ) - return { - 'module_parameters': module, - 'inverter_parameters': inverter, - 'temperature_model_parameters': temperature_model_parameters - } - - def test_clearsky_ghi_fixed(clearsky, solarposition): """Identify every day as fixed, since clearsky GHI is sunny.""" assert orientation.fixed_nrel( diff --git a/pvanalytics/tests/test_system.py b/pvanalytics/tests/test_system.py index 33d07364..b3ee3651 100644 --- a/pvanalytics/tests/test_system.py +++ b/pvanalytics/tests/test_system.py @@ -1,8 +1,9 @@ -"""Tests for funcitons that identify system characteristics.""" +"""Tests for system parameter identification functions.""" import pytest -import numpy as np import pandas as pd -from pvlib import pvsystem, tracking, modelchain, irradiance +import numpy as np +import pvlib +from pvlib import location, pvsystem, tracking, modelchain, irradiance from pvlib.temperature import TEMPERATURE_MODEL_PARAMETERS from pvanalytics import system @@ -51,9 +52,9 @@ def summer_ghi(summer_clearsky): @pytest.fixture def summer_power_fixed(summer_clearsky, albuquerque, system_parameters): """Simulated power from a FIXED PVSystem in Albuquerque, NM.""" - system = pvsystem.PVSystem(**system_parameters) + pv_system = pvsystem.PVSystem(**system_parameters) mc = modelchain.ModelChain( - system, + pv_system, albuquerque, orientation_strategy='south_at_latitude_tilt' ) @@ -64,9 +65,9 @@ def summer_power_fixed(summer_clearsky, albuquerque, system_parameters): @pytest.fixture def summer_power_tracking(summer_clearsky, albuquerque, system_parameters): """Simulated power for a pvlib SingleAxisTracker PVSystem in Albuquerque""" - system = tracking.SingleAxisTracker(**system_parameters) + pv_system = tracking.SingleAxisTracker(**system_parameters) mc = modelchain.ModelChain( - system, + pv_system, albuquerque, orientation_strategy='south_at_latitude_tilt' ) @@ -406,3 +407,85 @@ def test_orientation_with_gaps(clearsky_year, solarposition_year): ) assert azimuth == 180 assert tilt == 15 + + +@pytest.fixture(scope='module') +def naive_times(): + """One year at 1-hour intervals""" + return pd.date_range( + start='2020', + end='2021', + freq='H' + ) + + +@pytest.fixture(scope='module', + params=[(35, -106, 'Etc/GMT+7'), + (50, 10, 'Etc/GMT-1'), + (-37, 144, 'Etc/GMT-10')], + ids=['Albuquerque', 'Berlin', 'Melbourne']) +def system_location(request): + """Location of the system.""" + return location.Location( + request.param[0], request.param[1], tz=request.param[2] + ) + + +@pytest.fixture(scope='module', + params=[(0, 180), (30, 180), (30, 90), (30, 270), (30, 0)], + ids=['South-0', 'South-30', 'East-30', 'West-30', 'North-30']) +def system_power(request, system_location, naive_times): + tilt = request.param[0] + azimuth = request.param[1] + local_time = naive_times.tz_localize(system_location.tz) + clearsky = system_location.get_clearsky( + local_time, model='simplified_solis' + ) + solar_position = system_location.get_solarposition(local_time) + poa = irradiance.get_total_irradiance( + tilt, azimuth, + solar_position['zenith'], + solar_position['azimuth'], + **clearsky + ) + temp_cell = pvlib.temperature.sapm_cell( + poa['poa_global'], + 25, 0, + **pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS[ + 'sapm' + ][ + 'open_rack_glass_glass' + ] + ) + pdc = pvsystem.pvwatts_dc(poa['poa_global'], temp_cell, 100, -0.002) + pac = pvsystem.inverter.pvwatts(pdc, 120) + return { + 'location': system_location, + 'tilt': tilt, + 'azimuth': azimuth, + 'clearsky': clearsky, + 'solar_position': solar_position, + 'ac': pac + } + + +@pytest.mark.slow +def test_orientation_fit_pvwatts(system_power): + day_mask = system_power['ac'] > 0 + tilt, azimuth, rsquared = system.orientation_fit_pvwatts( + system_power['ac'][day_mask], + solar_zenith=system_power['solar_position']['zenith'][day_mask], + solar_azimuth=system_power['solar_position']['azimuth'][day_mask], + **system_power['clearsky'][day_mask] + ) + assert rsquared > 0.9 + assert tilt == pytest.approx(system_power['tilt'], abs=10) + if system_power['tilt'] == 0: + # Any azimuth will give the same results at tilt 0. + return + if system_power['azimuth'] == 0: + # 0 degrees equals 360 degrees. + assert (azimuth == pytest.approx(0, abs=10) + or azimuth == pytest.approx(360, abs=10)) + else: + assert azimuth == pytest.approx(system_power['azimuth'], abs=10) From ead8058b69bca9345c5791acaaefd3bba9221774 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Mon, 5 Oct 2020 15:33:17 -0600 Subject: [PATCH 02/14] Add to API documentation Add entry to the list of functions for orientation inference in api.rst and fix minor issues in the docstring. --- docs/api.rst | 1 + pvanalytics/system.py | 11 ++++++----- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/docs/api.rst b/docs/api.rst index abdf0377..f5aeaf4f 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -241,6 +241,7 @@ power or plane of array irradiance measurements. :toctree: generated/ system.infer_orientation_daily_peak + system.orientation_fit_pvwatts Metrics ======= diff --git a/pvanalytics/system.py b/pvanalytics/system.py index efdfa10c..cd134e08 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -490,6 +490,7 @@ def orientation_fit_pvwatts(power_ac, ghi, dhi, dni, Uses non-linear least squares to optimize over four free variables to find the values that result in the best fit between power modeled using PVWatts and `power_ac`. The four free variables are + - surface tilt - surface azimuth - the DC capacity of the system @@ -500,11 +501,11 @@ def orientation_fit_pvwatts(power_ac, ghi, dhi, dni, power_ac : Series AC power from the system in clear sky conditions. ghi : Series - Clear sky GHI at the same times as `power_ac` + Clear sky GHI with same index as `power_ac`. dhi : Series - Clear sky DHI. + Clear sky DHI with same index as `power_ac`. dni : Series - Clear sky DNI. + Clear sky DNI with same index as `power_ac`. solar_zenith : Series Solar zenith. [degrees] solar_azimuth : Series @@ -519,8 +520,8 @@ def orientation_fit_pvwatts(power_ac, ghi, dhi, dni, temperature_model_parameters : dict, optional Parameters fot the cell temperature model. If not specified ``pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS['sapm'][ - 'open_rack_glass_glass'] is used. See - :py:func`pvlib.temperature.sapm_cell` for more information. + 'open_rack_glass_glass']`` is used. See + :py:func:`pvlib.temperature.sapm_cell` for more information. Returns ------- From a9c780bb731c1e12b289c655146cf36ab7dbc6a5 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 20 Oct 2020 10:10:20 -0600 Subject: [PATCH 03/14] Additional tests for system.orientation_fit_pvwatts() Test with missing data and with Series for temperature and wind_speed parameters. --- pvanalytics/system.py | 12 +++++ pvanalytics/tests/test_system.py | 87 ++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) diff --git a/pvanalytics/system.py b/pvanalytics/system.py index cd134e08..28703243 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -496,6 +496,8 @@ def orientation_fit_pvwatts(power_ac, ghi, dhi, dni, - the DC capacity of the system - the DC input limit of the inverter. + All parameters passed as a Series must have the same index. + Parameters ---------- power_ac : Series @@ -533,6 +535,16 @@ def orientation_fit_pvwatts(power_ac, ghi, dhi, dni, :math:`r^2` value for the fit at the returned orientation. """ + power_ac = power_ac.dropna() + ghi = ghi.dropna() + dhi = dhi.dropna() + dni = dni.dropna() + solar_azimuth = solar_azimuth.dropna() + solar_zenith = solar_zenith.dropna() + if isinstance(temperature, pd.Series): + temperature = temperature.dropna() + if isinstance(wind_speed, pd.Series): + wind_speed = wind_speed.dropna() if temperature_model_parameters is None: temperature_model_parameters = \ TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'] diff --git a/pvanalytics/tests/test_system.py b/pvanalytics/tests/test_system.py index b3ee3651..f26ca3cb 100644 --- a/pvanalytics/tests/test_system.py +++ b/pvanalytics/tests/test_system.py @@ -489,3 +489,90 @@ def test_orientation_fit_pvwatts(system_power): or azimuth == pytest.approx(360, abs=10)) else: assert azimuth == pytest.approx(system_power['azimuth'], abs=10) + + +def test_orientation_fit_pvwatts_missing_data(naive_times): + tilt = 30 + azimuth = 100 + system_location = location.Location(35, -106) + local_time = naive_times.tz_localize('MST') + clearsky = system_location.get_clearsky( + local_time, model='simplified_solis' + ) + clearsky.loc['3/1/2020':'3/15/2020'] = np.nan + solar_position = system_location.get_solarposition(clearsky.index) + solar_position.loc['3/1/2020':'3/15/2020'] = np.nan + poa = irradiance.get_total_irradiance( + tilt, azimuth, + solar_position['zenith'], + solar_position['azimuth'], + **clearsky + ) + temp_cell = pvlib.temperature.sapm_cell( + poa['poa_global'], + 25, 0, + **pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS[ + 'sapm' + ][ + 'open_rack_glass_glass' + ] + ) + pdc = pvsystem.pvwatts_dc(poa['poa_global'], temp_cell, 100, -0.002) + pac = pvsystem.inverter.pvwatts(pdc, 120) + tilt_out, azimuth_out, rsquared = system.orientation_fit_pvwatts( + pac, **clearsky, + solar_zenith=solar_position['zenith'], + solar_azimuth=solar_position['azimuth'] + ) + assert rsquared > 0.9 + assert tilt_out == pytest.approx(tilt, abs=10) + assert azimuth_out == pytest.approx(azimuth, abs=10) + clearsky.dropna(inplace=True) + pac.dropna(inplace=True) + solar_position.dropna(inplace=True) + tilt_out, azimuth_out, rsquared = system.orientation_fit_pvwatts( + pac, **clearsky, + solar_zenith=solar_position['zenith'], + solar_azimuth=solar_position['azimuth'] + ) + assert rsquared > 0.9 + assert tilt_out == pytest.approx(tilt, abs=10) + assert azimuth_out == pytest.approx(azimuth, abs=10) + + +def test_orientation_fit_pvwatts_temp_wind_as_series(naive_times): + tilt = 30 + azimuth = 100 + system_location = location.Location(35, -106) + local_time = naive_times.tz_localize('MST') + clearsky = system_location.get_clearsky( + local_time, model='simplified_solis' + ) + solar_position = system_location.get_solarposition(clearsky.index) + poa = irradiance.get_total_irradiance( + tilt, azimuth, + solar_position['zenith'], + solar_position['azimuth'], + **clearsky + ) + temp_cell = pvlib.temperature.sapm_cell( + poa['poa_global'], + 25, 1, + **pvlib.temperature.TEMPERATURE_MODEL_PARAMETERS[ + 'sapm' + ][ + 'open_rack_glass_glass' + ] + ) + pdc = pvsystem.pvwatts_dc(poa['poa_global'], temp_cell, 100, -0.002) + pac = pvsystem.inverter.pvwatts(pdc, 120) + tilt_out, azimuth_out, rsquared = system.orientation_fit_pvwatts( + pac, **clearsky, + solar_zenith=solar_position['zenith'], + solar_azimuth=solar_position['azimuth'], + temperature=pd.Series(25, index=clearsky.index), + wind_speed=pd.Series(1, index=clearsky.index) + ) + assert rsquared > 0.9 + assert tilt_out == pytest.approx(tilt, abs=10) + assert azimuth_out == pytest.approx(azimuth, abs=10) From 63865ef069fbfb68676c0a9f402906bbf4be0e1e Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 20 Oct 2020 15:20:34 -0600 Subject: [PATCH 04/14] Require input with no missing values --- pvanalytics/system.py | 26 +++++++----- pvanalytics/tests/test_system.py | 72 ++++++++++++++++++++++++++------ 2 files changed, 74 insertions(+), 24 deletions(-) diff --git a/pvanalytics/system.py b/pvanalytics/system.py index 28703243..702a7773 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -496,7 +496,9 @@ def orientation_fit_pvwatts(power_ac, ghi, dhi, dni, - the DC capacity of the system - the DC input limit of the inverter. - All parameters passed as a Series must have the same index. + All parameters passed as a Series must have the same index and must not + contain any undefined values (i.e. NaNs). If any input contains NaNs a + ValueError is raised. Parameters ---------- @@ -534,17 +536,19 @@ def orientation_fit_pvwatts(power_ac, ghi, dhi, dni, r_squared : float :math:`r^2` value for the fit at the returned orientation. + Raises + ------ + ValueError + If any input passed as a Series contains undefined values (i.e. NaNs). """ - power_ac = power_ac.dropna() - ghi = ghi.dropna() - dhi = dhi.dropna() - dni = dni.dropna() - solar_azimuth = solar_azimuth.dropna() - solar_zenith = solar_zenith.dropna() - if isinstance(temperature, pd.Series): - temperature = temperature.dropna() - if isinstance(wind_speed, pd.Series): - wind_speed = wind_speed.dropna() + if power_ac.hasnans: + raise ValueError("power_ac must not contain undefined values") + if ghi.hasnans or dhi.hasnans or dni.hasnans: + raise ValueError("ghi, dhi, and dni must not contain undefined values") + if isinstance(temperature, pd.Series) and temperature.hasnans: + raise ValueError("temperature must not contain undefined values") + if isinstance(wind_speed, pd.Series) and wind_speed.hasnans: + raise ValueError("wind_speed must not contain undefined values") if temperature_model_parameters is None: temperature_model_parameters = \ TEMPERATURE_MODEL_PARAMETERS['sapm']['open_rack_glass_glass'] diff --git a/pvanalytics/tests/test_system.py b/pvanalytics/tests/test_system.py index f26ca3cb..79ca33ee 100644 --- a/pvanalytics/tests/test_system.py +++ b/pvanalytics/tests/test_system.py @@ -519,17 +519,23 @@ def test_orientation_fit_pvwatts_missing_data(naive_times): ) pdc = pvsystem.pvwatts_dc(poa['poa_global'], temp_cell, 100, -0.002) pac = pvsystem.inverter.pvwatts(pdc, 120) - tilt_out, azimuth_out, rsquared = system.orientation_fit_pvwatts( - pac, **clearsky, - solar_zenith=solar_position['zenith'], - solar_azimuth=solar_position['azimuth'] - ) - assert rsquared > 0.9 - assert tilt_out == pytest.approx(tilt, abs=10) - assert azimuth_out == pytest.approx(azimuth, abs=10) - clearsky.dropna(inplace=True) - pac.dropna(inplace=True) solar_position.dropna(inplace=True) + with pytest.raises(ValueError, + match=".* must not contain undefined values"): + system.orientation_fit_pvwatts( + pac, **clearsky, + solar_zenith=solar_position['zenith'], + solar_azimuth=solar_position['azimuth'] + ) + pac.dropna(inplace=True) + with pytest.raises(ValueError, + match=".* must not contain undefined values"): + system.orientation_fit_pvwatts( + pac, **clearsky, + solar_zenith=solar_position['zenith'], + solar_azimuth=solar_position['azimuth'] + ) + clearsky.dropna(inplace=True) tilt_out, azimuth_out, rsquared = system.orientation_fit_pvwatts( pac, **clearsky, solar_zenith=solar_position['zenith'], @@ -564,14 +570,54 @@ def test_orientation_fit_pvwatts_temp_wind_as_series(naive_times): 'open_rack_glass_glass' ] ) + temperature = pd.Series(25, index=clearsky.index) + wind_speed = pd.Series(1, index=clearsky.index) + temperature_missing = temperature.copy() + temperature_missing.loc['4/5/2020':'4/10/2020'] = np.nan + wind_speed_missing = wind_speed.copy() + wind_speed_missing.loc['5/5/2020':'5/15/2020'] = np.nan pdc = pvsystem.pvwatts_dc(poa['poa_global'], temp_cell, 100, -0.002) pac = pvsystem.inverter.pvwatts(pdc, 120) + with pytest.raises(ValueError, + match=".* must not contain undefined values"): + system.orientation_fit_pvwatts( + pac, **clearsky, + solar_zenith=solar_position['zenith'], + solar_azimuth=solar_position['azimuth'], + temperature=temperature_missing, + wind_speed=wind_speed_missing + ) + with pytest.raises(ValueError, + match="temperature must not contain undefined values"): + system.orientation_fit_pvwatts( + pac, **clearsky, + solar_zenith=solar_position['zenith'], + solar_azimuth=solar_position['azimuth'], + temperature=temperature_missing, + wind_speed=wind_speed + ) + with pytest.raises(ValueError, + match="wind_speed must not contain undefined values"): + system.orientation_fit_pvwatts( + pac, **clearsky, + solar_zenith=solar_position['zenith'], + solar_azimuth=solar_position['azimuth'], + temperature=temperature, + wind_speed=wind_speed_missing + ) + # ValueError if indices don't match + with pytest.raises(ValueError): + system.orientation_fit_pvwatts( + pac, **clearsky, + solar_zenith=solar_position['zenith'], + solar_azimuth=solar_position['azimuth'], + temperature=temperature_missing.dropna(), + wind_speed=wind_speed_missing.dropna() + ) tilt_out, azimuth_out, rsquared = system.orientation_fit_pvwatts( pac, **clearsky, - solar_zenith=solar_position['zenith'], solar_azimuth=solar_position['azimuth'], - temperature=pd.Series(25, index=clearsky.index), - wind_speed=pd.Series(1, index=clearsky.index) + solar_zenith=solar_position['zenith'] ) assert rsquared > 0.9 assert tilt_out == pytest.approx(tilt, abs=10) From 092f51170603638858ef45c6d5f2e8ac5230718d Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 20 Oct 2020 15:22:13 -0600 Subject: [PATCH 05/14] Require pvlib>=0.8 Need this for pvlib.inverter.pvwatts() --- requirements.txt | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements.txt b/requirements.txt index 8b8e54a0..6981aca0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ numpy>=1.16.0 pandas>=0.25.0,<1.1.0 -pvlib>=0.7.0 +pvlib>=0.8.0 scipy>=1.3.0 diff --git a/setup.py b/setup.py index 7bf1af55..36417b84 100644 --- a/setup.py +++ b/setup.py @@ -33,7 +33,7 @@ INSTALL_REQUIRES = [ 'numpy >= 1.16.0', 'pandas >= 0.25.1', - 'pvlib >= 0.7.0', + 'pvlib >= 0.8.0', 'scipy >= 1.3.0' ] From f9aef5f7585731f1c8b0a9461399247b127c8a11 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 20 Oct 2020 15:34:39 -0600 Subject: [PATCH 06/14] Use larger default for `temperature_coefficient` --- pvanalytics/system.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pvanalytics/system.py b/pvanalytics/system.py index 702a7773..7ae9e95a 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -482,7 +482,7 @@ def _rsquared(data, residuals): def orientation_fit_pvwatts(power_ac, ghi, dhi, dni, solar_zenith, solar_azimuth, temperature=25, wind_speed=0, - temperature_coefficient=-0.002, + temperature_coefficient=-0.004, temperature_model_parameters=None): """Get the tilt and azimuth that give pvwatts output that most closely fits the data in `power_ac`. From 608b2558fdc65209cedf112a630f3e8c4db274e9 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 20 Oct 2020 15:38:58 -0600 Subject: [PATCH 07/14] Change block comment to docstring --- pvanalytics/system.py | 71 ++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/pvanalytics/system.py b/pvanalytics/system.py index 7ae9e95a..16a871e8 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -412,41 +412,42 @@ def _power_residuals_from_clearsky(system_params, wind_speed, temperature_coefficient, temperature_model_parameters): - # Return the residuals between a system with parameters given in `params` - # and the data in `power_ac`. - # - # Parameters - # ---------- - # system_params : array-like - # array of four floats: tilt, azimuth, DC capacity, and inverter - # DC input limit. - # ghi : Series - # Clear sky GHI - # dhi : Series - # Clear sky DHI - # dni : Series - # Clear sky DNI - # solar_zenith : Series - # Solar zenith at the same times as data in `power_ac` - # solar_azimuth : Series - # Solar azimuth at the same times as data in `power_ac` - # power_ac : Series - # Measured AC power under clear sky conditions. - # temperature : float or Series - # Air temperature at which to model the hypothetical system. If a - # float then a constant temperature is used. If a Series, must have - # the same index as `power_ac`. [C] - # wind_speed : float or Series - # Wind speed. If a float then a constant wind speed is used. If a - # Series, must have the same index as `power_ac`. [m/s] - # temperature_model_parameters : dict - # Parameters fot the cell temperature model. - # - # Returns - # ------- - # Series - # Difference between `power_ac` and the PVWatts output with the - # given parameters. + """Return the residuals between a system with parameters given in `params` + and the data in `power_ac`. + + Parameters + ---------- + system_params : array-like + array of four floats: tilt, azimuth, DC capacity, and inverter + DC input limit. + ghi : Series + Clear sky GHI + dhi : Series + Clear sky DHI + dni : Series + Clear sky DNI + solar_zenith : Series + Solar zenith at the same times as data in `power_ac` + solar_azimuth : Series + Solar azimuth at the same times as data in `power_ac` + power_ac : Series + Measured AC power under clear sky conditions. + temperature : float or Series + Air temperature at which to model the hypothetical system. If a + float then a constant temperature is used. If a Series, must have + the same index as `power_ac`. [C] + wind_speed : float or Series + Wind speed. If a float then a constant wind speed is used. If a + Series, must have the same index as `power_ac`. [m/s] + temperature_model_parameters : dict + Parameters fot the cell temperature model. + + Returns + ------- + Series + Difference between `power_ac` and the PVWatts output with the + given parameters. + """ tilt = system_params[0] azimuth = system_params[1] dc_capacity = system_params[2] From 332b02a600c60a30f8f6e75feaac3d00aeede448 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Tue, 20 Oct 2020 15:40:23 -0600 Subject: [PATCH 08/14] Rename dc_limit to dc_inverter_limit --- pvanalytics/system.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pvanalytics/system.py b/pvanalytics/system.py index 16a871e8..8a58728b 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -451,7 +451,7 @@ def _power_residuals_from_clearsky(system_params, tilt = system_params[0] azimuth = system_params[1] dc_capacity = system_params[2] - dc_limit = system_params[3] + dc_inverter_limit = system_params[3] poa = pvlib.irradiance.get_total_irradiance( tilt, azimuth, solar_zenith, @@ -470,7 +470,7 @@ def _power_residuals_from_clearsky(system_params, dc_capacity, temperature_coefficient ) - return power_ac - pvlib.inverter.pvwatts(pdc, dc_limit) + return power_ac - pvlib.inverter.pvwatts(pdc, dc_inverter_limit) def _rsquared(data, residuals): From 086210e6fd2bb0520d2648ce4f4208f170ce51f1 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 23 Oct 2020 07:34:13 -0600 Subject: [PATCH 09/14] Rename orientation_fit_pvwatts() to infer_orientation_fit_pvwatts() Keeps the names consistent within the system module. --- pvanalytics/system.py | 10 +++--- pvanalytics/tests/test_system.py | 61 ++++++++++++++++++-------------- 2 files changed, 39 insertions(+), 32 deletions(-) diff --git a/pvanalytics/system.py b/pvanalytics/system.py index 8a58728b..e05023bc 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -480,11 +480,11 @@ def _rsquared(data, residuals): return r*r -def orientation_fit_pvwatts(power_ac, ghi, dhi, dni, - solar_zenith, solar_azimuth, - temperature=25, wind_speed=0, - temperature_coefficient=-0.004, - temperature_model_parameters=None): +def infer_orientation_fit_pvwatts(power_ac, ghi, dhi, dni, + solar_zenith, solar_azimuth, + temperature=25, wind_speed=0, + temperature_coefficient=-0.004, + temperature_model_parameters=None): """Get the tilt and azimuth that give pvwatts output that most closely fits the data in `power_ac`. diff --git a/pvanalytics/tests/test_system.py b/pvanalytics/tests/test_system.py index 79ca33ee..2a7cad2e 100644 --- a/pvanalytics/tests/test_system.py +++ b/pvanalytics/tests/test_system.py @@ -472,12 +472,11 @@ def system_power(request, system_location, naive_times): @pytest.mark.slow def test_orientation_fit_pvwatts(system_power): day_mask = system_power['ac'] > 0 - tilt, azimuth, rsquared = system.orientation_fit_pvwatts( + tilt, azimuth, rsquared = system.infer_orientation_fit_pvwatts( system_power['ac'][day_mask], solar_zenith=system_power['solar_position']['zenith'][day_mask], solar_azimuth=system_power['solar_position']['azimuth'][day_mask], - **system_power['clearsky'][day_mask] - ) + **system_power['clearsky'][day_mask]) assert rsquared > 0.9 assert tilt == pytest.approx(system_power['tilt'], abs=10) if system_power['tilt'] == 0: @@ -522,24 +521,27 @@ def test_orientation_fit_pvwatts_missing_data(naive_times): solar_position.dropna(inplace=True) with pytest.raises(ValueError, match=".* must not contain undefined values"): - system.orientation_fit_pvwatts( - pac, **clearsky, + system.infer_orientation_fit_pvwatts( + pac, solar_zenith=solar_position['zenith'], - solar_azimuth=solar_position['azimuth'] + solar_azimuth=solar_position['azimuth'], + **clearsky ) pac.dropna(inplace=True) with pytest.raises(ValueError, match=".* must not contain undefined values"): - system.orientation_fit_pvwatts( - pac, **clearsky, + system.infer_orientation_fit_pvwatts( + pac, solar_zenith=solar_position['zenith'], - solar_azimuth=solar_position['azimuth'] + solar_azimuth=solar_position['azimuth'], + **clearsky ) clearsky.dropna(inplace=True) - tilt_out, azimuth_out, rsquared = system.orientation_fit_pvwatts( - pac, **clearsky, + tilt_out, azimuth_out, rsquared = system.infer_orientation_fit_pvwatts( + pac, solar_zenith=solar_position['zenith'], - solar_azimuth=solar_position['azimuth'] + solar_azimuth=solar_position['azimuth'], + **clearsky ) assert rsquared > 0.9 assert tilt_out == pytest.approx(tilt, abs=10) @@ -580,44 +582,49 @@ def test_orientation_fit_pvwatts_temp_wind_as_series(naive_times): pac = pvsystem.inverter.pvwatts(pdc, 120) with pytest.raises(ValueError, match=".* must not contain undefined values"): - system.orientation_fit_pvwatts( - pac, **clearsky, + system.infer_orientation_fit_pvwatts( + pac, solar_zenith=solar_position['zenith'], solar_azimuth=solar_position['azimuth'], temperature=temperature_missing, - wind_speed=wind_speed_missing + wind_speed=wind_speed_missing, + **clearsky ) with pytest.raises(ValueError, match="temperature must not contain undefined values"): - system.orientation_fit_pvwatts( - pac, **clearsky, + system.infer_orientation_fit_pvwatts( + pac, solar_zenith=solar_position['zenith'], solar_azimuth=solar_position['azimuth'], temperature=temperature_missing, - wind_speed=wind_speed + wind_speed=wind_speed, + **clearsky ) with pytest.raises(ValueError, match="wind_speed must not contain undefined values"): - system.orientation_fit_pvwatts( - pac, **clearsky, + system.infer_orientation_fit_pvwatts( + pac, solar_zenith=solar_position['zenith'], solar_azimuth=solar_position['azimuth'], temperature=temperature, - wind_speed=wind_speed_missing + wind_speed=wind_speed_missing, + **clearsky ) # ValueError if indices don't match with pytest.raises(ValueError): - system.orientation_fit_pvwatts( - pac, **clearsky, + system.infer_orientation_fit_pvwatts( + pac, solar_zenith=solar_position['zenith'], solar_azimuth=solar_position['azimuth'], temperature=temperature_missing.dropna(), - wind_speed=wind_speed_missing.dropna() + wind_speed=wind_speed_missing.dropna(), + **clearsky ) - tilt_out, azimuth_out, rsquared = system.orientation_fit_pvwatts( - pac, **clearsky, + tilt_out, azimuth_out, rsquared = system.infer_orientation_fit_pvwatts( + pac, + solar_zenith=solar_position['zenith'], solar_azimuth=solar_position['azimuth'], - solar_zenith=solar_position['zenith'] + **clearsky ) assert rsquared > 0.9 assert tilt_out == pytest.approx(tilt, abs=10) From 8c5f38cf6127306bd88ddf0af8fd12d9e3f0b49c Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 23 Oct 2020 12:11:12 -0600 Subject: [PATCH 10/14] Improve documentation Co-authored-by: Cliff Hansen --- pvanalytics/system.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/pvanalytics/system.py b/pvanalytics/system.py index e05023bc..a2c6a7b4 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -412,7 +412,7 @@ def _power_residuals_from_clearsky(system_params, wind_speed, temperature_coefficient, temperature_model_parameters): - """Return the residuals between a system with parameters given in `params` + """Return the residuals between a system with parameters given in `system_params` and the data in `power_ac`. Parameters @@ -439,14 +439,22 @@ def _power_residuals_from_clearsky(system_params, wind_speed : float or Series Wind speed. If a float then a constant wind speed is used. If a Series, must have the same index as `power_ac`. [m/s] + temperature_coefficient : float + Temperature coefficient of DC power. [1/C] temperature_model_parameters : dict - Parameters fot the cell temperature model. + Parameters for the cell temperature model. Returns ------- Series Difference between `power_ac` and the PVWatts output with the given parameters. + + Notes + ------ + Uses the defaults in `pvlib.irradiance.get_total_irradiance` to calculated plane-of-array + irradiance, i.e., the isotropic model for sky diffuse irradiance, and the Perez irradiance + transposition model. """ tilt = system_params[0] azimuth = system_params[1] @@ -485,9 +493,12 @@ def infer_orientation_fit_pvwatts(power_ac, ghi, dhi, dni, temperature=25, wind_speed=0, temperature_coefficient=-0.004, temperature_model_parameters=None): - """Get the tilt and azimuth that give pvwatts output that most closely + """Get the tilt and azimuth that give PVWatts output that most closely fits the data in `power_ac`. + Input data `power_ac`, `ghi`, `dhi`, `dni` should reflect clear-sky + conditions. + Uses non-linear least squares to optimize over four free variables to find the values that result in the best fit between power modeled using PVWatts and `power_ac`. The four free variables are @@ -506,11 +517,11 @@ def infer_orientation_fit_pvwatts(power_ac, ghi, dhi, dni, power_ac : Series AC power from the system in clear sky conditions. ghi : Series - Clear sky GHI with same index as `power_ac`. + Clear sky GHI with same index as `power_ac`. [W/m^2] dhi : Series - Clear sky DHI with same index as `power_ac`. + Clear sky DHI with same index as `power_ac`. [W/m^2] dni : Series - Clear sky DNI with same index as `power_ac`. + Clear sky DNI with same index as `power_ac`. [W/m^2] solar_zenith : Series Solar zenith. [degrees] solar_azimuth : Series From af636cd41169b9965ceb55da815e7ec6e6aedf98 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 23 Oct 2020 12:23:04 -0600 Subject: [PATCH 11/14] Add notes on returned values and operating on clipped data --- pvanalytics/system.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/pvanalytics/system.py b/pvanalytics/system.py index a2c6a7b4..6bc1cca0 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -508,6 +508,12 @@ def infer_orientation_fit_pvwatts(power_ac, ghi, dhi, dni, - the DC capacity of the system - the DC input limit of the inverter. + Of these four parameters, only tilt and azimuth are returned. While, DC + capacity and the DC input limit are calculated, their values may not be + accurate. While its value is not returned, because the DC input limit is + used as a free variable for the optimization process, this function + can operate on `power_ac` data that includes inverter clipping. + All parameters passed as a Series must have the same index and must not contain any undefined values (i.e. NaNs). If any input contains NaNs a ValueError is raised. From eebea089f89cc3baaa78c59f76a66e70fafbaa99 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 23 Oct 2020 12:30:20 -0600 Subject: [PATCH 12/14] Reorder parameters to _power_residuals_from_clearsky() --- pvanalytics/system.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/pvanalytics/system.py b/pvanalytics/system.py index 6bc1cca0..a79ba065 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -407,8 +407,9 @@ def infer_orientation_daily_peak(power_or_poa, sunny, tilts, def _power_residuals_from_clearsky(system_params, ghi, dhi, dni, + power_ac, solar_zenith, solar_azimuth, - power_ac, temperature, + temperature, wind_speed, temperature_coefficient, temperature_model_parameters): @@ -426,12 +427,12 @@ def _power_residuals_from_clearsky(system_params, Clear sky DHI dni : Series Clear sky DNI + power_ac : Series + Measured AC power under clear sky conditions. solar_zenith : Series Solar zenith at the same times as data in `power_ac` solar_azimuth : Series Solar azimuth at the same times as data in `power_ac` - power_ac : Series - Measured AC power under clear sky conditions. temperature : float or Series Air temperature at which to model the hypothetical system. If a float then a constant temperature is used. If a Series, must have From 930dfe593b056a8174fcbe98161cbcf2dae5c395 Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 23 Oct 2020 12:31:54 -0600 Subject: [PATCH 13/14] Clean up whitespace --- pvanalytics/system.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/pvanalytics/system.py b/pvanalytics/system.py index a79ba065..94b83a6d 100644 --- a/pvanalytics/system.py +++ b/pvanalytics/system.py @@ -413,8 +413,8 @@ def _power_residuals_from_clearsky(system_params, wind_speed, temperature_coefficient, temperature_model_parameters): - """Return the residuals between a system with parameters given in `system_params` - and the data in `power_ac`. + """Return the residuals between a system with parameters given in + `system_params` and the data in `power_ac`. Parameters ---------- @@ -450,12 +450,12 @@ def _power_residuals_from_clearsky(system_params, Series Difference between `power_ac` and the PVWatts output with the given parameters. - + Notes ------ - Uses the defaults in `pvlib.irradiance.get_total_irradiance` to calculated plane-of-array - irradiance, i.e., the isotropic model for sky diffuse irradiance, and the Perez irradiance - transposition model. + Uses the defaults in :py:func:`pvlib.irradiance.get_total_irradiance` to + calculated plane-of-array irradiance, i.e., the isotropic model for sky + diffuse irradiance, and the Perez irradiance transposition model. """ tilt = system_params[0] azimuth = system_params[1] @@ -499,7 +499,7 @@ def infer_orientation_fit_pvwatts(power_ac, ghi, dhi, dni, Input data `power_ac`, `ghi`, `dhi`, `dni` should reflect clear-sky conditions. - + Uses non-linear least squares to optimize over four free variables to find the values that result in the best fit between power modeled using PVWatts and `power_ac`. The four free variables are From 843346173879f7dc5587ddf3584bc4633485543c Mon Sep 17 00:00:00 2001 From: Will Vining Date: Fri, 23 Oct 2020 13:29:53 -0600 Subject: [PATCH 14/14] Change function name in api.rst --- docs/api.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/api.rst b/docs/api.rst index f5aeaf4f..3d0ed60c 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -241,7 +241,7 @@ power or plane of array irradiance measurements. :toctree: generated/ system.infer_orientation_daily_peak - system.orientation_fit_pvwatts + system.infer_orientation_fit_pvwatts Metrics =======