-
Notifications
You must be signed in to change notification settings - Fork 1.1k
pvfactors limited implementation for bifacial calculations #635
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
Changes from 13 commits
80f910f
8acfd58
2e982d0
199ad02
397c850
2168e91
1c976e3
a340c20
fd3adf3
6435ce3
381000c
74b20af
89bad6c
81b8887
418ca72
e93c460
ef203f0
7b62e4a
330660a
7cd7cf9
cff21dd
160419a
bd5078b
cbc7434
9829b81
9970179
608863f
78756cd
55dd3a6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
""" | ||
The ``bifacial`` module contains functions for modeling back surface | ||
plane-of-array irradiance under various conditions. | ||
""" | ||
|
||
import pandas as pd | ||
from pvlib.tools import enforce_numpy_arrays | ||
|
||
|
||
@enforce_numpy_arrays | ||
def pvfactors_timeseries( | ||
solar_azimuth, solar_zenith, surface_azimuth, surface_tilt, | ||
timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, albedo, | ||
n_pvrows=3, index_observed_pvrow=1, | ||
rho_front_pvrow=0.03, rho_back_pvrow=0.05, | ||
horizon_band_angle=15., | ||
run_parallel_calculations=True, n_workers_for_parallel_calcs=None): | ||
""" | ||
Calculate front and back surface plane-of-array irradiance on | ||
a fixed tilt or single-axis tracker PV array configuration, and using | ||
the open-source "pvfactors" package. | ||
Please refer to pvfactors online documentation for more details: | ||
https://sunpower.github.io/pvfactors/ | ||
|
||
Inputs | ||
------ | ||
solar_azimuth: numeric | ||
Sun's azimuth angles using pvlib's azimuth convention (deg) | ||
solar_zenith: numeric | ||
Sun's zenith angles (deg) | ||
surface_azimuth: numeric | ||
Azimuth angle of the front surface of the PV modules, using pvlib's | ||
convention (deg) | ||
surface_tilt: numeric | ||
Tilt angle of the PV modules, going from 0 to 180 (deg) | ||
timestamps: array of :class:datetime.datetime objects | ||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
List of simulation timestamps | ||
dni: numeric | ||
Direct normal irradiance (W/m2) | ||
dhi: numeric | ||
Diffuse horizontal irradiance (W/m2) | ||
gcr: float | ||
Ground coverage ratio of the pv array | ||
pvrow_height: float | ||
Height of the pv rows, measured at their center (m) | ||
pvrow_width: float | ||
Width of the pv rows in the considered 2D plane (m) | ||
albedo: float | ||
Ground albedo | ||
n_pvrows: int, default 3 | ||
Number of PV rows to consider in the PV array | ||
index_observed_pvrow: int, default 1 | ||
Index of the PV row whose incident irradiance will be returned. Indices | ||
of PV rows go from 0 to n_pvrows-1. | ||
rho_front_pvrow: float, default 0.03 | ||
Front surface reflectivity of PV rows | ||
rho_back_pvrow: float, default 0.05 | ||
Back surface reflectivity of PV rows | ||
horizon_band_angle: float, default 15 | ||
Elevation angle of the sky dome's diffuse horizon band (deg) | ||
run_parallel_calculations: bool, default True | ||
pvfactors is capable of using multiprocessing. Use this flag to decide | ||
to run calculations in parallel (recommended) or not. | ||
n_workers_for_parallel_calcs: int, default None | ||
Number of workers to use in the case of parallel calculations. The | ||
default value of 'None' will lead to using a value equal to the number | ||
of CPU's on the machine running the model. | ||
|
||
Returns | ||
------- | ||
front_poa_irradiance: numeric | ||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
Calculated incident irradiance on the front surface of the PV modules | ||
(W/m2) | ||
back_poa_irradiance: numeric | ||
Calculated incident irradiance on the back surface of the PV modules | ||
(W/m2) | ||
df_registries: pandas DataFrame | ||
DataFrame containing all the detailed outputs and elements calculated | ||
for every timestamp of the simulation. Please refer to pvfactors | ||
documentation for more details | ||
|
||
References | ||
---------- | ||
.. [1] Anoma, Marc Abou, et al. "View Factor Model and Validation for | ||
Bifacial PV and Diffuse Shade on Single-Axis Trackers." 44th IEEE | ||
Photovoltaic Specialist Conference. 2017. | ||
""" | ||
|
||
# Import pvfactors functions for timeseries calculations. | ||
from pvfactors.timeseries import (calculate_radiosities_parallel_perez, | ||
calculate_radiosities_serially_perez, | ||
get_average_pvrow_outputs) | ||
idx_slice = pd.IndexSlice | ||
|
||
# Build up pv array configuration parameters | ||
pvarray_parameters = { | ||
'n_pvrows': n_pvrows, | ||
'pvrow_height': pvrow_height, | ||
'pvrow_width': pvrow_width, | ||
'surface_azimuth': surface_azimuth[0], # not necessary | ||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
'surface_tilt': surface_tilt[0], # not necessary | ||
'gcr': gcr, | ||
'solar_zenith': solar_zenith[0], # not necessary | ||
'solar_azimuth': solar_azimuth[0], # not necessary | ||
'rho_ground': albedo, | ||
'rho_front_pvrow': rho_front_pvrow, | ||
'rho_back_pvrow': rho_back_pvrow, | ||
'horizon_band_angle': horizon_band_angle | ||
} | ||
|
||
# Run pvfactors calculations: either in parallel or serially | ||
if run_parallel_calculations: | ||
df_registries, df_custom_perez = calculate_radiosities_parallel_perez( | ||
pvarray_parameters, timestamps, solar_zenith, solar_azimuth, | ||
surface_tilt, surface_azimuth, dni, dhi, | ||
n_processes=n_workers_for_parallel_calcs) | ||
else: | ||
inputs = (pvarray_parameters, timestamps, solar_zenith, solar_azimuth, | ||
surface_tilt, surface_azimuth, dni, dhi) | ||
df_registries, df_custom_perez = calculate_radiosities_serially_perez( | ||
inputs) | ||
|
||
# Get the average surface outputs | ||
df_outputs = get_average_pvrow_outputs(df_registries, | ||
values=['qinc'], | ||
include_shading=True) | ||
|
||
# Select the calculated outputs from the pvrow to observe | ||
ipoa_front = df_outputs.loc[:, idx_slice[index_observed_pvrow, | ||
'front', 'qinc']].values | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. perhaps only a question because I'm not familiar enough with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I just believed that having a 1D numpy array was simpler for these results, but I can adjust it to |
||
|
||
ipoa_back = df_outputs.loc[:, idx_slice[index_observed_pvrow, | ||
'back', 'qinc']].values | ||
|
||
return ipoa_front, ipoa_back, df_registries |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
import numpy as np | ||
from datetime import datetime | ||
from pvlib.bifacial import pvfactors_timeseries | ||
from conftest import (requires_pvfactors, requires_future, requires_shapely, | ||
anomam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
requires_scipy) | ||
|
||
|
||
@requires_scipy | ||
@requires_shapely | ||
@requires_future | ||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
@requires_pvfactors | ||
def test_pvfactors_timeseries(): | ||
""" Test that pvfactors is functional, using the TLDR section inputs of the | ||
package github repo README.md file""" | ||
|
||
# Create some inputs | ||
timestamps = np.array([datetime(2017, 8, 31, 11), | ||
datetime(2017, 8, 31, 12)]) | ||
solar_zenith = np.array([20., 10.]) | ||
solar_azimuth = np.array([110., 140.]) | ||
surface_tilt = np.array([10., 0.]) | ||
surface_azimuth = np.array([90., 90.]) | ||
dni = np.array([1000., 300.]) | ||
dhi = np.array([50., 500.]) | ||
gcr = 0.4 | ||
pvrow_height = 1.75 | ||
pvrow_width = 2.44 | ||
albedo = 0.2 | ||
n_pvrows = 3 | ||
index_observed_pvrow = 1 | ||
rho_front_pvrow = 0.03 | ||
rho_back_pvrow = 0.05 | ||
horizon_band_angle = 15. | ||
|
||
# Expected values | ||
expected_ipoa_front = [1034.96216923, 795.4423259] | ||
expected_ipoa_back = [92.11871485, 70.39404124] | ||
tolerance = 1e-6 | ||
|
||
# Test serial calculations | ||
ipoa_front, ipoa_back, _ = pvfactors_timeseries( | ||
solar_azimuth, solar_zenith, surface_azimuth, surface_tilt, | ||
timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, albedo, | ||
n_pvrows=n_pvrows, index_observed_pvrow=index_observed_pvrow, | ||
rho_front_pvrow=rho_front_pvrow, rho_back_pvrow=rho_back_pvrow, | ||
horizon_band_angle=horizon_band_angle, | ||
run_parallel_calculations=False, n_workers_for_parallel_calcs=None) | ||
|
||
np.testing.assert_allclose(ipoa_front, expected_ipoa_front, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This should use the pandas test functions if we adopt the always return a Series approach |
||
atol=0, rtol=tolerance) | ||
np.testing.assert_allclose(ipoa_back, expected_ipoa_back, | ||
atol=0, rtol=tolerance) | ||
|
||
# Run calculations in parallel | ||
ipoa_front, ipoa_back, _ = pvfactors_timeseries( | ||
solar_azimuth, solar_zenith, surface_azimuth, surface_tilt, | ||
timestamps, dni, dhi, gcr, pvrow_height, pvrow_width, albedo, | ||
n_pvrows=n_pvrows, index_observed_pvrow=index_observed_pvrow, | ||
rho_front_pvrow=rho_front_pvrow, rho_back_pvrow=rho_back_pvrow, | ||
horizon_band_angle=horizon_band_angle, | ||
run_parallel_calculations=True, n_workers_for_parallel_calcs=None) | ||
|
||
np.testing.assert_allclose(ipoa_front, expected_ipoa_front, | ||
atol=0, rtol=tolerance) | ||
np.testing.assert_allclose(ipoa_back, expected_ipoa_back, | ||
atol=0, rtol=tolerance) |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -8,6 +8,7 @@ | |
import numpy as np | ||
import pandas as pd | ||
import pytz | ||
from functools import wraps | ||
anomam marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
|
||
def cosd(angle): | ||
|
@@ -425,3 +426,58 @@ def _golden_sect_DataFrame(params, VL, VH, func): | |
raise Exception("EXCEPTION:iterations exceeded maximum (50)") | ||
|
||
return func(df, 'V1'), df['V1'] | ||
|
||
|
||
def enforce_numpy_arrays(f): | ||
""" This decorator function will convert any inputted pandas dataframe or | ||
series to a numpy array, and make sure to convert outtputed numpy arrays | ||
to pandas series if any input was a pandas structure """ | ||
|
||
@wraps(f) | ||
def wrapper(*args, **kwargs): | ||
# Convert all inputs to numpy arrays if pandas series or dataframe | ||
is_return_type_pandas = False | ||
|
||
new_args = [] | ||
for arg in args: | ||
if isinstance(arg, pd.Series): | ||
is_return_type_pandas = True | ||
new_arg = arg.values | ||
new_args.append(new_arg) | ||
elif isinstance(arg, pd.DataFrame): | ||
is_return_type_pandas = True | ||
new_arg = arg.values.ravel() # make sure that 1D | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's better to raise an exception than allow a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks, I removed the decorator function and implemented checks in the pvfactors function |
||
new_args.append(new_arg) | ||
else: | ||
new_args.append(arg) | ||
new_args = tuple(new_args) | ||
|
||
new_kwargs = {} | ||
for key, val in kwargs.items(): | ||
if isinstance(val, pd.DataFrame) or isinstance(val, pd.Series): | ||
is_return_type_pandas = True | ||
new_val = val.values | ||
new_kwargs[key] = new_val | ||
else: | ||
new_kwargs[key] = val | ||
|
||
# Run function | ||
outputs = f(*new_args, **new_kwargs) | ||
|
||
# If inputs were provided as pandas series or dataframe, convert | ||
# outputted numpy arrays to pandas series | ||
new_outputs = [] | ||
if is_return_type_pandas: | ||
for output in outputs: | ||
if isinstance(output, np.ndarray): | ||
new_output = pd.Series(output) | ||
cwhanse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
new_outputs.append(new_output) | ||
else: | ||
new_outputs.append(output) | ||
new_outputs = tuple(new_outputs) | ||
else: | ||
new_outputs = outputs | ||
|
||
return new_outputs | ||
|
||
return wrapper |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@anomam do you happen to remember why you chose this particular default value for
horizon_band_angle
? From what I can tell in the git history, the analogous default on the pvfactors side was 6.5 (both at the time of this PR and today) rather than 15. I know this PR is ancient history at this point so I'm not expecting much, just curious :)There are now some small differences between this wrapper and pvfactors itself in the
rho_*
parameters as well, but I think those were only introduced later on after this PR was merged.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
hey @kanderso-nrel ! It was a long time ago so I'm not entirely sure anymore, but what I remember is that these values might have needed a bit of tuning to match the measurements depending on the sites etc. I think for the horizon band it might be safe to check what the Perez paper says about what value to use.
For the reflectivity parameter values, it's something I actually never really had time to study in depth. I think the default values for the PV rows were low enough to not have much impact, but that's probably not accurate in all cases...
Sorry if my answers are vague, I think getting better ones would require more work/studies/experience...