#### Warning

Modeling partial module shading is complicated and depends significantly\n on the module's electrical topology. This example makes some simplifying\n assumptions that are not generally applicable. For instance, it assumes\n that shading only applies to beam irradiance (*i.e.* all cells receive\n the same amount of diffuse irradiance) and cell temperature is uniform\n and not affected by cell-level irradiance variation.

\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"from pvlib import pvsystem, singlediode\nimport pandas as pd\nimport numpy as np\nfrom scipy.interpolate import interp1d\nimport matplotlib.pyplot as plt\n\nfrom scipy.constants import e as qe, k as kB\n\n# For simplicity, use cell temperature of 25C for all calculations.\n# kB is J/K, qe is C=J/V\n# kB * T / qe -> V\nVth = kB * (273.15+25) / qe\n\ncell_parameters = {\n 'I_L_ref': 8.24,\n 'I_o_ref': 2.36e-9,\n 'a_ref': 1.3*Vth,\n 'R_sh_ref': 1000,\n 'R_s': 0.00181,\n 'alpha_sc': 0.0042,\n 'breakdown_factor': 2e-3,\n 'breakdown_exp': 3,\n 'breakdown_voltage': -15,\n}"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Simulating a cell IV curve\n\nFirst, calculate IV curves for individual cells. The process is as follows:\n\n1) Given a set of cell parameters at reference conditions and the operating\n conditions of interest (irradiance and temperature), use a single-diode\n model to calculate the single diode equation parameters for the cell at\n the operating conditions. Here we use the De Soto model via\n :py:func:`pvlib.pvsystem.calcparams_desoto`.\n2) The single diode equation cannot be solved analytically, so pvlib has\n implemented a couple methods of solving it for us. However, currently\n only the Bishop '88 method (:py:func:`pvlib.singlediode.bishop88`) has\n the ability to model the reverse bias characteristic in addition to the\n forward characteristic. Depending on the nature of the shadow, it is\n sometimes necessary to model the reverse bias portion of the IV curve,\n so we use the Bishop '88 method here. This gives us a set of (V, I)\n points on the cell's IV curve.\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def simulate_full_curve(parameters, Geff, Tcell, ivcurve_pnts=1000):\n \"\"\"\n Use De Soto and Bishop to simulate a full IV curve with both\n forward and reverse bias regions.\n \"\"\"\n # adjust the reference parameters according to the operating\n # conditions using the De Soto model:\n sde_args = pvsystem.calcparams_desoto(\n Geff,\n Tcell,\n alpha_sc=parameters['alpha_sc'],\n a_ref=parameters['a_ref'],\n I_L_ref=parameters['I_L_ref'],\n I_o_ref=parameters['I_o_ref'],\n R_sh_ref=parameters['R_sh_ref'],\n R_s=parameters['R_s'],\n )\n # sde_args has values:\n # (photocurrent, saturation_current, resistance_series,\n # resistance_shunt, nNsVth)\n\n # Use Bishop's method to calculate points on the IV curve with V ranging\n # from the reverse breakdown voltage to open circuit\n kwargs = {\n 'breakdown_factor': parameters['breakdown_factor'],\n 'breakdown_exp': parameters['breakdown_exp'],\n 'breakdown_voltage': parameters['breakdown_voltage'],\n }\n v_oc = singlediode.bishop88_v_from_i(\n 0.0, *sde_args, **kwargs\n )\n # ideally would use some intelligent log-spacing to concentrate points\n # around the forward- and reverse-bias knees, but this is good enough:\n vd = np.linspace(0.99*kwargs['breakdown_voltage'], v_oc, ivcurve_pnts)\n\n ivcurve_i, ivcurve_v, _ = singlediode.bishop88(vd, *sde_args, **kwargs)\n return pd.DataFrame({\n 'i': ivcurve_i,\n 'v': ivcurve_v,\n })"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now that we can calculate cell-level IV curves, let's compare a\nfully-illuminated cell's curve to a shaded cell's curve. Note that shading\ntypically does not reduce a cell's illumination to zero -- tree shading and\nrow-to-row shading block the beam portion of irradiance but leave the diffuse\nportion largely intact. In this example plot, we choose $200 W/m^2$\nas the amount of irradiance received by a shaded cell.\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def plot_curves(dfs, labels, title):\n \"\"\"plot the forward- and reverse-bias portions of an IV curve\"\"\"\n fig, axes = plt.subplots(1, 2, sharey=True, figsize=(5, 3))\n for df, label in zip(dfs, labels):\n df.plot('v', 'i', label=label, ax=axes[0])\n df.plot('v', 'i', label=label, ax=axes[1])\n axes[0].set_xlim(right=0)\n axes[0].set_ylim([0, 25])\n axes[1].set_xlim([0, df['v'].max()*1.5])\n axes[0].set_ylabel('current [A]')\n axes[0].set_xlabel('voltage [V]')\n axes[1].set_xlabel('voltage [V]')\n fig.suptitle(title)\n fig.tight_layout()\n return axes\n\n\ncell_curve_full_sun = simulate_full_curve(cell_parameters, Geff=1000, Tcell=25)\ncell_curve_shaded = simulate_full_curve(cell_parameters, Geff=200, Tcell=25)\nax = plot_curves([cell_curve_full_sun, cell_curve_shaded],\n labels=['Full Sun', 'Shaded'],\n title='Cell-level reverse- and forward-biased IV curves')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"This figure shows how a cell's current decreases roughly in proportion to\nthe irradiance reduction from shading, but voltage changes much less.\nAt the cell level, the effect of shading is essentially to shift the I-V\ncurve down to lower currents rather than change the curve's shape.\n\nNote that the forward and reverse curves are plotted separately to\naccommodate the different voltage scales involved -- a normal crystalline\nsilicon cell reaches only ~0.6V in forward bias, but can get to -10 to -20V\nin reverse bias.\n\n## Combining cell IV curves to create a module IV curve\n\nTo combine the individual cell IV curves and form a module's IV curve,\nthe cells in each substring must be added in series. The substrings are\nin series as well, but with parallel bypass diodes to protect from reverse\nbias voltages. To add in series, the voltages for a given current are\nadded. However, because each cell's curve is discretized and the currents\nmight not line up, we align each curve to a common set of current values\nwith interpolation.\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def interpolate(df, i):\n \"\"\"convenience wrapper around scipy.interpolate.interp1d\"\"\"\n f_interp = interp1d(np.flipud(df['i']), np.flipud(df['v']), kind='linear',\n fill_value='extrapolate')\n return f_interp(i)\n\n\ndef combine_series(dfs):\n \"\"\"\n Combine IV curves in series by aligning currents and summing voltages.\n The current range is based on the first curve's current range.\n \"\"\"\n df1 = dfs[0]\n imin = df1['i'].min()\n imax = df1['i'].max()\n i = np.linspace(imin, imax, 1000)\n v = 0\n for df2 in dfs:\n v_cell = interpolate(df2, i)\n v += v_cell\n return pd.DataFrame({'i': i, 'v': v})"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Rather than simulate all 72 cells in the module, we'll assume that there\nare only three types of cells (fully illuminated, fully shaded, and\npartially shaded), and within each type all cells behave identically. This\nmeans that simulating one cell from each type (for three cell simulations\ntotal) is sufficient to model the module as a whole.\n\nThis function also models the effect of bypass diodes in parallel with each\nsubstring. Bypass diodes are normally inactive but conduct when substring\nvoltage becomes sufficiently negative, presumably due to the substring\nentering reverse bias from mismatch between substrings. In that case the\nsubstring's voltage is clamped to the diode's trigger voltage (assumed to\nbe 0.5V here).\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def simulate_module(cell_parameters, poa_direct, poa_diffuse, Tcell,\n shaded_fraction, cells_per_string=24, strings=3):\n \"\"\"\n Simulate the IV curve for a partially shaded module.\n The shade is assumed to be coming up from the bottom of the module when in\n portrait orientation, so it affects all substrings equally.\n For simplicity, cell temperature is assumed to be uniform across the\n module, regardless of variation in cell-level illumination.\n Substrings are assumed to be \"down and back\", so the number of cells per\n string is divided between two columns of cells.\n \"\"\"\n # find the number of cells per column that are in full shadow\n nrow = cells_per_string // 2\n nrow_full_shade = int(shaded_fraction * nrow)\n # find the fraction of shade in the border row\n partial_shade_fraction = 1 - (shaded_fraction * nrow - nrow_full_shade)\n\n df_lit = simulate_full_curve(\n cell_parameters,\n poa_diffuse + poa_direct,\n Tcell)\n df_partial = simulate_full_curve(\n cell_parameters,\n poa_diffuse + partial_shade_fraction * poa_direct,\n Tcell)\n df_shaded = simulate_full_curve(\n cell_parameters,\n poa_diffuse,\n Tcell)\n # build a list of IV curves for a single column of cells (half a substring)\n include_partial_cell = (shaded_fraction < 1)\n half_substring_curves = (\n [df_lit] * (nrow - nrow_full_shade - 1)\n + ([df_partial] if include_partial_cell else []) # noqa: W503\n + [df_shaded] * nrow_full_shade # noqa: W503\n )\n substring_curve = combine_series(half_substring_curves)\n substring_curve['v'] *= 2 # turn half strings into whole strings\n # bypass diode:\n substring_curve['v'] = substring_curve['v'].clip(lower=-0.5)\n # no need to interpolate since we're just scaling voltage directly:\n substring_curve['v'] *= strings\n return substring_curve"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now let's see how shade affects the IV curves at the module level. For this\nexample, the bottom 10% of the module is shaded. Assuming 12 cells per\ncolumn, that means one row of cells is fully shaded and another row is\npartially shaded. Even though only 10% of the module is shaded, the\nmaximum power is decreased by roughly 80%!\n\nNote the effect of the bypass diodes. Without bypass diodes, operating the\nshaded module at the same current as the fully illuminated module would\ncreate a reverse-bias voltage of several hundred volts! However, the diodes\nprevent the reverse voltage from exceeding 1.5V (three diodes at 0.5V each).\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"kwargs = {\n 'cell_parameters': cell_parameters,\n 'poa_direct': 800,\n 'poa_diffuse': 200,\n 'Tcell': 25\n}\nmodule_curve_full_sun = simulate_module(shaded_fraction=0, **kwargs)\nmodule_curve_shaded = simulate_module(shaded_fraction=0.1, **kwargs)\nax = plot_curves([module_curve_full_sun, module_curve_shaded],\n labels=['Full Sun', 'Shaded'],\n title='Module-level reverse- and forward-biased IV curves')"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Calculating shading loss across shading scenarios\n\nClearly the module-level IV-curve is strongly affected by partial shading.\nThis heatmap shows the module maximum power under a range of partial shade\nconditions, where \"diffuse fraction\" refers to the ratio\n$poa_{diffuse} / poa_{global}$ and \"shaded fraction\" refers to the\nfraction of the module that receives only diffuse irradiance.\n\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {
"collapsed": false
},
"outputs": [],
"source": [
"def find_pmp(df):\n \"\"\"simple function to find Pmp on an IV curve\"\"\"\n return df.product(axis=1).max()\n\n\n# find Pmp under different shading conditions\ndata = []\nfor diffuse_fraction in np.linspace(0, 1, 11):\n for shaded_fraction in np.linspace(0, 1, 51):\n\n df = simulate_module(cell_parameters,\n poa_direct=(1-diffuse_fraction)*1000,\n poa_diffuse=diffuse_fraction*1000,\n Tcell=25,\n shaded_fraction=shaded_fraction)\n data.append({\n 'fd': diffuse_fraction,\n 'fs': shaded_fraction,\n 'pmp': find_pmp(df)\n })\n\nresults = pd.DataFrame(data)\nresults['pmp'] /= results['pmp'].max() # normalize power to 0-1\nresults_pivot = results.pivot('fd', 'fs', 'pmp')\nplt.figure()\nplt.imshow(results_pivot, origin='lower', aspect='auto')\nplt.xlabel('shaded fraction')\nplt.ylabel('diffuse fraction')\nxlabels = [f\"{fs:0.02f}\" for fs in results_pivot.columns[::5]]\nylabels = [f\"{fd:0.02f}\" for fd in results_pivot.index]\nplt.xticks(range(0, 5*len(xlabels), 5), xlabels)\nplt.yticks(range(0, len(ylabels)), ylabels)\nplt.title('Module P_mp across shading conditions')\nplt.colorbar()\nplt.show()\n# use this figure as the thumbnail:\n# sphinx_gallery_thumbnail_number = 3"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"The heatmap makes a few things evident:\n\n- When diffuse fraction is equal to 1, there is no beam irradiance to lose,\n so shading has no effect on production.\n- When shaded fraction is equal to 0, no irradiance is blocked, so module\n output does not change with the diffuse fraction.\n- Under sunny conditions (diffuse fraction < 0.5), module output is\n significantly reduced after just the first cell is shaded\n (1/12 = ~8% shaded fraction).\n\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.7.17"
}
},
"nbformat": 4,
"nbformat_minor": 0
}