11import numpy as np
22import pandas as pd
33
4- from pvlib .tools import cosd , sind , tand
4+ from pvlib .tools import cosd , sind , tand , acosd , asind
55from pvlib .pvsystem import (
66 PVSystem , Array , SingleAxisTrackerMount , _unwrap_single_value
77)
@@ -334,9 +334,9 @@ def singleaxis(apparent_zenith, apparent_azimuth,
334334 Returns
335335 -------
336336 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]
340340 * `aoi`: The angle-of-incidence of direct irradiance onto the
341341 rotated panel surface. [degrees]
342342 * `surface_tilt`: The angle between the panel surface and the earth
@@ -349,6 +349,7 @@ def singleaxis(apparent_zenith, apparent_azimuth,
349349 --------
350350 pvlib.tracking.calc_axis_tilt
351351 pvlib.tracking.calc_cross_axis_tilt
352+ pvlib.tracking.calc_surface_orientation
352353
353354 References
354355 ----------
@@ -396,9 +397,10 @@ def singleaxis(apparent_zenith, apparent_azimuth,
396397 cos_axis_tilt = cosd (axis_tilt )
397398 sin_axis_tilt = sind (axis_tilt )
398399 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)
402404 zp = (x * sin_axis_tilt * sin_axis_azimuth
403405 + y * sin_axis_tilt * cos_axis_azimuth
404406 + z * cos_axis_tilt )
@@ -446,88 +448,79 @@ def singleaxis(apparent_zenith, apparent_azimuth,
446448 # system-plane normal
447449 tracker_theta = np .clip (tracker_theta , - max_angle , max_angle )
448450
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 )
517457
518458 # Bundle DataFrame for return values and filter for sun below horizon.
519459 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 }
521461 if index is not None :
522462 out = pd .DataFrame (out , index = index )
523- out = out [['tracker_theta' , 'aoi' , 'surface_azimuth' , 'surface_tilt' ]]
524463 out [zen_gt_90 ] = np .nan
525464 else :
526465 out = {k : np .where (zen_gt_90 , np .nan , v ) for k , v in out .items ()}
527466
528467 return out
529468
530469
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+
531524def calc_axis_tilt (slope_azimuth , slope_tilt , axis_azimuth ):
532525 """
533526 Calculate tracker axis tilt in the global reference frame when on a sloped
0 commit comments