Skip to content
Closed
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
19 changes: 9 additions & 10 deletions docs/examples/bifacial/plot_irradiance_nonuniformity_loss.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,31 +40,30 @@
#
# .. sectionauthor:: Echedey Luis <echelual (at) gmail.com>

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.cm import ScalarMappable
from matplotlib.colors import Normalize

from pvlib.bifacial import power_mismatch_deline

# %%
# Problem description
# -------------------
# Let's set a fixed irradiance to each cell row of the PV array with the values
# described in Figure 1 (A), [1]_. We will cover this case for educational
# purposes, although it can be achieved with the packages
# `solarfactors <https://github.com/pvlib/solarfactors/>`_ and
# `bifacial_radiance <https://github.com/NREL/bifacial_radiance>`_.
# :ref:`solarfactors <https://github.com/pvlib/solarfactors/>` and
# :ref:`bifacial_radiance <https://github.com/NREL/bifacial_radiance>`.
#
# Here we set and plot the global irradiance level of each cell.

import numpy as np
import matplotlib.pyplot as plt
from matplotlib.cm import ScalarMappable
from matplotlib.colors import Normalize

from pvlib.bifacial import power_mismatch_deline

x = np.arange(12, 0, -1)
y = np.arange(6, 0, -1)
cells_irrad = np.repeat([1059, 976, 967, 986, 1034, 1128], len(x)).reshape(
len(y), len(x)
)

# plot the irradiance levels of each cell
color_map = "gray"
color_norm = Normalize(930, 1150)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
The PVSyst model for cell temperature :math:`T_{C}` is given by:

.. math::
:label: pvsyst

T_{C} = T_{a} + \frac{\alpha \cdot E \cdot (1 - \eta_{m})}{U_{c} + U_{v} \cdot WS},

