From 99ea8821e90a2d39f206625009066fe83acbe046 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 18 May 2020 11:08:15 -0600 Subject: [PATCH 01/11] Create plot_partial_module_shading_simple.py --- .../plot_partial_module_shading_simple.py | 196 ++++++++++++++++++ 1 file changed, 196 insertions(+) create mode 100644 docs/examples/plot_partial_module_shading_simple.py diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/plot_partial_module_shading_simple.py new file mode 100644 index 0000000000..bc06e78dcf --- /dev/null +++ b/docs/examples/plot_partial_module_shading_simple.py @@ -0,0 +1,196 @@ +""" +Calculating the effect of partial module shading +================================================ + +Example of modeling cell-to-cell mismatch loss from partial module shading. +""" + +# %% +# Even though the PV cell is the primary power generation unit, PV modeling is +# often done at the module level for simplicity because module-level parameters +# are much more available and it significantly reduces the computational scope +# of the simulation. However, module-level simulations are too coarse to be +# able to model effects like cell to cell mismatch or partial shading. This +# example calculates cell-level IV curves and combines them to reconstruct +# the module-level IV curve. It uses this approach to find the maximum power +# under various shading and irradiance conditions. +# +# The primary functions used here are: +# - :py:meth:`pvlib.pvsystem.calcparams_desoto` to estimate the SDE parameters +# at the operating conditions +# - :py:meth:`pvlib.singlediode.bishop88` to calculate the full cell IV curve, +# including the reverse bias region. +# +# .. note:: +# +# This example requires the reverse bias functionality added in pvlib 0.7.2 +# +# .. warning:: +# +# Modeling partial module shading is complicated and depends significantly +# on the module's electrical topology. This example makes some simplifying +# assumptions that are not generally applicable. For instance, it assumes +# that all of the module's cell strings perform identically, making it +# possible to ignore the effect of bypass diodes. + +from pvlib import pvsystem, singlediode +import pandas as pd +import numpy as np +from scipy.interpolate import interp1d +import matplotlib.pyplot as plt + +kb = 1.380649e-23 # J/K +qe = 1.602176634e-19 # C +Vth = kb * (273.15+25) / qe + +parameters = { + 'I_L_ref': 8.24, + 'I_o_ref': 2.36e-9, + 'a_ref': 1.3*Vth, + 'R_sh_ref': 1000, + 'R_s': 0.00181, + 'alpha_sc': 0.0042, + 'breakdown_factor': 2e-3, + 'breakdown_exp': 3, + 'breakdown_voltage': -15, +} + + +def simulate_full_curve(parameters, Geff, Tcell, method='brentq', + ivcurve_pnts=1000): + """ + Use De Soto and Bishop to simulate a full IV curve with both + forward and reverse bias regions. + """ + + # adjust the reference parameters according to the operating + # conditions using the De Soto model: + sde_args = pvsystem.calcparams_desoto( + Geff, + Tcell, + 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'], + ) + # sde_args has values: + # (photocurrent, saturation_current, resistance_series, + # resistance_shunt, nNsVth) + + # Use Bishop's method to calculate points on the IV curve with V ranging + # from the reverse breakdown voltage to open circuit + kwargs = { + 'breakdown_factor': parameters['breakdown_factor'], + 'breakdown_exp': parameters['breakdown_exp'], + 'breakdown_voltage': parameters['breakdown_voltage'], + } + v_oc = singlediode.bishop88_v_from_i( + 0.0, *sde_args, method=method, **kwargs + ) + vd = np.linspace(0.99*kwargs['breakdown_voltage'], v_oc, ivcurve_pnts) + + ivcurve_i, ivcurve_v, _ = singlediode.bishop88(vd, *sde_args, **kwargs) + return pd.DataFrame({ + 'i': ivcurve_i, + 'v': ivcurve_v, + }) + + +def interpolate(df, i): + """convenience wrapper around scipy.interpolate.interp1d""" + f_interp = interp1d(np.flipud(df['i']), np.flipud(df['v']), kind='linear', + fill_value='extrapolate') + return f_interp(i) + + +def combine(dfs): + """ + Combine IV curves in series by aligning currents and summing voltages. + The current range is based on the first curve's forward bias region. + """ + df1 = dfs[0] + imin = df1['i'].min() + imax = df1.loc[df1['v'] > 0, 'i'].max() + i = np.linspace(imin, imax, 1000) + v = 0 + for df2 in dfs: + v_cell = interpolate(df2, i) + v += v_cell + return pd.DataFrame({'i': i, 'v': v}) + + +def simulate_module(parameters, poa_direct, poa_diffuse, Tcell, + shaded_fraction, cells_per_string=24, strings=3): + """ + Simulate the IV curve for a partially shaded module. + The shade is assumed to be coming up from the bottom of the module when in + portrait orientation, so it affects all substrings equally. + Substrings are assumed to be "down and back", so the number of cells per + string is divided between two columns of cells. + """ + # find the number of cells per column that are in full shadow + nrow = cells_per_string//2 + nrow_full_shade = int(shaded_fraction * nrow) + # find the fraction of shade in the border row + partial_shade_fraction = 1 - (shaded_fraction * nrow - nrow_full_shade) + + df_lit = simulate_full_curve( + parameters, + poa_diffuse + poa_direct, + Tcell) + df_partial = simulate_full_curve( + parameters, + poa_diffuse + partial_shade_fraction * poa_direct, + Tcell) + df_shaded = simulate_full_curve( + parameters, + poa_diffuse, + Tcell) + # build a list of IV curves for a single column of cells (half a substring) + include_partial_cell = (shaded_fraction < 1) + half_substring_curves = ( + [df_lit] * (nrow - nrow_full_shade - 1) + + ([df_partial] if include_partial_cell else []) + + [df_shaded] * nrow_full_shade + ) + df = combine(half_substring_curves) + # all substrings perform equally, so can just scale voltage directly + df['v'] *= strings*2 + return df + + +def find_pmp(df): + """simple function to find Pmp on an IV curve""" + return df.product(axis=1).max() + + +# find Pmp under different shading conditions +data = [] +for diffuse_fraction in np.linspace(0, 1, 11): + for shaded_fraction in np.linspace(0, 1, 51): + + df = simulate_module(parameters, + poa_direct=(1-diffuse_fraction)*1000, + poa_diffuse=diffuse_fraction*1000, + Tcell=40, + shaded_fraction=shaded_fraction) + data.append({ + 'fd': diffuse_fraction, + 'fs': shaded_fraction, + 'pmp': find_pmp(df) + }) + +results = pd.DataFrame(data) +results['pmp'] /= results['pmp'].max() # normalize power to 0-1 +results_pivot = results.pivot('fd', 'fs', 'pmp') +plt.imshow(results_pivot, origin='lower', aspect='auto') +plt.xlabel('shaded fraction') +plt.ylabel('diffuse fraction') +xlabels = ["{:0.02f}".format(fs) for fs in results_pivot.columns[::5]] +ylabels = ["{:0.02f}".format(fd) for fd in results_pivot.index] +plt.xticks(range(0, 5*len(xlabels), 5), xlabels) +plt.yticks(range(0, len(ylabels)), ylabels) +plt.title('Module P_mp across shading conditions') +plt.colorbar() From dc12c48b25befea71fc643efef902a928778cd7e Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Mon, 18 May 2020 11:33:57 -0600 Subject: [PATCH 02/11] add commentary --- .../plot_partial_module_shading_simple.py | 29 ++++++++++++++++--- 1 file changed, 25 insertions(+), 4 deletions(-) diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/plot_partial_module_shading_simple.py index bc06e78dcf..ff4716e245 100644 --- a/docs/examples/plot_partial_module_shading_simple.py +++ b/docs/examples/plot_partial_module_shading_simple.py @@ -1,6 +1,6 @@ """ -Calculating the effect of partial module shading -================================================ +Calculating power loss from partial module shading +================================================== Example of modeling cell-to-cell mismatch loss from partial module shading. """ @@ -16,8 +16,9 @@ # under various shading and irradiance conditions. # # The primary functions used here are: +# # - :py:meth:`pvlib.pvsystem.calcparams_desoto` to estimate the SDE parameters -# at the operating conditions +# at the specified operating conditions. # - :py:meth:`pvlib.singlediode.bishop88` to calculate the full cell IV curve, # including the reverse bias region. # @@ -31,7 +32,9 @@ # on the module's electrical topology. This example makes some simplifying # assumptions that are not generally applicable. For instance, it assumes # that all of the module's cell strings perform identically, making it -# possible to ignore the effect of bypass diodes. +# possible to ignore the effect of bypass diodes. It also assumes that +# shading only applies to beam irradiance, *i.e.* all cells receive the +# same amount of diffuse irradiance. from pvlib import pvsystem, singlediode import pandas as pd @@ -194,3 +197,21 @@ def find_pmp(df): plt.yticks(range(0, len(ylabels)), ylabels) plt.title('Module P_mp across shading conditions') plt.colorbar() +plt.show() + +# %% +# +# This heatmap shows the module maximum power under different partial shade +# conditions, where "diffuse fraction" refers to the ratio +# :math:`poa_{diffuse} / poa_{global}` and "shaded fraction" refers to the +# fraction of the module that receives only diffuse irradiance. +# +# The heatmap makes a few things evident: +# +# - When diffuse fraction is equal to 1, there is no beam irradiance to lose, +# so shading has no effect on production. +# - When shaded fraction is equal to 0, no irradiance is blocked, so module +# output does not change with the diffuse fraction. +# - Under sunny conditions (diffuse fraction < 0.5), module output is +# significantly reduced after just the first cell is shaded +# (1/12 = ~8% shaded fraction). From 19a7eb4f9e668c93c9cde961e3f45d45c994f942 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 19 May 2020 06:59:19 -0600 Subject: [PATCH 03/11] rename parameters to cell_parameters, stickler --- .../plot_partial_module_shading_simple.py | 24 +++++++++---------- 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/plot_partial_module_shading_simple.py index ff4716e245..475974ca7b 100644 --- a/docs/examples/plot_partial_module_shading_simple.py +++ b/docs/examples/plot_partial_module_shading_simple.py @@ -46,7 +46,7 @@ qe = 1.602176634e-19 # C Vth = kb * (273.15+25) / qe -parameters = { +cell_parameters = { 'I_L_ref': 8.24, 'I_o_ref': 2.36e-9, 'a_ref': 1.3*Vth, @@ -124,7 +124,7 @@ def combine(dfs): return pd.DataFrame({'i': i, 'v': v}) -def simulate_module(parameters, poa_direct, poa_diffuse, Tcell, +def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, shaded_fraction, cells_per_string=24, strings=3): """ Simulate the IV curve for a partially shaded module. @@ -140,17 +140,17 @@ def simulate_module(parameters, poa_direct, poa_diffuse, Tcell, partial_shade_fraction = 1 - (shaded_fraction * nrow - nrow_full_shade) df_lit = simulate_full_curve( - parameters, - poa_diffuse + poa_direct, - Tcell) + cell_parameters, + poa_diffuse + poa_direct, + Tcell) df_partial = simulate_full_curve( - parameters, - poa_diffuse + partial_shade_fraction * poa_direct, - Tcell) + cell_parameters, + poa_diffuse + partial_shade_fraction * poa_direct, + Tcell) df_shaded = simulate_full_curve( - parameters, - poa_diffuse, - Tcell) + cell_parameters, + poa_diffuse, + Tcell) # build a list of IV curves for a single column of cells (half a substring) include_partial_cell = (shaded_fraction < 1) half_substring_curves = ( @@ -174,7 +174,7 @@ def find_pmp(df): for diffuse_fraction in np.linspace(0, 1, 11): for shaded_fraction in np.linspace(0, 1, 51): - df = simulate_module(parameters, + df = simulate_module(cell_parameters, poa_direct=(1-diffuse_fraction)*1000, poa_diffuse=diffuse_fraction*1000, Tcell=40, From e27038a9b45470dda7ddcde88350474cecd96cf3 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 19 May 2020 06:59:36 -0600 Subject: [PATCH 04/11] remame combine to combine_series --- docs/examples/plot_partial_module_shading_simple.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/plot_partial_module_shading_simple.py index 475974ca7b..aedf3427f2 100644 --- a/docs/examples/plot_partial_module_shading_simple.py +++ b/docs/examples/plot_partial_module_shading_simple.py @@ -108,7 +108,7 @@ def interpolate(df, i): return f_interp(i) -def combine(dfs): +def combine_series(dfs): """ Combine IV curves in series by aligning currents and summing voltages. The current range is based on the first curve's forward bias region. @@ -158,7 +158,7 @@ def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, ([df_partial] if include_partial_cell else []) + [df_shaded] * nrow_full_shade ) - df = combine(half_substring_curves) + df = combine_series(half_substring_curves) # all substrings perform equally, so can just scale voltage directly df['v'] *= strings*2 return df From 1531c1910ca61dd0fe0785ad3da4c7e5476b8686 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 19 May 2020 07:33:00 -0600 Subject: [PATCH 05/11] add example cell curves and commentary --- .../plot_partial_module_shading_simple.py | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/plot_partial_module_shading_simple.py index aedf3427f2..ad3f565807 100644 --- a/docs/examples/plot_partial_module_shading_simple.py +++ b/docs/examples/plot_partial_module_shading_simple.py @@ -58,6 +58,12 @@ 'breakdown_voltage': -15, } +# %% +# Simulating a cell's IV curve +# ---------------------------- +# +# First, calculate IV curves for individual cells: + def simulate_full_curve(parameters, Geff, Tcell, method='brentq', ivcurve_pnts=1000): @@ -101,6 +107,34 @@ def simulate_full_curve(parameters, Geff, Tcell, method='brentq', }) +def plot_curves(dfs, labels): + """plot the forward- and reverse-bias portions of an IV curve""" + fig, axes = plt.subplots(1, 2) + for df, label in zip(dfs, labels): + forward = df.loc[df['v'] > 0, :] + reverse = df.loc[df['v'] < 0, :] + reverse.plot('v', 'i', label=label, ax=axes[0]) + forward.plot('v', 'i', label=label, ax=axes[1]) + return axes + + +cell_curve_full_sun = simulate_full_curve(cell_parameters, Geff=1000, Tcell=40) +cell_curve_shaded = simulate_full_curve(cell_parameters, Geff=200, Tcell=40) +plot_curves([cell_curve_full_sun, cell_curve_shaded], ['Full Sun', 'Shaded']) + + +# %% +# Combining cell IV curves to create a module IV curve +# ---------------------------------------------------- +# +# To combine the individual cell IV curves and form a module's IV curve, +# the cells in each substring must be added in series and the substrings +# added in parallel. To add in series, the voltages for a given current are +# added. However, because each cell's curve is discretized and the currents +# might not line up, we align each curve to a common set of current values +# with interpolation. + + def interpolate(df, i): """convenience wrapper around scipy.interpolate.interp1d""" f_interp = interp1d(np.flipud(df['i']), np.flipud(df['v']), kind='linear', @@ -188,6 +222,7 @@ def find_pmp(df): results = pd.DataFrame(data) results['pmp'] /= results['pmp'].max() # normalize power to 0-1 results_pivot = results.pivot('fd', 'fs', 'pmp') +plt.figure() plt.imshow(results_pivot, origin='lower', aspect='auto') plt.xlabel('shaded fraction') plt.ylabel('diffuse fraction') From 411405440a01b64aeaceaa4148287fb55b803126 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 19 May 2020 07:59:26 -0600 Subject: [PATCH 06/11] add module curve plot --- .../plot_partial_module_shading_simple.py | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/plot_partial_module_shading_simple.py index ad3f565807..9183baf3fd 100644 --- a/docs/examples/plot_partial_module_shading_simple.py +++ b/docs/examples/plot_partial_module_shading_simple.py @@ -111,16 +111,18 @@ def plot_curves(dfs, labels): """plot the forward- and reverse-bias portions of an IV curve""" fig, axes = plt.subplots(1, 2) for df, label in zip(dfs, labels): - forward = df.loc[df['v'] > 0, :] - reverse = df.loc[df['v'] < 0, :] - reverse.plot('v', 'i', label=label, ax=axes[0]) - forward.plot('v', 'i', label=label, ax=axes[1]) + df.plot('v', 'i', label=label, ax=axes[0]) + df.plot('v', 'i', label=label, ax=axes[1]) + axes[0].set_xlim(right=0) + axes[1].set_xlim(left=0) + return axes cell_curve_full_sun = simulate_full_curve(cell_parameters, Geff=1000, Tcell=40) cell_curve_shaded = simulate_full_curve(cell_parameters, Geff=200, Tcell=40) plot_curves([cell_curve_full_sun, cell_curve_shaded], ['Full Sun', 'Shaded']) +plt.gcf().suptitle('Cell-level reverse- and forward-biased IV curves') # %% @@ -145,11 +147,11 @@ def interpolate(df, i): def combine_series(dfs): """ Combine IV curves in series by aligning currents and summing voltages. - The current range is based on the first curve's forward bias region. + The current range is based on the first curve's current range. """ df1 = dfs[0] imin = df1['i'].min() - imax = df1.loc[df1['v'] > 0, 'i'].max() + imax = df1['i'].max() i = np.linspace(imin, imax, 1000) v = 0 for df2 in dfs: @@ -198,6 +200,29 @@ def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, return df +kwargs = { + 'cell_parameters': cell_parameters, + 'poa_direct': 800, + 'poa_diffuse': 200, + 'Tcell': 40 +} +module_curve_full_sun = simulate_module(shaded_fraction=0, **kwargs) +module_curve_shaded = simulate_module(shaded_fraction=0.1, **kwargs) +plot_curves([module_curve_full_sun, module_curve_shaded], + ['Full Sun', 'Shaded']) +plt.gcf().suptitle('Module-level reverse- and forward-biased IV curves') + +# %% +# Calculating shading loss across shading scenarios +# ------------------------------------------------- +# +# Clearly the module-level IV-curve is strongly affected by partial shading. +# This heatmap shows the module maximum power under a range of partial shade +# conditions, where "diffuse fraction" refers to the ratio +# :math:`poa_{diffuse} / poa_{global}` and "shaded fraction" refers to the +# fraction of the module that receives only diffuse irradiance. + + def find_pmp(df): """simple function to find Pmp on an IV curve""" return df.product(axis=1).max() @@ -233,14 +258,10 @@ def find_pmp(df): plt.title('Module P_mp across shading conditions') plt.colorbar() plt.show() +# use this figure as the thumbnail: +# sphinx_gallery_thumbnail_number = 3 # %% -# -# This heatmap shows the module maximum power under different partial shade -# conditions, where "diffuse fraction" refers to the ratio -# :math:`poa_{diffuse} / poa_{global}` and "shaded fraction" refers to the -# fraction of the module that receives only diffuse irradiance. -# # The heatmap makes a few things evident: # # - When diffuse fraction is equal to 1, there is no beam irradiance to lose, From 3c7bb0118804136a15a9e19e6e1bd27a38b89211 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 19 May 2020 08:00:57 -0600 Subject: [PATCH 07/11] lint --- docs/examples/plot_partial_module_shading_simple.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/plot_partial_module_shading_simple.py index 9183baf3fd..2fc1170066 100644 --- a/docs/examples/plot_partial_module_shading_simple.py +++ b/docs/examples/plot_partial_module_shading_simple.py @@ -190,9 +190,9 @@ def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, # build a list of IV curves for a single column of cells (half a substring) include_partial_cell = (shaded_fraction < 1) half_substring_curves = ( - [df_lit] * (nrow - nrow_full_shade - 1) + - ([df_partial] if include_partial_cell else []) + - [df_shaded] * nrow_full_shade + [df_lit] * (nrow - nrow_full_shade - 1) + + ([df_partial] if include_partial_cell else []) + + [df_shaded] * nrow_full_shade ) df = combine_series(half_substring_curves) # all substrings perform equally, so can just scale voltage directly From 7c16fd022ff7fdbf7f39da05c9ab33df8a5ef7c8 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 19 May 2020 08:16:35 -0600 Subject: [PATCH 08/11] lint again, don't know how to fix this --- docs/examples/plot_partial_module_shading_simple.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/plot_partial_module_shading_simple.py index 2fc1170066..eb1348915d 100644 --- a/docs/examples/plot_partial_module_shading_simple.py +++ b/docs/examples/plot_partial_module_shading_simple.py @@ -191,8 +191,8 @@ def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, include_partial_cell = (shaded_fraction < 1) half_substring_curves = ( [df_lit] * (nrow - nrow_full_shade - 1) - + ([df_partial] if include_partial_cell else []) - + [df_shaded] * nrow_full_shade + + ([df_partial] if include_partial_cell else []) # noqa: W503 + + [df_shaded] * nrow_full_shade # noqa: W503 ) df = combine_series(half_substring_curves) # all substrings perform equally, so can just scale voltage directly From 086dd8418d03bcb9332e1f197acc71681b6149fa Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Tue, 19 May 2020 22:01:06 -0600 Subject: [PATCH 09/11] updates from review --- .../plot_partial_module_shading_simple.py | 126 ++++++++++++++---- 1 file changed, 99 insertions(+), 27 deletions(-) diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/plot_partial_module_shading_simple.py index eb1348915d..d69f398e1f 100644 --- a/docs/examples/plot_partial_module_shading_simple.py +++ b/docs/examples/plot_partial_module_shading_simple.py @@ -17,8 +17,8 @@ # # The primary functions used here are: # -# - :py:meth:`pvlib.pvsystem.calcparams_desoto` to estimate the SDE parameters -# at the specified operating conditions. +# - :py:meth:`pvlib.pvsystem.calcparams_desoto` to estimate the single +# diode equation parameters at some specified operating conditions. # - :py:meth:`pvlib.singlediode.bishop88` to calculate the full cell IV curve, # including the reverse bias region. # @@ -31,10 +31,9 @@ # Modeling partial module shading is complicated and depends significantly # on the module's electrical topology. This example makes some simplifying # assumptions that are not generally applicable. For instance, it assumes -# that all of the module's cell strings perform identically, making it -# possible to ignore the effect of bypass diodes. It also assumes that -# shading only applies to beam irradiance, *i.e.* all cells receive the -# same amount of diffuse irradiance. +# that shading only applies to beam irradiance (*i.e.* all cells receive +# the same amount of diffuse irradiance) and cell temperature is uniform +# and not affected by cell-level irradiance variation. from pvlib import pvsystem, singlediode import pandas as pd @@ -42,8 +41,12 @@ from scipy.interpolate import interp1d import matplotlib.pyplot as plt +from scipy.constants import e as qe, k as kB + kb = 1.380649e-23 # J/K qe = 1.602176634e-19 # C +# kb is J/K, qe is C=J/V +# kb * T / qe -> V Vth = kb * (273.15+25) / qe cell_parameters = { @@ -59,11 +62,24 @@ } # %% -# Simulating a cell's IV curve -# ---------------------------- +# Simulating a cell IV curve +# -------------------------- # -# First, calculate IV curves for individual cells: - +# First, calculate IV curves for individual cells. The process is as follows: +# +# 1) Given a set of cell parameters at reference conditions and the operating +# conditions of interest (irradiance and temperature), use a single-diode +# model to calculate the single diode equation parameters for the cell at +# the operating conditions. Here we use the De Soto model via +# :py:func:`pvlib.pvsystem.calcparams_desoto`. +# 2) The single diode equation cannot be solved analytically, so pvlib has +# implemented a couple methods of solving it for us. However, currently +# only the Bishop '88 method (:py:func:`pvlib.singlediode.bishop88`) has +# the ability to model the reverse bias characteristic in addition to the +# forward characteristic. Depending on the nature of the shadow, it is +# sometimes necessary to model the reverse bias portion of the IV curve, +# so we use the Bishop '88 method here. This gives us a set of (V, I) +# points on the cell's IV curve. def simulate_full_curve(parameters, Geff, Tcell, method='brentq', ivcurve_pnts=1000): @@ -98,6 +114,8 @@ def simulate_full_curve(parameters, Geff, Tcell, method='brentq', v_oc = singlediode.bishop88_v_from_i( 0.0, *sde_args, method=method, **kwargs ) + # ideally would use some intelligent log-spacing to concentrate points + # around the forward- and reverse-bias knees, but this is good enough: vd = np.linspace(0.99*kwargs['breakdown_voltage'], v_oc, ivcurve_pnts) ivcurve_i, ivcurve_v, _ = singlediode.bishop88(vd, *sde_args, **kwargs) @@ -107,31 +125,55 @@ def simulate_full_curve(parameters, Geff, Tcell, method='brentq', }) -def plot_curves(dfs, labels): +# %% +# Now that we can calculate cell-level IV curves, let's compare a +# fully-illuminated cell's curve to a shaded cell's curve. Note that shading +# typically does not reduce a cell's illumination to zero -- tree shading and +# row-to-row shading block the beam portion of irradiance but leave the diffuse +# portion largely intact. In this example plot, we choose :math:`200 W/m^2` +# as the amount of irradiance received by a shaded cell. + +def plot_curves(dfs, labels, title): """plot the forward- and reverse-bias portions of an IV curve""" - fig, axes = plt.subplots(1, 2) + fig, axes = plt.subplots(1, 2, sharey=True, figsize=(5, 3)) for df, label in zip(dfs, labels): df.plot('v', 'i', label=label, ax=axes[0]) df.plot('v', 'i', label=label, ax=axes[1]) axes[0].set_xlim(right=0) - axes[1].set_xlim(left=0) - + axes[0].set_ylim([0, 25]) + axes[1].set_xlim([0, df['v'].max()*1.5]) + axes[0].set_ylabel('current [A]') + axes[0].set_xlabel('voltage [V]') + axes[1].set_xlabel('voltage [V]') + fig.suptitle(title) + fig.tight_layout() return axes cell_curve_full_sun = simulate_full_curve(cell_parameters, Geff=1000, Tcell=40) cell_curve_shaded = simulate_full_curve(cell_parameters, Geff=200, Tcell=40) -plot_curves([cell_curve_full_sun, cell_curve_shaded], ['Full Sun', 'Shaded']) -plt.gcf().suptitle('Cell-level reverse- and forward-biased IV curves') - +ax = plot_curves([cell_curve_full_sun, cell_curve_shaded], + labels=['Full Sun', 'Shaded'], + title='Cell-level reverse- and forward-biased IV curves') # %% +# This figure shows how a cell's current decreases roughly in proportion to +# the irradiance reduction from shading, but voltage changes much less. +# At the cell level, the effect of shading is essentially to shift the I-V +# curve down to lower currents rather than change the curve's shape. +# +# Note that the forward and reverse curves are plotted separately to +# accommodate the different voltage scales involved -- a normal crystalline +# silicon cell reaches only ~0.6V in forward bias, but can get to -10 to -20V +# in reverse bias. +# # Combining cell IV curves to create a module IV curve # ---------------------------------------------------- # # To combine the individual cell IV curves and form a module's IV curve, -# the cells in each substring must be added in series and the substrings -# added in parallel. To add in series, the voltages for a given current are +# the cells in each substring must be added in series. The substrings are +# in series as well, but with parallel bypass diodes to protect from reverse +# bias voltages. To add in series, the voltages for a given current are # added. However, because each cell's curve is discretized and the currents # might not line up, we align each curve to a common set of current values # with interpolation. @@ -160,17 +202,33 @@ def combine_series(dfs): return pd.DataFrame({'i': i, 'v': v}) +# %% +# Rather than simulate all 72 cells in the module, we'll assume that there +# are only three types of cells (fully illuminated, fully shaded, and +# partially shaded), and within each type all cells behave identically. This +# means that simulating one cell from each type (for three cell simulations +# total) is sufficient to model the module as a whole. +# +# This function also models the effect of bypass diodes in parallel with each +# substring. Bypass diodes are normally inactive but conduct when substring +# voltage becomes sufficiently negative, presumably due to the substring +# entering reverse bias from mismatch between substrings. In that case the +# substring's voltage is clamped to the diode's trigger voltage (assumed to +# be 0.5V here). + def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, shaded_fraction, cells_per_string=24, strings=3): """ Simulate the IV curve for a partially shaded module. The shade is assumed to be coming up from the bottom of the module when in portrait orientation, so it affects all substrings equally. + For simplicity, cell temperature is assumed to be uniform across the + module, regardless of variation in cell-level illumination. Substrings are assumed to be "down and back", so the number of cells per string is divided between two columns of cells. """ # find the number of cells per column that are in full shadow - nrow = cells_per_string//2 + nrow = cells_per_string // 2 nrow_full_shade = int(shaded_fraction * nrow) # find the fraction of shade in the border row partial_shade_fraction = 1 - (shaded_fraction * nrow - nrow_full_shade) @@ -194,11 +252,25 @@ def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, + ([df_partial] if include_partial_cell else []) # noqa: W503 + [df_shaded] * nrow_full_shade # noqa: W503 ) - df = combine_series(half_substring_curves) - # all substrings perform equally, so can just scale voltage directly - df['v'] *= strings*2 - return df + substring_curve = combine_series(half_substring_curves) + substring_curve['v'] *= 2 # turn half strings into whole strings + # bypass diode: + substring_curve['v'] = substring_curve['v'].clip(lower=-0.5) + # no need to interpolate since we're just scaling voltage directly: + substring_curve['v'] *= strings + return substring_curve +# %% +# Now let's see how shade affects the IV curves at the module level. For this +# example, the bottom 10% of the module is shaded. Assuming 12 cells per +# column, that means one row of cells is fully shaded and another row is +# partially shaded. Even though only 10% of the module is shaded, the +# maximum power is decreased by roughly 80%! +# +# Note the effect of the bypass diodes. Without bypass diodes, operating the +# shaded module at the same current as the fully illuminated module would +# create a reverse-bias voltage of several hundred volts! However, the diodes +# prevent the reverse voltage from exceeding 1.5V (three diodes at 0.5V each). kwargs = { 'cell_parameters': cell_parameters, @@ -208,9 +280,9 @@ def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, } module_curve_full_sun = simulate_module(shaded_fraction=0, **kwargs) module_curve_shaded = simulate_module(shaded_fraction=0.1, **kwargs) -plot_curves([module_curve_full_sun, module_curve_shaded], - ['Full Sun', 'Shaded']) -plt.gcf().suptitle('Module-level reverse- and forward-biased IV curves') +ax = plot_curves([module_curve_full_sun, module_curve_shaded], + labels=['Full Sun', 'Shaded'], + title='Module-level reverse- and forward-biased IV curves') # %% # Calculating shading loss across shading scenarios From 1f71f5a398c91cf24e1bbe13ca4eec29346598ea Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 21 May 2020 18:46:13 -0600 Subject: [PATCH 10/11] all tcell=25, remove method parameter --- .../plot_partial_module_shading_simple.py | 24 ++++++++----------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/plot_partial_module_shading_simple.py index d69f398e1f..911b389cb5 100644 --- a/docs/examples/plot_partial_module_shading_simple.py +++ b/docs/examples/plot_partial_module_shading_simple.py @@ -43,11 +43,10 @@ from scipy.constants import e as qe, k as kB -kb = 1.380649e-23 # J/K -qe = 1.602176634e-19 # C -# kb is J/K, qe is C=J/V -# kb * T / qe -> V -Vth = kb * (273.15+25) / qe +# For simplicity, use cell temperature of 25C for all calculations. +# kB is J/K, qe is C=J/V +# kB * T / qe -> V +Vth = kB * (273.15+25) / qe cell_parameters = { 'I_L_ref': 8.24, @@ -81,13 +80,11 @@ # so we use the Bishop '88 method here. This gives us a set of (V, I) # points on the cell's IV curve. -def simulate_full_curve(parameters, Geff, Tcell, method='brentq', - ivcurve_pnts=1000): +def simulate_full_curve(parameters, Geff, Tcell, ivcurve_pnts=1000): """ Use De Soto and Bishop to simulate a full IV curve with both forward and reverse bias regions. """ - # adjust the reference parameters according to the operating # conditions using the De Soto model: sde_args = pvsystem.calcparams_desoto( @@ -112,7 +109,7 @@ def simulate_full_curve(parameters, Geff, Tcell, method='brentq', 'breakdown_voltage': parameters['breakdown_voltage'], } v_oc = singlediode.bishop88_v_from_i( - 0.0, *sde_args, method=method, **kwargs + 0.0, *sde_args, **kwargs ) # ideally would use some intelligent log-spacing to concentrate points # around the forward- and reverse-bias knees, but this is good enough: @@ -150,8 +147,8 @@ def plot_curves(dfs, labels, title): return axes -cell_curve_full_sun = simulate_full_curve(cell_parameters, Geff=1000, Tcell=40) -cell_curve_shaded = simulate_full_curve(cell_parameters, Geff=200, Tcell=40) +cell_curve_full_sun = simulate_full_curve(cell_parameters, Geff=1000, Tcell=25) +cell_curve_shaded = simulate_full_curve(cell_parameters, Geff=200, Tcell=25) ax = plot_curves([cell_curve_full_sun, cell_curve_shaded], labels=['Full Sun', 'Shaded'], title='Cell-level reverse- and forward-biased IV curves') @@ -178,7 +175,6 @@ def plot_curves(dfs, labels, title): # might not line up, we align each curve to a common set of current values # with interpolation. - def interpolate(df, i): """convenience wrapper around scipy.interpolate.interp1d""" f_interp = interp1d(np.flipud(df['i']), np.flipud(df['v']), kind='linear', @@ -276,7 +272,7 @@ def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, 'cell_parameters': cell_parameters, 'poa_direct': 800, 'poa_diffuse': 200, - 'Tcell': 40 + 'Tcell': 25 } module_curve_full_sun = simulate_module(shaded_fraction=0, **kwargs) module_curve_shaded = simulate_module(shaded_fraction=0.1, **kwargs) @@ -308,7 +304,7 @@ def find_pmp(df): df = simulate_module(cell_parameters, poa_direct=(1-diffuse_fraction)*1000, poa_diffuse=diffuse_fraction*1000, - Tcell=40, + Tcell=25, shaded_fraction=shaded_fraction) data.append({ 'fd': diffuse_fraction, From d83e8c21f7ac3021275baee7a195077107b7ccd2 Mon Sep 17 00:00:00 2001 From: Kevin Anderson Date: Thu, 21 May 2020 18:49:32 -0600 Subject: [PATCH 11/11] stickler --- docs/examples/plot_partial_module_shading_simple.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/examples/plot_partial_module_shading_simple.py b/docs/examples/plot_partial_module_shading_simple.py index 911b389cb5..ec96d9ca45 100644 --- a/docs/examples/plot_partial_module_shading_simple.py +++ b/docs/examples/plot_partial_module_shading_simple.py @@ -80,6 +80,7 @@ # so we use the Bishop '88 method here. This gives us a set of (V, I) # points on the cell's IV curve. + def simulate_full_curve(parameters, Geff, Tcell, ivcurve_pnts=1000): """ Use De Soto and Bishop to simulate a full IV curve with both @@ -175,6 +176,7 @@ def plot_curves(dfs, labels, title): # might not line up, we align each curve to a common set of current values # with interpolation. + def interpolate(df, i): """convenience wrapper around scipy.interpolate.interp1d""" f_interp = interp1d(np.flipud(df['i']), np.flipud(df['v']), kind='linear', @@ -268,6 +270,7 @@ def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell, # create a reverse-bias voltage of several hundred volts! However, the diodes # prevent the reverse voltage from exceeding 1.5V (three diodes at 0.5V each). + kwargs = { 'cell_parameters': cell_parameters, 'poa_direct': 800,