-
Notifications
You must be signed in to change notification settings - Fork 1.1k
backtracking for rare case when sun below tracker improvement #824
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
Comments
Related to #65 is a typo? I'm not following step 1, what is |
Thanks, I updated the description to reference #656, sorry. oops re: system plane, that's related to the new PR, #823, it's the plane that contains all the trackers, but it isn't necessary here, because we can have this same issue on a tilted tracker that is parallel to the slope (system plane) it's on. |
PTAL @kevinsa5 thx! |
To make sure I understand the geometry -- here's a sideview truetracking vs backtracking diagram I have laying around, so apologies for the extraneous labeling. The math behind "normal" backtracking when the sun is above the array is to align points C and H (or D and F, with backtracking enabled) such that their tangent line is parallel to the sun's rays. This tangent line coincides with the edge of the shadow cast by the row in front. As the sun crosses the system plane (dashed line) and passes "underneath" the array, each row's shadow crosses over the row behind it, which means that the relevant shadow edge switches -- instead of aligning points D and F, the goal is now to align points I and J. As such, the correct backtracking behavior is for tracker angle to momentarily touch zero right when the sun crosses the system plane, then to increase again until sun is below horizon (or max angle is reached). For instance, if position I-D in the figure represents the correct backtracking angle for some solar position where the sun is above the array as it sets, the row should proceed to track flatter until the sun crosses the dashed line, and then reverse course back to I-D and beyond as the sun continues to drop below the plane. @mikofski does that sound right? |
I'm not sure that this point is necessarily true:
From my understanding, there are two backtracking positions that eliminate row-to-row shading and present the same cross section to incoming beam irradiance. In the below diagram (another that I already had laying around), the sun has crossed below the system plane and it shows the two possible backtracking positions. P1 is what you're describing where the front face of the module has rotated around and is now facing backwards/down and P2 is what I tried to describe above where the modules are still facing forwards/up. I believe the two are equivalent in terms of DNI collection and shading, but the forwards/up variant would presumably have better diffuse collection and is more accommodating of tracker rotation limits. I don't have the math already coded up for this so I can't generate a comparison plot of tracker angle, but I think it would look something like the absolute value of your blue line -- a moment where tracker_angle=0 when the sun crosses the system plane, with positive tracker angle on both sides of the zero. |
Excellent insight! I agree, and I think your proposal is much better!! I just made the one change you suggested to take absolute value, actually because import pvlib
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# in Brazil so facing north
axis_azimuth = 0.0
axis_tilt = 20
max_angle = 75.0
gcr = 0.35
# Brazil, timezone is UTC-3[hrs]
starttime = '2017-01-01T00:30:00-0300'
stoptime = '2017-12-31T23:59:59-0300'
lat, lon = -27.597300, -48.549610
times = pd.DatetimeIndex(pd.date_range(
starttime, stoptime, freq='5T'))
solpos = pvlib.solarposition.get_solarposition(
times, lat, lon)
# get the early times
ts0 = '2017-01-01 05:30:00-03:00'
ts1 = '2017-01-01 12:30:00-03:00'
apparent_zenith = solpos['apparent_zenith'][ts0:ts1]
azimuth = solpos['azimuth'][ts0:ts1]
# current implementation
sat = pvlib.tracking.singleaxis(
apparent_zenith, azimuth, axis_tilt, axis_azimuth, max_angle, True, gcr)
# turn off backtracking and set max angle to 180[deg]
sat180no = pvlib.tracking.singleaxis(
apparent_zenith, azimuth, axis_tilt, axis_azimuth, max_angle=180, gcr=gcr, backtrack=False)
# calculate cos(R)
# cos(R) = L / Lx, R is rotation, L is surface length,
# Lx is shadow on ground, tracker shades when Lx > x
# x is row spacing related to GCR, x = L/GCR
lrot = np.cos(np.radians(sat180no.tracker_theta))
# proposed backtracking algorithm for sun below trackers
# Note: if tr_rot > 90[deg] then lrot < 0
# which *can* happen at low angles if axis tilt > 0
# tracker should never backtrack more than 90[deg], when lrot = 0
# if sun below trackers then use abs() to reverse direction of trackers
cos_rot = np.minimum(np.abs(lrot) / gcr, 1.0)
backtrack_rot = np.degrees(np.arccos(cos_rot))
# combine backtracking correction with the true-tracked rotation
# Note: arccosine always positive between [-90, 90] so change
# sign of backtrack correction depending on which way tracker is rotating
tracker_wbacktrack = sat180no.tracker_theta - np.sign(sat180no.tracker_theta) * backtrack_rot
# plot figure
df = pd.DataFrame({
'sat': sat.tracker_theta,
'sat180no': sat180no.tracker_theta,
'lrot': lrot,
'cos_rot': cos_rot,
'backtrack_rot': backtrack_rot,
'tracker_wbacktrack': tracker_wbacktrack})
plt.ion()
df[['sat', 'sat180no', 'tracker_wbacktrack']].iloc[:25].plot()
plt.title('proposed backtracking for sun below tracker')
plt.ylabel('tracker rotation [degrees]')
plt.yticks(np.arange(-30,200,15))
plt.grid() |
Here is a visual double check that this does what we want using Shapely:
Just tack on this code the bottom of the snippet (click to exp and, also at this gist): from shapely.geometry.polygon import LinearRing
from shapely import affinity
from shapely.geometry import LineString
L = 1.6 # length of trackers
P = L/gcr # distance between rows
f = plt.figure('trackers') # new figure
# true track position at 5:30AM
tracker_theta = -np.radians(df.sat180no.values[0])
# tracker 1 circle
pts1 = np.radians(np.arange(360))
pts1 = np.stack((L/2*np.cos(pts1), L/2*np.sin(pts1)), axis=1)
circle1 = LinearRing(pts1)
plt.plot(*circle1.xy, ':')
# tracker 2 circle
pts2 = np.radians(np.arange(360))
pts2 = np.stack((P + L/2*np.cos(pts2), L/2*np.sin(pts2)), axis=1)
circle2 = LinearRing(pts2)
plt.plot(*circle2.xy, ':')
# tracker 1 surface
tracker1 = LineString([(-L/2, 0), (L/2, 0)])
plt.plot(*tracker1.xy, '-.')
tracker1rot = affinity.rotate(
tracker1, tracker_theta, use_radians=True)
plt.plot(*tracker1rot.xy)
# tracker 2 surface
tracker2 = LineString([(P-L/2, 0), (P+L/2, 0)])
plt.plot(*tracker2.xy, '-.')
center2 = shapely.geometry.Point((P, 0))
tracker2rot = affinity.rotate(
tracker2, angle=tracker_theta, use_radians=True, origin=center2)
plt.plot(*tracker2rot.xy)
# sunray
a, b = tracker2rot.coords
d0 = b[0] - P
d1 = b[1] - P * np.tan(tracker_theta-np.pi/2)
sunray2 = LineString([b, (d0, d1)])
plt.plot(*sunray2.xy, '--')
# backtracking
tracker_theta = -np.radians(df.tracker_wbacktrack.values[0])
# backtrack tracker 1 surface
tracker1 = LineString([(-L/2, 0), (L/2, 0)])
tracker1rot = affinity.rotate(
tracker1, tracker_theta, use_radians=True)
plt.plot(*tracker1rot.xy)
# tracker 2 surface
tracker2 = LineString([(P-L/2, 0), (P+L/2, 0)])
center2 = shapely.geometry.Point((P, 0))
tracker2rot = affinity.rotate(
tracker2, angle=tracker_theta, use_radians=True, origin=center2)
plt.plot(*tracker2rot.xy)
# parallel sunrays
sun_angle1 = np.arctan2(*reversed(np.diff(sunray1.xy)))
# sun_angle2 = np.arctan2(*reversed(np.diff(sunray2.xy)))
a, b = tracker1rot.coords
c0 = a[0] + P + L
c1 = a[1] + (P+L) * np.tan(sun_angle1)
sunray1 = LineString([a, (c0, c1)])
plt.plot(*sunray1.xy, '--')
# alternate backtracking
tracker_theta = -np.radians(df.sat.values[0])
# backtrack tracker 1 surface
tracker1 = LineString([(-L/2, 0), (L/2, 0)])
tracker1rot = affinity.rotate(
tracker1, tracker_theta, use_radians=True)
plt.plot(*tracker1rot.xy)
# tracker 2 surface
tracker2 = LineString([(P-L/2, 0), (P+L/2, 0)])
center2 = shapely.geometry.Point((P, 0))
tracker2rot = affinity.rotate(
tracker2, angle=tracker_theta, use_radians=True, origin=center2)
plt.plot(*tracker2rot.xy)
plt.gca().axis('equal')
plt.ylim([-2,6])
plt.xlim([-2,6])
plt.grid()
plt.title('Backtracking with sun below trackers')
plt.xlabel('distance between rows')
plt.ylabel('height above "system" plane')
plt.legend([
'tracker 1',
'tracker 2',
'tracker 1: system plane',
'tracker 1: true track 98.3[deg]',
'tracker 2: system plane',
'tracker 2: true track 98.3[deg]',
'sunray',
'tracker 1: backtrack 32.5[deg]',
'tracker 2: backtrack 32.5[deg]',
'parallel sunray',
'tracker 1: alt backtrack -16[deg] or 164[deg]',
'tracker 2: alt backtrack -16[deg] or 164[deg]']) |
Yep, I think we're on the same page now. I like the results your code gives in the morning, but I'm getting bad results in the evening, I believe because of a combination of two things: pvlib returns truetracking angles in [-90,+270] (see the responsible line of code) which means
Does that look right? A couple other thoughts:
|
Describe the bug
To Reproduce
Steps to reproduce the behavior:
Expected behavior
The tracker should avoid shade. It should not jump from one direction to the other. If the sun ray is below the tracker then it will need to track to it's max rotation or backtrack. If there is shading at it's max rotation then it should track backtrack to zero, or perhaps parallel to the sun rays. Perhaps if bifacial, then it could go backwards, 180 from the correct backtrack position to show it's backside to the sun.
proposed algorithm (updated after this comment):
also remove abs from aoi calculation
pvlib-python/pvlib/tracking.py
Line 461 in c699575
Screenshots
If applicable, add screenshots to help explain your problem.
Versions:
pvlib.__version__
: 0.6.3pandas.__version__
: 0.24Additional context
Add any other context about the problem here.
The text was updated successfully, but these errors were encountered: