1
1
import numpy as np
2
2
import pandas as pd
3
3
4
- from pvlib .tools import cosd , sind , tand
4
+ from pvlib .tools import cosd , sind , tand , acosd , asind
5
5
from pvlib .pvsystem import (
6
6
PVSystem , Array , SingleAxisTrackerMount , _unwrap_single_value
7
7
)
@@ -334,9 +334,9 @@ def singleaxis(apparent_zenith, apparent_azimuth,
334
334
Returns
335
335
-------
336
336
dict or DataFrame with the following columns:
337
- * `tracker_theta`: The rotation angle of the tracker.
338
- tracker_theta = 0 is horizontal, and positive rotation angles are
339
- clockwise . [degrees]
337
+ * `tracker_theta`: The rotation angle of the tracker is a right-handed
338
+ rotation defined by `axis_azimuth`.
339
+ tracker_theta = 0 is horizontal . [degrees]
340
340
* `aoi`: The angle-of-incidence of direct irradiance onto the
341
341
rotated panel surface. [degrees]
342
342
* `surface_tilt`: The angle between the panel surface and the earth
@@ -349,6 +349,7 @@ def singleaxis(apparent_zenith, apparent_azimuth,
349
349
--------
350
350
pvlib.tracking.calc_axis_tilt
351
351
pvlib.tracking.calc_cross_axis_tilt
352
+ pvlib.tracking.calc_surface_orientation
352
353
353
354
References
354
355
----------
@@ -396,9 +397,10 @@ def singleaxis(apparent_zenith, apparent_azimuth,
396
397
cos_axis_tilt = cosd (axis_tilt )
397
398
sin_axis_tilt = sind (axis_tilt )
398
399
xp = x * cos_axis_azimuth - y * sin_axis_azimuth
399
- yp = (x * cos_axis_tilt * sin_axis_azimuth
400
- + y * cos_axis_tilt * cos_axis_azimuth
401
- - z * sin_axis_tilt )
400
+ # not necessary to calculate y'
401
+ # yp = (x*cos_axis_tilt*sin_axis_azimuth
402
+ # + y*cos_axis_tilt*cos_axis_azimuth
403
+ # - z*sin_axis_tilt)
402
404
zp = (x * sin_axis_tilt * sin_axis_azimuth
403
405
+ y * sin_axis_tilt * cos_axis_azimuth
404
406
+ z * cos_axis_tilt )
@@ -446,88 +448,79 @@ def singleaxis(apparent_zenith, apparent_azimuth,
446
448
# system-plane normal
447
449
tracker_theta = np .clip (tracker_theta , - max_angle , max_angle )
448
450
449
- # Calculate panel normal vector in panel-oriented x, y, z coordinates.
450
- # y-axis is axis of tracker rotation. tracker_theta is a compass angle
451
- # (clockwise is positive) rather than a trigonometric angle.
452
- # NOTE: the *0 is a trick to preserve NaN values.
453
- panel_norm = np .array ([sind (tracker_theta ),
454
- tracker_theta * 0 ,
455
- cosd (tracker_theta )])
456
-
457
- # sun position in vector format in panel-oriented x, y, z coordinates
458
- sun_vec = np .array ([xp , yp , zp ])
459
-
460
- # calculate angle-of-incidence on panel
461
- # TODO: use irradiance.aoi
462
- projection = np .clip (np .sum (sun_vec * panel_norm , axis = 0 ), - 1 , 1 )
463
- aoi = np .degrees (np .arccos (projection ))
464
-
465
- # Calculate panel tilt and azimuth in a coordinate system where the panel
466
- # tilt is the angle from horizontal, and the panel azimuth is the compass
467
- # angle (clockwise from north) to the projection of the panel's normal to
468
- # the earth's surface. These outputs are provided for convenience and
469
- # comparison with other PV software which use these angle conventions.
470
-
471
- # Project normal vector to earth surface. First rotate about x-axis by
472
- # angle -axis_tilt so that y-axis is also parallel to earth surface, then
473
- # project.
474
-
475
- # Calculate standard rotation matrix
476
- rot_x = np .array ([[1 , 0 , 0 ],
477
- [0 , cosd (- axis_tilt ), - sind (- axis_tilt )],
478
- [0 , sind (- axis_tilt ), cosd (- axis_tilt )]])
479
-
480
- # panel_norm_earth contains the normal vector expressed in earth-surface
481
- # coordinates (z normal to surface, y aligned with tracker axis parallel to
482
- # earth)
483
- panel_norm_earth = np .dot (rot_x , panel_norm ).T
484
-
485
- # projection to plane tangent to earth surface, in earth surface
486
- # coordinates
487
- projected_normal = np .array ([panel_norm_earth [:, 0 ],
488
- panel_norm_earth [:, 1 ],
489
- panel_norm_earth [:, 2 ]* 0 ]).T
490
-
491
- # calculate vector magnitudes
492
- projected_normal_mag = np .sqrt (np .nansum (projected_normal ** 2 , axis = 1 ))
493
-
494
- # renormalize the projected vector, avoid creating nan values.
495
- non_zeros = projected_normal_mag != 0
496
- projected_normal [non_zeros ] = (projected_normal [non_zeros ].T /
497
- projected_normal_mag [non_zeros ]).T
498
-
499
- # calculation of surface_azimuth
500
- surface_azimuth = \
501
- np .degrees (np .arctan2 (projected_normal [:, 1 ], projected_normal [:, 0 ]))
502
-
503
- # Rotate 0 reference from panel's x-axis to its y-axis and then back to
504
- # north.
505
- surface_azimuth = 90 - surface_azimuth + axis_azimuth
506
-
507
- # Map azimuth into [0,360) domain.
508
- with np .errstate (invalid = 'ignore' ):
509
- surface_azimuth = surface_azimuth % 360
510
-
511
- # Calculate surface_tilt
512
- dotproduct = (panel_norm_earth * projected_normal ).sum (axis = 1 )
513
- # for edge cases like axis_tilt=90, numpy's SIMD can produce values like
514
- # dotproduct = (1 + 2e-16). Clip off the excess so that arccos works:
515
- dotproduct = np .clip (dotproduct , - 1 , 1 )
516
- surface_tilt = 90 - np .degrees (np .arccos (dotproduct ))
451
+ # Calculate auxiliary angles
452
+ surface = calc_surface_orientation (tracker_theta , axis_tilt , axis_azimuth )
453
+ surface_tilt = surface ['surface_tilt' ]
454
+ surface_azimuth = surface ['surface_azimuth' ]
455
+ aoi = irradiance .aoi (surface_tilt , surface_azimuth ,
456
+ apparent_zenith , apparent_azimuth )
517
457
518
458
# Bundle DataFrame for return values and filter for sun below horizon.
519
459
out = {'tracker_theta' : tracker_theta , 'aoi' : aoi ,
520
- 'surface_tilt ' : surface_tilt , 'surface_azimuth ' : surface_azimuth }
460
+ 'surface_azimuth ' : surface_azimuth , 'surface_tilt ' : surface_tilt }
521
461
if index is not None :
522
462
out = pd .DataFrame (out , index = index )
523
- out = out [['tracker_theta' , 'aoi' , 'surface_azimuth' , 'surface_tilt' ]]
524
463
out [zen_gt_90 ] = np .nan
525
464
else :
526
465
out = {k : np .where (zen_gt_90 , np .nan , v ) for k , v in out .items ()}
527
466
528
467
return out
529
468
530
469
470
+ def calc_surface_orientation (tracker_theta , axis_tilt = 0 , axis_azimuth = 0 ):
471
+ """
472
+ Calculate the surface tilt and azimuth angles for a given tracker rotation.
473
+
474
+ Parameters
475
+ ----------
476
+ tracker_theta : numeric
477
+ Tracker rotation angle as a right-handed rotation around
478
+ the axis defined by ``axis_tilt`` and ``axis_azimuth``. For example,
479
+ with ``axis_tilt=0`` and ``axis_azimuth=180``, ``tracker_theta > 0``
480
+ results in ``surface_azimuth`` to the West while ``tracker_theta < 0``
481
+ results in ``surface_azimuth`` to the East. [degree]
482
+ axis_tilt : float, default 0
483
+ The tilt of the axis of rotation with respect to horizontal. [degree]
484
+ axis_azimuth : float, default 0
485
+ A value denoting the compass direction along which the axis of
486
+ rotation lies. Measured east of north. [degree]
487
+
488
+ Returns
489
+ -------
490
+ dict or DataFrame
491
+ Contains keys ``'surface_tilt'`` and ``'surface_azimuth'`` representing
492
+ the module orientation accounting for tracker rotation and axis
493
+ orientation. [degree]
494
+
495
+ References
496
+ ----------
497
+ .. [1] William F. Marion and Aron P. Dobos, "Rotation Angle for the Optimum
498
+ Tracking of One-Axis Trackers", Technical Report NREL/TP-6A20-58891,
499
+ July 2013. :doi:`10.2172/1089596`
500
+ """
501
+ with np .errstate (invalid = 'ignore' , divide = 'ignore' ):
502
+ surface_tilt = acosd (cosd (tracker_theta ) * cosd (axis_tilt ))
503
+
504
+ # clip(..., -1, +1) to prevent arcsin(1 + epsilon) issues:
505
+ azimuth_delta = asind (np .clip (sind (tracker_theta ) / sind (surface_tilt ),
506
+ a_min = - 1 , a_max = 1 ))
507
+ # Combine Eqs 2, 3, and 4:
508
+ azimuth_delta = np .where (abs (tracker_theta ) < 90 ,
509
+ azimuth_delta ,
510
+ - azimuth_delta + np .sign (tracker_theta ) * 180 )
511
+ # handle surface_tilt=0 case:
512
+ azimuth_delta = np .where (sind (surface_tilt ) != 0 , azimuth_delta , 90 )
513
+ surface_azimuth = (axis_azimuth + azimuth_delta ) % 360
514
+
515
+ out = {
516
+ 'surface_tilt' : surface_tilt ,
517
+ 'surface_azimuth' : surface_azimuth ,
518
+ }
519
+ if hasattr (tracker_theta , 'index' ):
520
+ out = pd .DataFrame (out )
521
+ return out
522
+
523
+
531
524
def calc_axis_tilt (slope_azimuth , slope_tilt , axis_azimuth ):
532
525
"""
533
526
Calculate tracker axis tilt in the global reference frame when on a sloped
0 commit comments