Skip to content
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
51 changes: 51 additions & 0 deletions docs/RdAnalysis_example.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -721,6 +721,57 @@
" scatter_ymin=0.5, scatter_ymax=1.1,\n",
" hist_xmin=-30, hist_xmax=45);"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Substituting a different modeled power\n",
"Incidentally, you can optionally run the analysis while normalizing with your own calculated expected power. By default, `RdAnalysis` normalizes by the included `pvwatts_dc_power` calculation. To override with a different value, pass in `RdAnalysis.power_expected`. You can substitute any modeled power here, like from SAM or PVSyst or PVLib. "
]
},
{
"cell_type": "code",
"execution_count": 23,
"metadata": {},
"outputs": [],
"source": [
"power_expected = rdtools.normalization.pvwatts_dc_power(poa_global=rd.poa,\n",
" power_dc_rated=meta['power_dc_rated'], \n",
" temperature_cell=rd.cell_temperature,\n",
" gamma_pdc=meta['gamma_pdc'])\n",
"\n",
"rd_with_expected_power = rdtools.RdAnalysis(pv=df['power'], poa=df['poa'], \n",
" power_expected = power_expected,\n",
" interp_freq=freq,\n",
" max_timedelta=pd.to_timedelta('15 minutes'),\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 24,
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"-0.44132287274254445\n"
]
}
],
"source": [
"rd_with_expected_power.sensor_analysis(analyses=['yoy_degradation']) # This step will run using power_expected if it's defined.\n",
"print(rd.results['sensor']['yoy_degradation']['p50_rd']) "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Results are the same as above"
]
}
],
"metadata": {
Expand Down
1 change: 0 additions & 1 deletion docs/sphinx/source/changelog/v2.1.0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ API Changes
-----------
* Add :py:class:`~rdtools.analysis.RdAnalysis` class for single-line analysis. (:pull:`117`).


Enhancements
------------
* Add new :py:mod:`~rdtools.analysis` module to focus on single-line analysis workflow
Expand Down
58 changes: 47 additions & 11 deletions rdtools/analysis.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ class RdAnalysis():
of 0 neglects the wind in this calculation
albedo : numeric
Albedo to be used in irradiance transposition calculations
power_expected : pd.Series
Right-labeled time series of expected PV power. (Note: Expected energy
is not supported.)
temperature_model : str
Model parameter pvlib.pvsystem.sapm_celltemp() used in calculating cell
temperature from ambient
Expand Down Expand Up @@ -87,7 +90,8 @@ class RdAnalysis():
def __init__(self, pv, poa=None, cell_temperature=None, ambient_temperature=None,
temperature_coefficient=None, aggregation_freq='D', pv_input='power', pvlib_location=None,
clearsky_poa=None, clearsky_cell_temperature=None, clearsky_ambient_temperature=None,
windspeed=0, albedo=0.25, temperature_model=None, pv_azimuth=None, pv_tilt=None,
windspeed=0, albedo=0.25, power_expected=None, temperature_model=None,
pv_azimuth=None, pv_tilt=None,
pv_nameplate=None, interp_freq=None, max_timedelta=None):

if interp_freq is not None:
Expand All @@ -98,6 +102,8 @@ def __init__(self, pv, poa=None, cell_temperature=None, ambient_temperature=None
cell_temperature = normalization.interpolate(cell_temperature, interp_freq, max_timedelta)
if ambient_temperature is not None:
ambient_temperature = normalization.interpolate(ambient_temperature, interp_freq, max_timedelta)
if power_expected is not None:
power_expected = normalization.interpolate(power_expected, interp_freq, max_timedelta)
if clearsky_poa is not None:
clearsky_poa = normalization.interpolate(clearsky_poa, interp_freq, max_timedelta)
if clearsky_cell_temperature is not None:
Expand Down Expand Up @@ -128,12 +134,14 @@ def __init__(self, pv, poa=None, cell_temperature=None, ambient_temperature=None
self.clearsky_poa = clearsky_poa
self.windspeed = windspeed
self.albedo = albedo
self.power_expected = power_expected
self.temperature_model = temperature_model
self.pv_azimuth = pv_azimuth
self.pv_tilt = pv_tilt
self.pv_nameplate = pv_nameplate
self.results = {}


# Initialize to use default filter parameters
self.filter_params = {
'normalized_filter': {},
Expand All @@ -143,6 +151,9 @@ def __init__(self, pv, poa=None, cell_temperature=None, ambient_temperature=None
'csi_filter': {},
'ad_hoc_filter': None # use this to include an explict filter
}
# remove tcell_filter from list if power_expected is passed in
if power_expected is not None and cell_temperature is None:
del self.filter_params['tcell_filter']

def calc_clearsky_poa(self, times=None, rescale=True, **kwargs):
'''
Expand Down Expand Up @@ -355,6 +366,18 @@ def filter(self, energy_normalized, case):
elif case == 'clearsky':
self.clearsky_filter = bool_filter

def _filter_check(self, post_filter):
'''
post-filter check for requisite 730 days of data

Parameters
----------
post_filter : pandas.Series
Time series filtered by boolean output from self.filter
'''
if post_filter.empty or post_filter.index[-1] - post_filter.index[0] < pd.Timedelta('730d'):
raise ValueError("Less than two years of data left after filtering")

def aggregate(self, energy_normalized, insolation):
'''
Return insolation-weighted normalized PV energy and the associated aggregated insolation
Expand Down Expand Up @@ -400,7 +423,7 @@ def yoy_degradation(self, aggregated, **kwargs):
'calc_info': Dict of detailed results
(see degradation.degradation_year_on_year() docs)
'''

self._filter_check(aggregated)
yoy_rd, yoy_ci, yoy_info = degradation.degradation_year_on_year(aggregated, **kwargs)

yoy_results = {
Expand Down Expand Up @@ -455,23 +478,32 @@ def srr_soiling(self, aggregated, aggregated_insolation, **kwargs):

def sensor_preprocess(self):
'''
Perform sensor-based normalization, filtering, and aggregation work flow.
Perform sensor-based normalization, filtering, and aggregation.
If optional parameter self.power_expected is passed in,
normalize_with_expected_power will be used instead of pvwatts.
'''
if self.poa is None:
raise ValueError('poa must be available to perform sensor_preprocess')
if self.cell_temperature is None and self.ambient_temperature is None:
raise ValueError('either cell or ambient temperature must be available to perform sensor_preprocess')
if self.cell_temperature is None:
self.cell_temperature = self.calc_cell_temperature(self.poa, self.windspeed, self.ambient_temperature)
energy_normalized, insolation = self.pvwatts_norm(self.poa, self.cell_temperature)

if self.power_expected is None:
# Thermal details required if power_expected is not manually set.
if self.cell_temperature is None and self.ambient_temperature is None:
raise ValueError('either cell or ambient temperature must be available to perform sensor_preprocess')
if self.cell_temperature is None:
self.cell_temperature = self.calc_cell_temperature(self.poa, self.windspeed, self.ambient_temperature)
energy_normalized, insolation = self.pvwatts_norm(self.poa, self.cell_temperature)
else: # self.power_expected passed in by user
energy_normalized, insolation = normalization.normalize_with_expected_power(self.pv_energy, self.power_expected, self.poa, pv_input='energy')
self.filter(energy_normalized, 'sensor')
aggregated, aggregated_insolation = self.aggregate(energy_normalized[self.sensor_filter], insolation[self.sensor_filter])
self.sensor_aggregated_performance = aggregated
self.sensor_aggregated_insolation = aggregated_insolation

def clearsky_preprocess(self):
'''
Perform clear-sky-based normalization, filtering, and aggregation work flow
Perform clear-sky-based normalization, filtering, and aggregation.
If optional parameter self.power_expected is passed in,
normalize_with_expected_power will be used instead of pvwatts.
'''
if self.clearsky_poa is None:
self.calc_clearsky_poa(model='isotropic')
Expand All @@ -480,15 +512,19 @@ def clearsky_preprocess(self):
self.calc_clearsky_tamb()
self.clearsky_cell_temperature = self.calc_cell_temperature(self.clearsky_poa, 0, self.clearsky_ambient_temperature)
# Note example notebook uses windspeed=0 in the clearskybranch
cs_normalized, cs_insolation = self.pvwatts_norm(self.clearsky_poa, self.clearsky_cell_temperature)
if self.power_expected is None:
cs_normalized, cs_insolation = self.pvwatts_norm(self.clearsky_poa, self.clearsky_cell_temperature)
else: # self.power_expected passed in by user
cs_normalized, cs_insolation = normalization.normalize_with_expected_power(self.pv_energy, self.power_expected, self.clearsky_poa, pv_input='energy')
self.filter(cs_normalized, 'clearsky')
cs_aggregated, cs_aggregated_insolation = self.aggregate(cs_normalized[self.clearsky_filter], cs_insolation[self.clearsky_filter])
self.clearsky_aggregated_performance = cs_aggregated
self.clearsky_aggregated_insolation = cs_aggregated_insolation

def sensor_analysis(self, analyses=['yoy_degradation'], yoy_kwargs={}, srr_kwargs={}):
'''
Perform entire sensor-based analysis workflow. Results are stored in self.results['sensor']
Perform entire sensor-based analysis workflow.
Results are stored in self.results['sensor']

Parameters
---------
Expand Down
17 changes: 16 additions & 1 deletion rdtools/test/analysis_test.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from rdtools import RdAnalysis
from rdtools import RdAnalysis, normalization
from soiling_test import normalized_daily, times
from plotting_test import assert_isinstance
import pytest
Expand Down Expand Up @@ -55,6 +55,14 @@ def sensor_analysis(sensor_parameters):
rd_analysis.sensor_analysis(analyses=['yoy_degradation'])
return rd_analysis

@pytest.fixture
def sensor_analysis_exp_power(sensor_parameters):
power_expected = normalization.pvwatts_dc_power(sensor_parameters['poa'],
power_dc_rated=1)
sensor_parameters['power_expected']=power_expected
rd_analysis = RdAnalysis(**sensor_parameters)
rd_analysis.sensor_analysis(analyses=['yoy_degradation'])
return rd_analysis

def test_sensor_analysis(sensor_analysis):
yoy_results = sensor_analysis.results['sensor']['yoy_degradation']
Expand All @@ -64,6 +72,13 @@ def test_sensor_analysis(sensor_analysis):
assert -1 == pytest.approx(rd, abs=1e-2)
assert [-1, -1] == pytest.approx(ci, abs=1e-2)

def test_sensor_analysis_exp_power(sensor_analysis_exp_power):
yoy_results = sensor_analysis_exp_power.results['sensor']['yoy_degradation']
rd = yoy_results['p50_rd']
ci = yoy_results['rd_confidence_interval']

assert 0 == pytest.approx(rd, abs=1e-2)
assert [0, 0] == pytest.approx(ci, abs=1e-2)

@pytest.fixture
def clearsky_parameters(basic_parameters, sensor_parameters,
Expand Down