@@ -21,7 +21,7 @@ class SingleAxisTracker(PVSystem):
21
21
22
22
axis_azimuth : float, default 0
23
23
A value denoting the compass direction along which the axis of
24
- rotation lies. Measured in decimal degrees East of North .
24
+ rotation lies. Measured in decimal degrees east of north .
25
25
26
26
max_angle : float, default 90
27
27
A value denoting the maximum rotation angle, in decimal degrees,
@@ -291,7 +291,7 @@ def singleaxis(apparent_zenith, apparent_azimuth,
291
291
292
292
axis_azimuth : float, default 0
293
293
A value denoting the compass direction along which the axis of
294
- rotation lies. Measured in decimal degrees East of North .
294
+ rotation lies. Measured in decimal degrees east of north .
295
295
296
296
max_angle : float, default 90
297
297
A value denoting the maximum rotation angle, in decimal degrees,
@@ -364,58 +364,24 @@ def singleaxis(apparent_zenith, apparent_azimuth,
364
364
if apparent_azimuth .ndim > 1 or apparent_zenith .ndim > 1 :
365
365
raise ValueError ('Input dimensions must not exceed 1' )
366
366
367
- # Calculate sun position x, y, z using coordinate system as in [1 ], Eq 2 .
367
+ # Calculate sun position x, y, z using coordinate system as in [2 ], Eq 1 .
368
368
369
- # Positive y axis is oriented parallel to earth surface along tracking axis
370
- # (for the purpose of illustration, assume y is oriented to the south);
371
- # positive x axis is orthogonal, 90 deg clockwise from y-axis, and parallel
372
- # to the earth's surface (if y axis is south, x axis is west);
373
- # positive z axis is normal to x, y axes, pointed upward.
374
-
375
- # Equations in [1] assume solar azimuth is relative to reference vector
376
- # pointed south, with clockwise positive.
377
- # Here, the input solar azimuth is degrees East of North,
378
- # i.e., relative to a reference vector pointed
379
- # north with clockwise positive.
380
-
381
- # NOTE: Equations in [2] agree with the reference frame used here, so
382
- # adjustments are not required
383
-
384
- # Rotate sun azimuth to coordinate system as in [1, 2]
385
- # to calculate sun position.
386
-
387
- # NOTE: sin(90-x) = cos(x) & cos(90-x) = sin(x)
369
+ # NOTE: solar elevation = 90 - solar zenith, then use trig identities:
370
+ # sin(90-x) = cos(x) & cos(90-x) = sin(x)
388
371
sin_zenith = sind (apparent_zenith )
389
372
x = sin_zenith * sind (apparent_azimuth )
390
373
y = sin_zenith * cosd (apparent_azimuth )
391
374
z = cosd (apparent_zenith )
392
375
393
- # translate array azimuth from compass bearing to [1] coord system
394
- # wholmgren: strange to see axis_azimuth calculated differently from az,
395
- # (not that it matters, or at least it shouldn't...).
396
-
397
- # NOTE: Coordinate system in [2] agrees with refernece frame used here, so
398
- # adjustments are not required
399
-
400
- # translate input array tilt angle axis_tilt to [1] coordinate system.
401
-
402
- # In [1] coordinates, axis_tilt is a rotation about the x-axis.
403
- # For a system with array azimuth (y-axis) oriented south,
404
- # the x-axis is oriented west, and a positive axis_tilt is a
405
- # counterclockwise rotation, i.e, lifting the north edge of the panel.
406
- # Thus, in [1] coordinate system, in the northern hemisphere a positive
407
- # axis_tilt indicates a rotation toward the equator,
408
- # whereas in the southern hemisphere rotation toward the equator is
409
- # indicated by axis_tilt<0. Here, the input axis_tilt is
410
- # always positive and is a rotation toward the equator.
411
-
412
- # Calculate sun position (xp, yp, zp) in panel-oriented coordinate system:
413
- # positive y-axis is oriented along tracking axis at panel tilt;
414
- # positive x-axis is orthogonal, clockwise, parallel to earth surface;
415
- # positive z-axis is normal to x-y axes, pointed upward.
416
- # Calculate sun position (xp,yp,zp) in panel coordinates using [1] Eq 11
417
- # note that equation for yp (y' in Eq. 11 of Lorenzo et al 2011) is
418
- # corrected, after conversation with paper's authors.
376
+ # Assume the tracker reference frame is right-handed. Positive y-axis is
377
+ # oriented along tracking axis tilted by the axis tilt and rotated
378
+ # clockwise from north by the axis azimuth; positive x-axis is orthogonal,
379
+ # 90 deg clockwise from y-axis, and parallel to the earth's surface (if y-
380
+ # axis is south, x-axis is west); positive z-axis is normal to x, y axes,
381
+ # pointed upward.
382
+
383
+ # Calculate sun position (xp, yp, zp) in tracker coordinate system using
384
+ # [2] Eq 4.
419
385
420
386
cos_axis_azimuth = cosd (axis_azimuth )
421
387
sin_axis_azimuth = sind (axis_azimuth )
@@ -432,63 +398,36 @@ def singleaxis(apparent_zenith, apparent_azimuth,
432
398
# The ideal tracking angle wid is the rotation to place the sun position
433
399
# vector (xp, yp, zp) in the (y, z) plane; i.e., normal to the panel and
434
400
# containing the axis of rotation. wid = 0 indicates that the panel is
435
- # horizontal. Here, our convention is that a clockwise rotation is
401
+ # horizontal. Here, our convention is that a clockwise rotation is
436
402
# positive, to view rotation angles in the same frame of reference as
437
- # azimuth. For example, for a system with tracking axis oriented south,
438
- # a rotation toward the east is negative, and a rotation to the west is
439
- # positive.
440
-
441
- # angle from x-y plane to projection of sun vector onto x-z plane
442
- # tmp = np.degrees(np.arctan(zp/xp))
443
-
444
- # Obtain wid by translating tmp to convention for rotation angles.
445
- # Have to account for which quadrant of the x-z plane in which the sun
446
- # vector lies. Complete solution here but probably not necessary to
447
- # consider QIII and QIV.
448
- # wid = pd.Series(index=times)
449
- # wid[(xp>=0) & (zp>=0)] = 90 - tmp[(xp>=0) & (zp>=0)] # QI
450
- # wid[(xp<0) & (zp>=0)] = -90 - tmp[(xp<0) & (zp>=0)] # QII
451
- # wid[(xp<0) & (zp<0)] = -90 - tmp[(xp<0) & (zp<0)] # QIII
452
- # wid[(xp>=0) & (zp<0)] = 90 - tmp[(xp>=0) & (zp<0)] # QIV
453
-
454
- # NOTE: Use arctan2 and avoid the tmp corrections.
403
+ # azimuth. For example, for a system with tracking axis oriented south, a
404
+ # rotation toward the east is negative, and a rotation to the west is
405
+ # positive. This is a right-handed rotation around the tracker y-axis.
455
406
456
407
# Calculate angle from x-y plane to projection of sun vector onto x-z plane
457
- # and then obtain wid by translating tmp to convention for rotation angles .
408
+ # using [2] Eq. 5 .
458
409
459
- # NOTE: if zp/xp = tan(90 - wid) = cot(wid) then tan(wid) = xp/zp
460
410
wid = np .degrees (np .arctan2 (xp , zp ))
461
411
462
412
# filter for sun above panel horizon
463
413
zen_gt_90 = apparent_zenith > 90
464
414
wid [zen_gt_90 ] = np .nan
465
415
466
- # Account for backtracking; modified from [1] to account for rotation
467
- # angle convention being used here.
416
+ # Account for backtracking
468
417
if backtrack :
469
418
# distance between rows in terms of rack lengths relative to side slope
470
419
axes_distance = 1 / gcr / cosd (side_slope )
471
- # clip needed for low angles. GH 656
472
- # temp = np.clip(axes_distance*cosd(wid - side_slope), -1, 1)
473
420
474
421
# NOTE: account for rare angles below array, see GH 824
475
422
temp = np .abs (axes_distance * cosd (wid - side_slope ))
476
423
477
- # backtrack angle
478
- # (always positive b/c acosd returns values between 0 and 180)
479
- # wc = np.degrees(np.arccos(temp))
480
- # equation 14, ref [2]
424
+ # backtrack angle using [2], Eq. 14
481
425
with np .errstate (invalid = 'ignore' ):
482
426
wc = np .degrees (- np .sign (wid )* np .arccos (temp ))
483
427
484
- # Eq 4 applied when wid in QIV (wid < 0 evalulates True), QI
485
- # with np.errstate(invalid='ignore'):
486
- # errstate for GH 622
487
- # tracker_theta = np.where(wid < 0, wid + wc, wid - wc)
488
-
489
428
# NOTE: in the middle of the day, arccos(temp) is out of range because
490
429
# there's no row-to-row shade to avoid, & backtracking is unnecessary
491
- # Equations 15-16, ref [2]
430
+ # [2], Eqs. 15-16
492
431
with np .errstate (invalid = 'ignore' ):
493
432
tracker_theta = wid + np .where (temp < 1 , wc , 0 )
494
433
else :
@@ -498,10 +437,10 @@ def singleaxis(apparent_zenith, apparent_azimuth,
498
437
# system-plane normal
499
438
tracker_theta = np .clip (tracker_theta , - max_angle , max_angle )
500
439
501
- # calculate panel normal vector in panel-oriented x, y, z coordinates.
502
- # y-axis is axis of tracker rotation. tracker_theta is a compass angle
440
+ # Calculate panel normal vector in panel-oriented x, y, z coordinates.
441
+ # y-axis is axis of tracker rotation. tracker_theta is a compass angle
503
442
# (clockwise is positive) rather than a trigonometric angle.
504
- # the *0 is a trick to preserve NaN values.
443
+ # NOTE: the *0 is a trick to preserve NaN values.
505
444
panel_norm = np .array ([sind (tracker_theta ),
506
445
tracker_theta * 0 ,
507
446
cosd (tracker_theta )])
@@ -512,99 +451,49 @@ def singleaxis(apparent_zenith, apparent_azimuth,
512
451
# calculate angle-of-incidence on panel
513
452
aoi = np .degrees (np .arccos (np .abs (np .sum (sun_vec * panel_norm , axis = 0 ))))
514
453
515
- # calculate panel tilt and azimuth
516
- # in a coordinate system where the panel tilt is the
517
- # angle from horizontal, and the panel azimuth is
518
- # the compass angle (clockwise from north) to the projection
519
- # of the panel's normal to the earth's surface.
520
- # These outputs are provided for convenience and comparison
521
- # with other PV software which use these angle conventions.
454
+ # Calculate panel tilt and azimuth in a coordinate system where the panel
455
+ # tilt is the angle from horizontal, and the panel azimuth is the compass
456
+ # angle (clockwise from north) to the projection of the panel's normal to
457
+ # the earth's surface. These outputs are provided for convenience and
458
+ # comparison with other PV software which use these angle conventions.
522
459
523
- # project normal vector to earth surface.
524
- # First rotate about x-axis by angle -axis_tilt so that y-axis is
525
- # also parallel to earth surface, then project.
460
+ # Project normal vector to earth surface. First rotate about x-axis by
461
+ # angle -axis_tilt so that y-axis is also parallel to earth surface, then
462
+ # project.
526
463
527
464
# Calculate standard rotation matrix
528
465
rot_x = np .array ([[1 , 0 , 0 ],
529
466
[0 , cosd (- axis_tilt ), - sind (- axis_tilt )],
530
467
[0 , sind (- axis_tilt ), cosd (- axis_tilt )]])
531
468
532
- # panel_norm_earth contains the normal vector
533
- # expressed in earth- surface coordinates
534
- # (z normal to surface, y aligned with tracker axis parallel to earth)
469
+ # panel_norm_earth contains the normal vector expressed in earth-surface
470
+ # coordinates (z normal to surface, y aligned with tracker axis parallel to
471
+ # earth)
535
472
panel_norm_earth = np .dot (rot_x , panel_norm ).T
536
473
537
- # projection to plane tangent to earth surface,
538
- # in earth surface coordinates
474
+ # projection to plane tangent to earth surface, in earth surface
475
+ # coordinates
539
476
projected_normal = np .array ([panel_norm_earth [:, 0 ],
540
477
panel_norm_earth [:, 1 ],
541
478
panel_norm_earth [:, 2 ]* 0 ]).T
542
479
543
480
# calculate vector magnitudes
544
481
projected_normal_mag = np .sqrt (np .nansum (projected_normal ** 2 , axis = 1 ))
545
482
546
- # renormalize the projected vector
547
- # avoid creating nan values.
483
+ # renormalize the projected vector, avoid creating nan values.
548
484
non_zeros = projected_normal_mag != 0
549
485
projected_normal [non_zeros ] = (projected_normal [non_zeros ].T /
550
486
projected_normal_mag [non_zeros ]).T
551
487
552
488
# calculation of surface_azimuth
553
- # 1. Find the angle.
554
- # surface_azimuth = pd.Series(
555
- # np.degrees(np.arctan(projected_normal[:,1]/projected_normal[:,0])),
556
- # index=times)
557
489
surface_azimuth = \
558
490
np .degrees (np .arctan2 (projected_normal [:, 1 ], projected_normal [:, 0 ]))
559
491
560
- # 2. Clean up atan when x-coord or y-coord is zero
561
- # surface_azimuth[(projected_normal[:,0]==0) & (projected_normal[:,1]>0)] = 90
562
- # surface_azimuth[(projected_normal[:,0]==0) & (projected_normal[:,1]<0)] = -90
563
- # surface_azimuth[(projected_normal[:,1]==0) & (projected_normal[:,0]>0)] = 0
564
- # surface_azimuth[(projected_normal[:,1]==0) & (projected_normal[:,0]<0)] = 180
565
-
566
- # 3. Correct atan for QII and QIII
567
- # surface_azimuth[(projected_normal[:,0]<0) & (projected_normal[:,1]>0)] += 180 # QII
568
- # surface_azimuth[(projected_normal[:,0]<0) & (projected_normal[:,1]<0)] += 180 # QIII
569
-
570
- # 4. Skip to below
571
-
572
- # at this point surface_azimuth contains angles between -90 and +270,
573
- # where 0 is along the positive x-axis,
574
- # the y-axis is in the direction of the tracker azimuth,
575
- # and positive angles are rotations from the positive x axis towards
576
- # the positive y-axis.
577
- # Adjust to compass angles
578
- # (clockwise rotation from 0 along the positive y-axis)
579
- # surface_azimuth[surface_azimuth<=90] = 90 - surface_azimuth[surface_azimuth<=90]
580
- # surface_azimuth[surface_azimuth>90] = 450 - surface_azimuth[surface_azimuth>90]
581
-
582
- # finally rotate to align y-axis with true north
583
- # PVLIB_MATLAB has this latitude correction,
584
- # but I don't think it's latitude dependent if you always
585
- # specify axis_azimuth with respect to North.
586
- # if latitude > 0 or True:
587
- # surface_azimuth = surface_azimuth - axis_azimuth
588
- # else:
589
- # surface_azimuth = surface_azimuth - axis_azimuth - 180
590
- # surface_azimuth[surface_azimuth<0] = 360 + surface_azimuth[surface_azimuth<0]
591
-
592
- # the commented code above is mostly part of PVLIB_MATLAB.
593
- # My (wholmgren) take is that it can be done more simply.
594
- # Say that we're pointing along the postive x axis (likely west).
595
- # We just need to rotate 90 degrees to get from the x axis
596
- # to the y axis (likely south),
597
- # and then add the axis_azimuth to get back to North.
598
- # Anything left over is the azimuth that we want,
599
- # and we can map it into the [0,360) domain.
600
-
601
- # 4. Rotate 0 reference from panel's x axis to it's y axis and
602
- # then back to North.
492
+ # Rotate 0 reference from panel's x-axis to its y-axis and then back to
493
+ # north.
603
494
surface_azimuth = 90 - surface_azimuth + axis_azimuth
604
495
605
- # 5. Map azimuth into [0,360) domain.
606
- # surface_azimuth[surface_azimuth < 0] += 360
607
- # surface_azimuth[surface_azimuth >= 360] -= 360
496
+ # Map azimuth into [0,360) domain.
608
497
with np .errstate (invalid = 'ignore' ):
609
498
surface_azimuth = surface_azimuth % 360
610
499
0 commit comments