diff --git a/docs/examples/interactive_examples/interactive_singlediode.py b/docs/examples/interactive_examples/interactive_singlediode.py new file mode 100644 index 0000000000..3fb002bb38 --- /dev/null +++ b/docs/examples/interactive_examples/interactive_singlediode.py @@ -0,0 +1,127 @@ +from bokeh.io import curdoc +from bokeh.layouts import column, row +from bokeh.models import ( + ColumnDataSource, Select, CustomJS, ColorBar, LinearColorMapper +) +from bokeh.transform import transform +from bokeh.plotting import figure, show + +import pandas as pd +import numpy as np +from pvlib import pvsystem + +irradiance_range = np.arange(0, 1300, 50) +temperature_range = np.arange(-10, 70, 10) + +parameters = { + 'alpha_sc': 0.004539, + 'a_ref': 2.6373, + 'I_L_ref': 5.114, + 'I_o_ref': 8.196e-10, + 'R_s': 1.065, + 'R_sh_ref': 381.68, +} + +dataset = [] +for irrad in irradiance_range: + for temp in temperature_range: + + IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( + irrad, + temp, + alpha_sc=parameters['alpha_sc'], + a_ref=parameters['a_ref'], + I_L_ref=parameters['I_L_ref'], + I_o_ref=parameters['I_o_ref'], + R_sh_ref=parameters['R_sh_ref'], + R_s=parameters['R_s'], + EgRef=1.121, + dEgdT=-0.0002677 + ) + curve_info = pvsystem.singlediode( + photocurrent=IL, + saturation_current=I0, + resistance_series=Rs, + resistance_shunt=Rsh, + nNsVth=nNsVth, + ivcurve_pnts=100, + method='lambertw' + ) + data = { + 'Geff': irrad, + 'Tcell': temp, + 'IL': IL, + 'I0': I0, + 'Rs': Rs, + 'Rsh': Rsh, + 'nNsVth': nNsVth, + 'Isc': curve_info['i_sc'], + 'Voc': curve_info['v_oc'], + 'Imp': curve_info['i_mp'], + 'Vmp': curve_info['v_mp'], + 'Pmp': curve_info['p_mp'], + } + dataset.append(data) + +df = pd.DataFrame(dataset) +source = ColumnDataSource(df) + +# default plot is Pmp vs Geff, colored by Tcell +source.data['x'] = source.data['Geff'] +source.data['y'] = source.data['Pmp'] +source.data['c'] = source.data['Tcell'] + +colormapper = LinearColorMapper(palette='Viridis256', + low=df['Tcell'].min(), + high=df['Tcell'].max()) + +tooltips = [(label, "@"+label) for label in df.columns] + +# scatter plot using the 'x', 'y', and 'c' columns +plot = figure(tooltips=tooltips) +plot.circle(source=source, x='x', y='y', + fill_color=transform('c', colormapper), + radius=5, radius_units='screen') +plot.xaxis[0].axis_label = 'Geff' +plot.yaxis[0].axis_label = 'Pmp' + +color_bar = ColorBar(color_mapper=colormapper, location=(0,0)) +plot.add_layout(color_bar, 'right') + +# set the x/y/c values when the user changes the selections +callback = CustomJS(args=dict(source=source, + colormapper=colormapper, + xaxis=plot.xaxis[0], + yaxis=plot.yaxis[0]), + code=""" + var name = cb_obj.title; + var src = cb_obj.value; + if(name == 'X-var:'){ + var dest = 'x'; + xaxis.axis_label = src; + } else if(name == 'Y-var:'){ + var dest = 'y'; + yaxis.axis_label = src; + } else if(name == 'Color-var:'){ + var dest = 'c'; + cmap.low = Math.min(source.data[src]); + cmap.high = Math.max(source.data[src]); + } else { + throw "bad source object name!"; + } + source.data[dest] = source.data[src]; + source.change.emit(); +""") + +# controls: +options = list(df.columns) +xselect = Select(title="X-var:", value="Geff", options=options) +yselect = Select(title="Y-var:", value="Pmp", options=options) +cselect = Select(title="Color-var:", value="Tcell", options=options) + +xselect.js_on_change('value', callback) +yselect.js_on_change('value', callback) +cselect.js_on_change('value', callback) + +layout = row(column(xselect, yselect, cselect, width=100), plot) +show(layout) diff --git a/docs/examples/interactive_examples/interactive_tracking.py b/docs/examples/interactive_examples/interactive_tracking.py new file mode 100644 index 0000000000..5c535312f3 --- /dev/null +++ b/docs/examples/interactive_examples/interactive_tracking.py @@ -0,0 +1,166 @@ +from bokeh.layouts import column, row +from bokeh.models import ( + ColumnDataSource, Slider, CustomJS, Span +) +from bokeh.plotting import figure, show + +import pandas as pd +import numpy as np +import pvlib + +times = pd.date_range('2019-06-01 05:15', '2019-06-01 21:00', + freq='1min', closed='left', tz='US/Eastern') +location = pvlib.location.Location(40, -80) +solpos = location.get_solarposition(times) + +theta = pvlib.tracking.singleaxis( + solpos['zenith'], + solpos['azimuth'], + axis_tilt=0, + axis_azimuth=0, + max_angle=90, + backtrack=True, + gcr=0.5, +)['tracker_theta'].fillna(0) + +# prevent weird shadows at night +solpos['elevation'] = solpos['elevation'].clip(lower=0) + +curves_source = ColumnDataSource(data={ + 'times': times, + 'tracker_theta': theta, # degrees + 'solar_elevation': np.radians(solpos['elevation']), + 'solar_azimuth': np.radians(solpos['azimuth']), +}) + +source = ColumnDataSource(data={ + # backtracking positions + 'tracker1_x': [-1.5, -0.5], + 'tracker1_y': [0.0, 0.0], + 'tracker2_x': [0.5, 1.5], + 'tracker2_y': [0.0, 0.0], + + # ground line + 'ground_x': [-3, 3], + 'ground_y': [-1, -1], + + # racking posts + 'post1_x': [-1, -1], + 'post1_y': [-1, 0], + 'post2_x': [1, 1], + 'post2_y': [-1, 0], +}) + +shadow_source = ColumnDataSource(data={ + 'xs': [[]], + 'ys': [[]], +}) + +# Set up Scene diagram +scene = figure(plot_height=350, plot_width=350, title="Tracker Position", + x_range=[-3, 3], y_range=[-3, 3]) + +plot_args = dict( + source=source, + line_width=3, + line_alpha=1.0, +) + +scene.line('tracker1_x', 'tracker1_y', **plot_args) +scene.line('tracker2_x', 'tracker2_y', **plot_args) + +scene.line('ground_x', 'ground_y', source=source, line_width=1, line_color='black') +scene.line('post1_x', 'post1_y', source=source, line_width=5, line_color='black') +scene.line('post2_x', 'post2_y', source=source, line_width=5, line_color='black') + +scene.patches(xs="xs", ys="ys", fill_color="#333333", alpha=0.2, source=shadow_source) + +# Set up daily tracker angle curve +curves = figure(plot_height=350, plot_width=350, title="Tracker Angle", + x_axis_type='datetime') + +curves_args = dict( + source=curves_source, + line_width=3, + line_alpha=1.0, +) +curves.line('times', 'tracker_theta', **curves_args) + +scrubber = Span(location=0, + dimension='height', line_color='black', + line_dash='dashed', line_width=3) +curves.add_layout(scrubber) + +time_slider = Slider(title="Minute of Day", value=0, start=0, end=len(times)-1, step=1) + +time_slider.callback = CustomJS(args=dict(span=scrubber, + slider=time_slider, + curves_source=curves_source, + position_source=source, + shadow_source=shadow_source), + code=""" + // update time scrubber position and scene geometry + + // update time scrubber position + var idx = slider.value; + span.location = curves_source.data['times'][idx]; + + // update tracker positions + var tracker_theta = curves_source.data['tracker_theta'][idx]; + var dx = 0.5 * Math.cos(3.14159/180 * tracker_theta); + var dy = 0.5 * Math.sin(3.14159/180 * tracker_theta); + var data = position_source.data; + data['tracker1_x'] = [-1 - dx, -1 + dx]; + data['tracker1_y'] = [ 0 - dy, 0 + dy]; + data['tracker2_x'] = [ 1 - dx, 1 + dx]; + data['tracker2_y'] = [ 0 - dy, 0 + dy]; + position_source.change.emit(); + + // update shadow patches + var solar_elevation = curves_source.data['solar_elevation'][idx]; + var solar_azimuth = curves_source.data['solar_azimuth'][idx]; + var z = Math.sin(solar_elevation); + var x = -Math.cos(solar_elevation)*Math.sin(solar_azimuth); + + // shadow length + z = z * 10; + x = x * 10; + + var shadow1_x = [ + data['tracker1_x'][0], + data['tracker1_x'][1], + data['tracker1_x'][1] - x, + data['tracker1_x'][0] - x + ]; + var shadow1_y = [ + data['tracker1_y'][0], + data['tracker1_y'][1], + data['tracker1_y'][1] - z, + data['tracker1_y'][0] - z + ]; + var shadow2_x = [ + data['tracker2_x'][0], + data['tracker2_x'][1], + data['tracker2_x'][1] - x, + data['tracker2_x'][0] - x + ]; + var shadow2_y = [ + data['tracker2_y'][0], + data['tracker2_y'][1], + data['tracker2_y'][1] - z, + data['tracker2_y'][0] - z + ]; + shadow_source.data = { + 'xs': [shadow1_x, shadow2_x], + 'ys': [shadow1_y, shadow2_y], + }; + shadow_source.change.emit(); + +""") + +layout = column( + time_slider, + row(scene, curves) +) + +show(layout) diff --git a/docs/examples/plot_single_axis_tracking.py b/docs/examples/plot_single_axis_tracking.py index 75ea3ba1ca..206ffcbb5e 100644 --- a/docs/examples/plot_single_axis_tracking.py +++ b/docs/examples/plot_single_axis_tracking.py @@ -20,6 +20,7 @@ # towards the sun as much as possible in order to maximize the cross section # presented towards incoming beam irradiance. +# sphinx_gallery_thumbnail_number = 2 from pvlib import solarposition, tracking import pandas as pd import matplotlib.pyplot as plt @@ -75,3 +76,12 @@ plt.legend() plt.show() + +#%% +# Interactive Demo +# ---------------- +# +# Drag the slider below to see how backtracking in morning and evening keeps +# each row's shadows tangent to the next row without any overlap. +# +# .. bokeh-plot:: ../../examples/interactive_examples/interactive_tracking.py diff --git a/docs/examples/plot_singlediode.py b/docs/examples/plot_singlediode.py new file mode 100644 index 0000000000..c0574764a3 --- /dev/null +++ b/docs/examples/plot_singlediode.py @@ -0,0 +1,123 @@ +""" +Single-diode equation +===================== + +Examples of modeling IV curves using a single-diode circuit equivalent model. +""" + +#%% +# Calculating a module IV curve for certain operating conditions is a two-step +# process. Multiple methods exist for both parts of the process. Here we use +# the De Soto 5-parameter model to calculate the electrical characteristics +# of a PV module at a certain irradiance and temperature using the module's +# base characteristics at reference conditions. Those parameters are then used +# to calculate the module's IV curve by solving the single-diode equation using +# the Lambert W method. +# +# The single-diode equation is a circuit-equivalent model of a PV +# cell and has five electrical parameters that depend on the operating +# conditions. For more details on the single-diode equation and the five +# parameters, see the `PVPMC single diode page +# `_. +# +# Calculating IV Curves +# ----------------------- +# This example uses :py:meth:`pvlib.pvsystem.calcparams_desoto` to calculate +# the 5 electrical parameters needed to solve the single-diode equation. +# :py:meth:`pvlib.pvsystem.singlediode` is then used to generate the IV curves. + +from pvlib import pvsystem +import pandas as pd +import matplotlib.pyplot as plt + +# Example module parameters for the Canadian Solar CS5P-220M: +parameters = { + 'Name': 'Canadian Solar CS5P-220M', + 'BIPV': 'N', + 'Date': '10/5/2009', + 'T_NOCT': 42.4, + 'A_c': 1.7, + 'N_s': 96, + 'I_sc_ref': 5.1, + 'V_oc_ref': 59.4, + 'I_mp_ref': 4.69, + 'V_mp_ref': 46.9, + 'alpha_sc': 0.004539, + 'beta_oc': -0.22216, + 'a_ref': 2.6373, + 'I_L_ref': 5.114, + 'I_o_ref': 8.196e-10, + 'R_s': 1.065, + 'R_sh_ref': 381.68, + 'Adjust': 8.7, + 'gamma_r': -0.476, + 'Version': 'MM106', + 'PTC': 200.1, + 'Technology': 'Mono-c-Si', +} + +times = pd.date_range(start='2015-06-01 10:00', periods=3, freq='h') +effective_irradiance = pd.Series([800, 600.0, 400.0], index=times) +temp_cell = pd.Series([60, 40, 20], index=times) + +# adjust the reference parameters according to the operating +# conditions using the De Soto model: +IL, I0, Rs, Rsh, nNsVth = pvsystem.calcparams_desoto( + effective_irradiance, + temp_cell, + alpha_sc=parameters['alpha_sc'], + a_ref=parameters['a_ref'], + I_L_ref=parameters['I_L_ref'], + I_o_ref=parameters['I_o_ref'], + R_sh_ref=parameters['R_sh_ref'], + R_s=parameters['R_s'], + EgRef=1.121, + dEgdT=-0.0002677 +) + +# plug the parameters into the SDE and solve for IV curves: +curve_info = pvsystem.singlediode( + photocurrent=IL, + saturation_current=I0, + resistance_series=Rs, + resistance_shunt=Rsh, + nNsVth=nNsVth, + ivcurve_pnts=100, + method='lambertw' +) + +# plot the calculated curves: +plt.figure() +for i in range(len(times)): + label = ( + "$G_{eff}=" + f"{effective_irradiance[i]}$ $W/m^2$; " + "$T_{cell}=" + f"{temp_cell[i]}$ C" + ) + plt.plot(curve_info['v'][i], curve_info['i'][i], label=label) + v_mp = curve_info['v_mp'][i] + i_mp = curve_info['i_mp'][i] + plt.plot([v_mp], [i_mp], ls='', marker='o', c='k') + +plt.legend() +plt.xlabel('Module voltage [V]') +plt.ylabel('Module current [A]') +plt.title(parameters['Name']) +plt.show() + +print(pd.DataFrame({ + 'i_sc': curve_info['i_sc'], + 'v_oc': curve_info['v_oc'], + 'i_mp': curve_info['i_mp'], + 'v_mp': curve_info['v_mp'], + 'p_mp': curve_info['p_mp'], +})) + +#%% +# Interactive Demo +# ---------------- +# +# The plot below shows the main IV curve points from sweeping across irradiance +# and temperature. Change the dropdowns to scatter different variables against +# each other. +# +# .. bokeh-plot:: ../../examples/interactive_examples/interactive_singlediode.py diff --git a/docs/sphinx/source/conf.py b/docs/sphinx/source/conf.py index 524a77a048..415952d7e6 100644 --- a/docs/sphinx/source/conf.py +++ b/docs/sphinx/source/conf.py @@ -60,6 +60,7 @@ def __getattr__(cls, name): 'IPython.sphinxext.ipython_directive', 'IPython.sphinxext.ipython_console_highlighting', 'sphinx_gallery.gen_gallery', + 'bokeh.sphinxext.bokeh_plot', ] napoleon_use_rtype = False # group rtype on same line together with return @@ -332,9 +333,7 @@ def setup(app): # settings for sphinx-gallery sphinx_gallery_conf = { 'examples_dirs': ['../../examples'], # location of gallery scripts - 'gallery_dirs': ['auto_examples'], # location of generated output - # sphinx-gallery only shows plots from plot_*.py files by default: - # 'filename_pattern': '*.py', + 'gallery_dirs': ['gallery'], # location of generated output } # supress warnings in gallery output # https://sphinx-gallery.github.io/stable/configuration.html diff --git a/docs/sphinx/source/index.rst b/docs/sphinx/source/index.rst index 06b7578a06..1f79d8d489 100644 --- a/docs/sphinx/source/index.rst +++ b/docs/sphinx/source/index.rst @@ -81,7 +81,7 @@ Contents package_overview introtutorial - auto_examples/index + gallery/index whatsnew installation contributing diff --git a/setup.py b/setup.py index b52a8ce731..d6dffaf050 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ 'optional': ['ephem', 'cython', 'netcdf4', 'nrel-pysam', 'numba', 'pvfactors', 'scipy', 'siphon', 'tables'], 'doc': ['ipython', 'matplotlib', 'sphinx == 1.8.5', 'sphinx_rtd_theme', - 'sphinx-gallery'], + 'sphinx-gallery', 'bokeh'], 'test': TESTS_REQUIRE } EXTRAS_REQUIRE['all'] = sorted(set(sum(EXTRAS_REQUIRE.values(), [])))