Expand Down
2 changes: 1 addition & 1 deletion docs/sphinx/source/_static/version-alert.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function warnOnLatestVersion() {
"<p class='last'> " +
"This document is for an <strong>unreleased development version</strong>. " +
"Documentation is available for the <a href='/en/stable/'>current stable release</a>, " +
"or for older versions through the &ldquo;v:&rdquo; menu at bottom left." +
"or for older versions through the &ldquo;v:&rdquo; menu at bottom right." +
"</p>";
warning.querySelector('a').href = window.location.pathname.replace('/latest', '/stable');

Expand Down
2 changes: 1 addition & 1 deletion docs/sphinx/source/user_guide/faq.rst
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ If you see a function in the pvlib documentation that doesn't seem to exist
in your pvlib installation, the documentation is likely for a different version
of pvlib. You can check your installed pvlib version by running
``print(pvlib.__version__)`` in python. To switch documentation versions, use
the `v:` version switcher widget in the bottom left corner of this page.
the `v:` version switcher widget in the bottom right corner of this page.

You can also upgrade your installed pvlib to the latest compatible version
with ``pip install -U pvlib``, but be sure to check the :ref:`whatsnew`
Expand Down
106 changes: 64 additions & 42 deletions pvlib/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,16 @@
from pvlib import shading


def singleaxis(apparent_zenith, apparent_azimuth,
axis_tilt=0, axis_azimuth=0, max_angle=90,
backtrack=True, gcr=2.0/7.0, cross_axis_tilt=0):
def singleaxis(
apparent_zenith,
apparent_azimuth,
axis_tilt=0,
axis_azimuth=0,
max_angle=90,
backtrack=True,
gcr=2.0 / 7.0,
cross_axis_tilt=0,
):
"""
Determine the rotation angle of a single-axis tracker when given particular
solar zenith and azimuth angles.
Expand Down Expand Up @@ -125,17 +132,20 @@ def singleaxis(apparent_zenith, apparent_azimuth,
apparent_zenith = np.atleast_1d(apparent_zenith)

if apparent_azimuth.ndim > 1 or apparent_zenith.ndim > 1:
raise ValueError('Input dimensions must not exceed 1')

# The ideal tracking angle wid is the rotation to place the sun position
# vector (xp, yp, zp) in the (x, z) plane, which is normal to the panel and
# contains the axis of rotation. wid = 0 indicates that the panel is
# horizontal. Here, our convention is that a clockwise rotation is
# positive, to view rotation angles in the same frame of reference as
# azimuth. For example, for a system with tracking axis oriented south, a
# rotation toward the east is negative, and a rotation to the west is
# positive. This is a right-handed rotation around the tracker y-axis.
wid = shading.projected_solar_zenith_angle(
raise ValueError("Input dimensions must not exceed 1")

# The ideal tracking angle omega_ideal is the rotation to place the sun
# position vector (xp, yp, zp) in the (x, z) plane, which is normal to the
# panel and contains the axis of rotation. omega_id = 0 indicates that
# the panel is horizontal. Here, our convention is that a clockwise
# rotation is positive, to view rotation angles in the same frame of
# reference as azimuth. For example, for a system with tracking axis
# oriented south, a rotation toward the east is negative, and a rotation
# to the west is positive. This is a right-handed rotation around the
# tracker y-axis. Omega_ideal is the plaintext representation of the
# ideal tracking angle from Lorenzo et al 2011.
# https://doi.org/10.1002/pip.1085
omega_ideal = shading.projected_solar_zenith_angle(
axis_tilt=axis_tilt,
axis_azimuth=axis_azimuth,
solar_zenith=apparent_zenith,
Expand All @@ -144,28 +154,28 @@ def singleaxis(apparent_zenith, apparent_azimuth,

# filter for sun above panel horizon
zen_gt_90 = apparent_zenith > 90
wid[zen_gt_90] = np.nan
omega_ideal[zen_gt_90] = np.nan

# Account for backtracking
if backtrack:
# distance between rows in terms of rack lengths relative to cross-axis
# tilt
axes_distance = 1/(gcr * cosd(cross_axis_tilt))
axes_distance = 1 / (gcr * cosd(cross_axis_tilt))

# NOTE: account for rare angles below array, see GH 824
temp = np.abs(axes_distance * cosd(wid - cross_axis_tilt))
temp = np.abs(axes_distance * cosd(omega_ideal - cross_axis_tilt))

# backtrack angle using [1], Eq. 14
with np.errstate(invalid='ignore'):
wc = np.degrees(-np.sign(wid)*np.arccos(temp))
with np.errstate(invalid="ignore"):
wc = np.degrees(-np.sign(omega_ideal) * np.arccos(temp))

# NOTE: in the middle of the day, arccos(temp) is out of range because
# there's no row-to-row shade to avoid, & backtracking is unnecessary
# [1], Eqs. 15-16
with np.errstate(invalid='ignore'):
tracker_theta = wid + np.where(temp < 1, wc, 0)
with np.errstate(invalid="ignore"):
tracker_theta = omega_ideal + np.where(temp < 1, wc, 0)
else:
tracker_theta = wid
tracker_theta = omega_ideal

# NOTE: max_angle defined relative to zero-point rotation, not the
# system-plane normal
Expand All @@ -182,14 +192,19 @@ def singleaxis(apparent_zenith, apparent_azimuth,

# Calculate auxiliary angles
surface = calc_surface_orientation(tracker_theta, axis_tilt, axis_azimuth)
surface_tilt = surface['surface_tilt']
surface_azimuth = surface['surface_azimuth']
aoi = irradiance.aoi(surface_tilt, surface_azimuth,
apparent_zenith, apparent_azimuth)
surface_tilt = surface["surface_tilt"]
surface_azimuth = surface["surface_azimuth"]
aoi = irradiance.aoi(
surface_tilt, surface_azimuth, apparent_zenith, apparent_azimuth
)

# Bundle DataFrame for return values and filter for sun below horizon.
out = {'tracker_theta': tracker_theta, 'aoi': aoi,
'surface_azimuth': surface_azimuth, 'surface_tilt': surface_tilt}
out = {
"tracker_theta": tracker_theta,
"aoi": aoi,
"surface_azimuth": surface_azimuth,
"surface_tilt": surface_tilt,
}
if index is not None:
out = pd.DataFrame(out, index=index)
out[zen_gt_90] = np.nan
Expand Down Expand Up @@ -231,25 +246,31 @@ def calc_surface_orientation(tracker_theta, axis_tilt=0, axis_azimuth=0):
Tracking of One-Axis Trackers", Technical Report NREL/TP-6A20-58891,
July 2013. :doi:`10.2172/1089596`
"""
with np.errstate(invalid='ignore', divide='ignore'):
with np.errstate(invalid="ignore", divide="ignore"):
surface_tilt = acosd(cosd(tracker_theta) * cosd(axis_tilt))

# clip(..., -1, +1) to prevent arcsin(1 + epsilon) issues:
azimuth_delta = asind(np.clip(sind(tracker_theta) / sind(surface_tilt),
a_min=-1, a_max=1))
azimuth_delta = asind(
np.clip(
sind(tracker_theta)
/ sind(surface_tilt), a_min=-1, a_max=1
)
)
# Combine Eqs 2, 3, and 4:
azimuth_delta = np.where(abs(tracker_theta) < 90,
azimuth_delta,
-azimuth_delta + np.sign(tracker_theta) * 180)
azimuth_delta = np.where(
abs(tracker_theta) < 90,
azimuth_delta,
-azimuth_delta + np.sign(tracker_theta) * 180,
)
# handle surface_tilt=0 case:
azimuth_delta = np.where(sind(surface_tilt) != 0, azimuth_delta, 90)
surface_azimuth = (axis_azimuth + azimuth_delta) % 360

out = {
'surface_tilt': surface_tilt,
'surface_azimuth': surface_azimuth,
"surface_tilt": surface_tilt,
"surface_azimuth": surface_azimuth,
}
if hasattr(tracker_theta, 'index'):
if hasattr(tracker_theta, "index"):
out = pd.DataFrame(out)
return out

Expand Down Expand Up @@ -320,8 +341,8 @@ def _calc_tracker_norm(ba, bg, dg):
sin_bg = sind(bg)
sin_dg = sind(dg)
vx = sin_dg * cos_ba * cos_bg
vy = sind(ba)*sin_bg + cosd(dg)*cos_ba*cos_bg
vz = -sin_dg*sin_bg*cos_ba
vy = sind(ba) * sin_bg + cosd(dg) * cos_ba * cos_bg
vz = -sin_dg * sin_bg * cos_ba
return vx, vy, vz


Expand All @@ -345,12 +366,13 @@ def _calc_beta_c(v, dg, ba):
"""
vnorm = np.sqrt(np.dot(v, v))
beta_c = np.arcsin(
((v[0]*cosd(dg) - v[1]*sind(dg)) * sind(ba) + v[2]*cosd(ba)) / vnorm)
((v[0] * cosd(dg) - v[1] * sind(dg)) * sind(ba) + v[2] * cosd(ba))
/ vnorm
)
return beta_c


def calc_cross_axis_tilt(
slope_azimuth, slope_tilt, axis_azimuth, axis_tilt):
def calc_cross_axis_tilt(slope_azimuth, slope_tilt, axis_azimuth, axis_tilt):
"""
Calculate the angle, relative to horizontal, of the line formed by the
intersection between the slope containing the tracker axes and a plane
Expand Down