acgc.solar

Module for calculating solar position (zenith angle, elevation, azimuth)

The functions here are are vectorized and generally broadcast over xarray dimensions, making this program faster than PySolar. Calculations assume spherical Earth (not ellipsoidal). Results should be accurate to < 0.2 degree, which is less than radius of the sun. Other modules (e.g. pvlib) should be used for high-precision calculations.

  1#!/usr/local/bin/env python3
  2'''Module for calculating solar position (zenith angle, elevation, azimuth)
  3
  4The functions here are are vectorized and generally broadcast over xarray dimensions,
  5making this program faster than PySolar. Calculations assume spherical Earth (not ellipsoidal). 
  6Results should be accurate to < 0.2 degree, which is less than radius of the sun.
  7Other modules (e.g. pvlib) should be used for high-precision calculations.
  8
  9'''
 10
 11from collections import namedtuple
 12import warnings
 13import numpy as np
 14import pandas as pd
 15
 16pi180 = np.pi / 180
 17
 18def insolation_toa( lat, lon, datetime, **kwargs ):
 19    '''Insolation at top of the atmosphere, accounting for solar zenith angle
 20    
 21    Parameters
 22    ----------
 23    **kwargs passed to `solar_zenith_angle`
 24    
 25    Returns
 26    -------
 27    Insolation : float
 28        radiation flux density accounting for solar zenith angle, W/m2
 29    '''
 30
 31    solar_pos = solar_position( datetime )
 32
 33    S = solar_constant(datetime, solar_pos=solar_pos)
 34    sza = solar_zenith_angle(lat, lon, datetime, **kwargs, solar_pos=solar_pos )
 35
 36    return S * np.cos(sza)
 37
 38def solar_azimuth_angle( lat, lon, datetime ):
 39    '''Solar azimuth angle (degrees) for a latitude, longitude, date and time
 40    
 41    SAA is degrees clockwise from north.
 42    
 43    Parameters
 44    ----------
 45    lat : float or ndarray
 46        latitude in degrees
 47    lon : float or ndarray
 48        longitudes in degrees
 49    datetime : datetime-like or str
 50        date and time. Include time zone or UTC will be assumed
 51
 52    Returns
 53    -------
 54    saa : float or ndarray
 55        solar azimuth angle in degrees (clockwise from north)
 56    '''
 57    # Convert to pandas Timestamp, if needed
 58    datetime = _to_timestamp(datetime)
 59
 60    # Subsolar point, latitude longitude, degrees
 61    solar_lat = solar_latitude( datetime )
 62    solar_lon = solar_longitude( datetime )
 63
 64    # Vector pointing toward sun
 65    x = np.cos( solar_lat * pi180 ) * np.sin( (solar_lon - lon) * pi180 )
 66    y = np.cos( lat*pi180 ) * np.sin( solar_lat*pi180 ) \
 67        - np.sin( lat*pi180 ) * np.cos( solar_lat*pi180 ) \
 68            * np.cos( (solar_lon - lon) * pi180 )
 69
 70    # Azimuth angle from north, degrees
 71    saa = np.arctan2( x, y ) / pi180
 72
 73    # Change range [-180,180] to [0,360]
 74    return np.mod( saa+360, 360 )
 75
 76def solar_elevation_angle( lat, lon, alt, datetime,
 77                       refraction=False, temperature=10., pressure=101325. ):
 78    '''Solar elevation angle (degrees) above the horizon
 79
 80    The altitude parameter should be the vertical distance 
 81    above the surrounding terrain that defines the horizon,
 82    not necessarily the altitude above sea level or the altitude above ground level.
 83    For example, on a mountain peak that is 4000 m above sea level and 
 84    1500 m above the surrounding plateau, the relevant altitude is 1500 m.
 85    For an observer on the plateau, the relevant altitude is 0 m.
 86
 87    See documentation for `solar_zenith_angle` and `horizon_zenith_angle`.
 88
 89    Parameters
 90    ----------
 91    lat : float or ndarray
 92        latitude in degrees
 93    lon : float or ndarray
 94        longitudes in degrees
 95    alt : float or ndarray
 96        altitude above surrounding terrain that defines the horizon, meters
 97    datetime : datetime-like or str
 98        date and time. Include time zone or UTC will be assumed
 99    refraction : bool, optional (default=False)
100        specifies whether to account for atmospheric refraction
101    temperature : float or ndarray, optional (default=10)
102        surface atmospheric temperature (Celsius), only used for refraction calculation
103    pressure : float or ndarray, optional (default=101325)
104        surface atmospheric pressure (Pa), only used for refraction calculation
105    
106    Returns
107    -------
108    sea : float or ndarray
109        solar elevation angle in degrees at the designated locations and times
110        If refraction=False, this is the true solar elevation angle
111        If refraction=True, this is the apparent solar elevation angle
112    
113    '''
114
115    if refraction and np.any(alt):
116        warnings.warn( 'Atmospheric refraction is calculated for surface conditions, '
117                    + 'but an altitude above the surface was specified',
118                     category=UserWarning,
119                     stacklevel=2 )
120
121    sea = horizon_zenith_angle( alt ) \
122         - solar_zenith_angle( lat, lon, datetime, refraction, temperature, pressure )
123
124    return sea
125
126def solar_zenith_angle( lat, lon, datetime,
127                        refraction=False, temperature=10., pressure=101325.,
128                        solar_pos=None ):
129    '''Solar zenith angle (degrees) for a given latitude, longitude, date and time.
130    
131    Accounts for equation of time and (optionally) for atmospheric refraction.
132    Altitude of the observer is not accounted for, which can be important when the sun 
133    is near the horizon. 
134    
135    Results are accurate to tenths of a degree, except where altitude is important
136    (< 20 degrees solar elevation)
137
138    Parameters
139    ----------
140    lat : float or ndarray
141        latitude in degrees
142    lon : float or ndarray
143        longitudes in degrees
144    datetime : datetime-like or str
145        date and time. Include time zone or UTC will be assumed
146    refraction : bool, optional (default=False)
147        specifies whether to account for atmospheric refraction
148    temperature : float or ndarray, optional (default=10)
149        surface atmospheric temperature (Celsius), only used for refraction calculation
150    pressure : float or ndarray, optional (default=101325)
151        surface atmospheric pressure (Pa), only used for refraction calculation
152    
153    Returns
154    -------
155    sza : float or ndarray
156        solar zenith angle in degrees at the designated locations and times
157        If refraction=False, this is the true solar zenith angle
158        If refraction=True, this is the apparent solar zenith angle
159    '''
160    # Convert to pandas Timestamp, if needed
161    datetime = _to_timestamp(datetime)
162
163    # Solar declination, degrees
164    if solar_pos is None:
165        dec = solar_declination( datetime )
166    else:
167        dec = solar_pos.declination
168
169    # Hour angle, degrees
170    Ha = solar_hour_angle( lon, datetime, solar_pos )
171
172    # True solar zenith angle, radians
173    sza = np.arccos( np.sin(lat*pi180) * np.sin(dec*pi180) + \
174          np.cos(lat*pi180) * np.cos(dec*pi180) * np.cos(Ha*pi180) )
175
176    # Convert radians -> degrees
177    sza /= pi180
178
179    if refraction:
180        # Subtract refraction angle (degrees) from zenith angle.
181        # SZA is always smaller due to refraction.
182        sza -= refraction_angle( 90-sza, pressure, temperature )
183
184    return sza
185
186def sunrise_time( *args, **kwargs ):
187    '''Compute sunrise time
188    
189    See `sun_times` for Parameters.'''
190    result = sun_times( *args, **kwargs )
191    return result[0]
192
193def sunset_time( *args, **kwargs ):
194    '''Compute sunset time
195    
196    See `sun_times` for Parameters.'''
197    result = sun_times( *args, **kwargs )
198    return result[1]
199
200def day_length( *args, **kwargs ):
201    '''Compute length of daylight
202    
203    See `sun_times` for Parameters.'''
204    result = sun_times( *args, **kwargs )
205    return result[2]
206
207def solar_noon( *args, **kwargs ):
208    '''Compute time of solar noon (meridian transit)
209    
210    See `sun_times` for Parameters.'''
211    result = sun_times( *args, **kwargs )
212    return result[3]
213
214def solar_constant( datetime, solar_pos=None ):
215    '''Compute solar constant for specific date or dates
216    
217    Parameters
218    ----------
219    datetime : datetime-like
220    solar_pos : tuple (default=None)
221        solar position parameters from a prior call to `solar_position`
222
223    Returns
224    -------
225    S : float
226        Solar direct beam radiation flux density, W/m2
227    '''
228    if solar_pos is None:
229        solar_pos = solar_position( datetime )
230    S = 1361/solar_pos.distance**2
231
232    return S
233
234def solar_declination( datetime, fast=False ):
235    '''Calculate solar declination (degrees) for specified date
236    
237    Implements Eq. 9.68-9.72 from M.Z. Jacobson, Fundamentals of Atmospheric Modeling
238    
239    Parameters
240    ----------
241    datetime : datetime-like or str
242        date and time. Include time zone or UTC will be assumed
243    fast : bool (default=False)
244        Specifies using a faster but less accurate calculation
245
246    Returns
247    -------
248    dec : float
249        solar declination in degrees at the specified date
250    '''
251    # Convert to pandas Timestamp, if needed
252    datetime = _to_timestamp(datetime)
253
254    # Select the accurate or fast calculation
255    accurate = not fast
256
257    if accurate:
258
259        # Solar declination, degrees
260        dec, junk, junk, junk = solar_position( datetime )
261
262    else:
263        # Number of days since beginning of 2000
264        NJD = datetime - np.datetime64('2000-01-01')
265        try:
266            NJD = NJD.dt.days
267        except AttributeError:
268            NJD = NJD.days
269
270        # Obliquity, degrees
271        ob = 23.439 - 4e-7 * NJD
272
273        # Parameters for ecliptic, degrees
274        gm = 357.528 + 0.9856003 * NJD
275        lm = 280.460 + 0.9856474 * NJD
276
277        # Ecliptic longitude of sun, degrees
278        ec = lm + 1.915 * np.sin( gm * pi180 ) + 0.020 * np.sin( 2 * gm * pi180 )
279
280        #Solar declination, degrees
281        dec = np.arcsin( np.sin( ob * pi180 ) * np.sin( ec * pi180 ) ) / pi180
282
283    return dec
284
285def solar_latitude( datetime ):
286    '''Latitude of the subsolar point
287    
288    Parameters
289    ----------
290    datetimeUTC : datetime-like or str
291        date and time. Include time zone or UTC will be assumed
292    
293    Returns
294    -------
295    latitude : float
296        degrees of latitude
297    '''
298    return solar_declination( datetime )
299
300def solar_longitude( datetime, solar_pos=None ):
301    '''Longitude of the subsolar point, degrees
302    
303    Parameters
304    ----------
305    datetimeUTC : datetime-like or str
306        date and time. Include time zone or UTC will be assumed
307    
308    Returns
309    -------
310    longitude : float
311        degrees of longitude
312    '''
313    # Convert to pandas Timestamp, if needed
314    datetimeUTC = _to_timestamp_utc(datetime)
315
316    # Longitude of subsolar point, degrees
317    # Equation of time will be added below
318    try:
319        # Treat as xarray.DataArray or pandas.Series
320        solar_lon = - 15 * ( datetimeUTC.dt.hour +
321                          datetimeUTC.dt.minute / 60 +
322                          datetimeUTC.dt.second / 3600 - 12 )
323    except AttributeError:
324        solar_lon = - 15 * ( datetimeUTC.hour +
325                          datetimeUTC.minute / 60 +
326                          datetimeUTC.second / 3600 - 12 )
327
328    # Add equation of time to the solar longitude, degrees
329    if solar_pos is None:
330        eot = equation_of_time( datetimeUTC, degrees=True )
331    else:
332        eot = solar_pos.equation_of_time
333    solar_lon -= eot
334
335    return solar_lon
336
337def solar_hour_angle( lon, datetime, solar_pos=None ):
338    '''Solar hour angle (degrees) for specified longitude, date and time
339
340    Hour angle is the angular displacement of the sun from the local meridian.
341    It is zero at local noon, negative in the morning, and positive is afternoon.
342    
343    Parameters
344    ----------
345    lon : float
346        longitude in degrees east
347    datetimeUTC : datetime-like or str
348        date and time. Include time zone or UTC will be assumed
349    
350    Returns
351    -------
352    ha : float
353        hour angle in degrees at the specified location and time
354    '''
355
356    # Subsolar longitude, degrees
357    solar_lon = solar_longitude(datetime, solar_pos)
358
359    # Hour angle, degrees
360    Ha = lon - solar_lon
361
362    return Ha
363
364def equation_of_time( datetime, degrees=False, fast=False ):
365    '''Equation of time for specified date
366    
367    Accounts for the solar day being slightly different from 24 hours
368
369    Parameters
370    ----------
371    datetime : datetime-like or str
372        date and time. Include time zone or UTC will be assumed
373    degrees : bool (default=False)
374        If True, then return value in compass degrees
375        If False, then return value in minutes of an hour
376    fast : bool (default=False)
377        specifies whether to use a faster, but less accurate calculation
378        
379    Returns
380    -------
381    eot : float
382        equation of time on the specified date, degrees or minutes
383    '''
384    # Convert to pandas Timestamp, if needed
385    datetime = _to_timestamp(datetime)
386
387    # Determine whether to use the fast or accurate calculation
388    accurate = not fast
389
390    if accurate:
391
392        # Equation of time, minutes
393        junk, junk, eot, junk = solar_position( datetime )
394
395    else:
396        # Implements the "alternative equation" from Wikipedia, derived from
397        # https://web.archive.org/web/20120323231813/http://www.green-life-innovators.org/tiki-index.php?page=The%2BLatitude%2Band%2BLongitude%2Bof%2Bthe%2BSun%2Bby%2BDavid%2BWilliams
398        # Results checked against NOAA solar calculator and agree within 10 seconds.
399        # Note: Leap years are not accounted for.
400
401        # Equation of time, accounts for the solar day differing slightly from 24 hr
402        try:
403            doy = datetime.dt.dayofyear
404        except AttributeError:
405            doy = datetime.dayofyear
406        W = 360 / 365.24
407        A = W * (doy+10)
408        B = A + 1.914 * np.sin( W * (doy-2) * pi180 )
409        C = ( A - np.arctan2( np.tan(B*pi180), np.cos(23.44*pi180) ) / pi180 ) / 180
410
411        # Equation of time in minutes of an hour (1440 minutes per day)
412        eot = 720 * ( C - np.round(C) )
413
414    # Equation of time, minutes -> degrees (360 degrees per day)
415    if degrees:
416        eot = eot / 60 * 360 / 24
417
418    return eot
419
420def solar_position( datetime ):
421    '''Compute position of sun (declination, right ascension, equation of time, distance)) on specified date
422    
423    Calculations follow the NOAA solar calculator spreadsheet
424    Applicable to years 1900-2100.
425
426    Parameters
427    ----------
428    date : datetime-like or str
429        date and time. Include time zone or UTC will be assumed
430    
431    Returns
432    -------
433    declination : float
434        position of the sun relative to Earth's equatorial plane, degrees
435    right_ascension : float
436        position of the sun along Earth's equatorial plane, degrees 
437        relative to the sun's position on the vernal equinox
438    equation_of_time : float
439        equation of time (minutes) between mean solar time and true solar time
440        Divide by 4 minutes per degree to obtain equation of time in degrees
441    distance : float
442        Earth-sun distance in AU (1 AU = 1.495978707e11 m)
443    '''
444    # Ensure time is Timestamp in UTC
445    datetimeUTC, tz_in = _to_timestamp_utc(datetime)
446
447    # Raise warning if any dates are outside date range
448    # recommended for orbital parameters used here
449    if np.logical_or( np.any( datetimeUTC < np.datetime64('1900-01-01') ),
450                      np.any( datetimeUTC > np.datetime64('2100-01-01') ) ):
451        warnings.warn('Solar position accuracy declines for dates outside 1900-2100', \
452                      RuntimeWarning )
453
454    # Number of days since 1 Jan 2000
455    NJD = datetimeUTC - np.datetime64('2000-01-01')
456    try:
457        NJD = NJD.dt.days
458    except AttributeError:
459        NJD = NJD.days
460
461    # Julian day (since 12:00 1 Jan 4713 BCE)
462    NJD += 2451544.50
463
464    # Julian century
465    JC = (NJD-2451545)/36525
466
467    # Earth orbital eccentricity, unitless
468    ec = 0.016708634 - JC*( 0.000042037 + 0.0000001267*JC )
469
470    # Earth mean orbital obliquity, degree
471    mean_ob = 23 + ( 26 + ( (21.448
472                             - JC * (46.815
473                                    + JC * (0.00059 - JC * 0.001813) ) ) )/60 )/60
474
475    # Earth true orbital obliquity, corrected for nutation, degree 
476    ob = mean_ob + 0.00256 * np.cos( (125.04 - 1934.136*JC ) * pi180 )
477
478    # Sun Mean ecliptic longitude, degree
479    mean_ec_lon = np.mod( 280.46646 + JC*( 36000.76983 + JC*0.0003032 ), 360 )
480
481    # Sun Mean anomaly, degree
482    mean_anom = 357.52911 + JC*( 35999.05029 - 0.0001537*JC )
483
484    # Sun Equation of center, degree
485    eq_center = np.sin(mean_anom*pi180) * (1.914602 - JC*( 0.004817 + 0.000014*JC )) \
486                    + np.sin(2*mean_anom*pi180) * (0.019993 - 0.000101*JC) \
487                    + np.sin(3*mean_anom*pi180) * 0.000289
488
489    # Sun True ecliptic longitude, degrees
490    true_ec_lon = mean_ec_lon + eq_center
491
492    # Sun True anomaly, degree
493    true_anom = mean_anom + eq_center
494
495    # Earth-Sun distance, AU
496    distance = (1.000001018 * (1-ec**2) ) / (1 + ec * np.cos( true_anom * pi180 ))
497
498    # Sun Apparent ecliptic longitude, corrected for nutation, degrees
499    ec_lon = true_ec_lon - 0.00569 - 0.00478 * np.sin( (125.04 - 1934.136*JC ) * pi180)
500
501    # Sun Right ascension, deg
502    right_ascension = np.arctan2( np.cos(ob*pi180) * np.sin(ec_lon*pi180),
503                                  np.cos(ec_lon*pi180) ) / pi180
504
505    # Sun Declination, deg
506    declination = np.arcsin( np.sin(ob*pi180) * np.sin(ec_lon*pi180) ) / pi180
507
508    # var y
509    vary = np.tan( ob/2 * pi180 )**2
510
511    # Equation of time, minutes
512    eot = vary * np.sin( 2 * mean_ec_lon * pi180) \
513        - 2 * ec * np.sin( mean_anom * pi180 ) \
514        + 4 * ec * vary * np.sin( mean_anom * pi180 ) * np.cos( 2 * mean_ec_lon * pi180) \
515        - 0.5 * vary**2 * np.sin( 4 * mean_ec_lon * pi180) \
516        - 1.25 * ec**2 * np.sin( 2 * mean_anom * pi180)
517    eot = eot * 4 / pi180
518
519    # Define named tuple to hold result
520    solar_pos_tuple = namedtuple('SolarPosition',
521                        'declination right_ascension equation_of_time distance datetimeUTC')
522    result = solar_pos_tuple(declination, right_ascension, eot, distance, datetimeUTC)
523
524    return result
525
526def sun_times( lat, lon, datetime, tz_out=None, sza_sunrise=90.833, fast=False ):
527    '''Compute times of sunrise, sunset, solar noon, and day length
528    
529    Common options for solar zenith angle at sunrise
530    1. 90.833 for first edge of sun rising, typical (0.567°) refraction (default)
531    2. 90.267 for first edge of sun rising, no refraction
532    3. 90 degrees for center of sun rising, no refraction
533
534    Parameters
535    ----------
536    lat : float or ndarray
537        latitude in degrees
538    lon : float or ndarray
539        longitudes in degrees
540    datetime : datetime-like or str
541        datetime, provide a time zone or UTC will be assumed 
542    tz_out : str, pytz.timezone, datetime.tzinfo or None (default=None)
543        timezone to be used for output times. 
544        If None is provided, then result will be in same time zone as input or UTC
545    sza_sunrise : float (default=90.833)
546        Solar zenith angle at which sunrise and sunset are calculated, degrees
547    fast : bool (default=False)
548        Select a faster but less accurate calculation
549
550    Returns
551    -------
552    sunrise : pandas.DatetimeIndex
553        sunrise time
554    sunset : pandas.DatetimeIndex
555        sunset time
556    day_length : pandas.Timedelta
557        duration of daylight
558    solar_noon : pandas.DatetimeIndex
559        time of meridian transit
560    '''
561    # Convert to pandas Timestamp in UTC, if needed
562    datetimeUTC, tz_in = _to_timestamp_utc(datetime)
563
564    # If no output timezone is specified, use the input time zone
565    if tz_out is None:
566        tz_out = tz_in
567
568    # Select fast or accurate calculation
569    accurate = not fast
570
571    # Solar declination (degrees) and equation of time (minutes)
572    if accurate:
573        dec, junk, eot, junk = solar_position( datetimeUTC )
574    else:
575        dec = solar_declination( datetimeUTC )
576        eot = equation_of_time( datetimeUTC )
577
578    # Sunrise hour angle, degree
579    # Degrees east of the local meridian where sun rises
580    ha_sunrise = np.arccos( np.cos(sza_sunrise*pi180) /
581                           (np.cos(lat*pi180) * np.cos(dec*pi180))
582                           - np.tan(lat*pi180)*np.tan(dec*pi180) ) / pi180
583
584    # Solar noon, local standard time, day fraction
585    solar_noon = (720 - 4*lon - eot ) / 1440
586
587    # Sunrise and sunset, local standard time, day fraction
588    t_sunrise = solar_noon - 4 * ha_sunrise / 1440
589    t_sunset  = solar_noon + 4 * ha_sunrise / 1440
590
591    # Date portion only
592    try:
593        # Convert to UTC
594        dateUTC = datetimeUTC.tz_convert('UTC').normalize()
595    except TypeError:
596        # Assume already in UTC
597        dateUTC = datetimeUTC.tz_localize('UTC').normalize()
598
599    # Convert day fraction -> date time
600    solar_noon = dateUTC + solar_noon * pd.Timedelta( 1, 'day' )
601    t_sunrise  = dateUTC + t_sunrise  * pd.Timedelta( 1, 'day' )
602    t_sunset   = dateUTC + t_sunset   * pd.Timedelta( 1, 'day' )
603
604    # Localize to input timezone
605    try:
606        solar_noon = solar_noon.dt.tz_convert(tz_out)
607        t_sunrise  = t_sunrise.dt.tz_convert(tz_out)
608        t_sunset   = t_sunset.dt.tz_convert(tz_out)
609    except AttributeError:
610        solar_noon = solar_noon.tz_convert(tz_out)
611        t_sunrise  = t_sunrise.tz_convert(tz_out)
612        t_sunset   = t_sunset.tz_convert(tz_out)
613
614    # Sunlight duration, minutes
615    day_length = 8 * ha_sunrise * pd.Timedelta(1, 'minute')
616
617    return t_sunrise, t_sunset, day_length, solar_noon
618
619def horizon_zenith_angle( alt ):
620    '''Angle from the zenith to the horizon
621    
622    The horizon is the locii of points where a line from the 
623    observation location to the ellipsoid is tangent to the ellipsoid surface.
624    
625    The altitude parameter should be the vertical distance 
626    above the surrounding terrain that defines the horizon,
627    not necessarily the altitude above sea level or the altitude above ground level.
628    For example, on a mountain peak that is 4000 m above sea level and 
629    1500 m above the surrounding plateau, the relevant altitude is 1500 m.
630    For an observer on the plateau, the relevant altitude is 0 m.
631
632    The implementation below assumes a spherical Earth.
633    Results using the WGS84 ellipsoid (see commented code below)
634    differ from the spherical case by << 1°. Terrain,
635    which is neglected here, has a larger effect on the horizon
636    location, so the simpler spherical calculation is appropriate. 
637
638    Parameters
639    ----------
640    lat : float or ndarray
641        latitude in degrees
642    alt : float or ndarray
643        altitude above surrounding terrain that defines the horizon, meters
644        
645    Returns
646    -------
647    hza : float or ndarray
648        horizon zenith angle in degrees
649    '''
650
651    # WGS84 ellipsoid parameters
652    # semi-major radius, m
653    r_earth = 6378137.0
654    # ellipsoidal flattening, unitless
655    f = 1/298.257223563
656
657    # Horizon zenith angle, degrees (spherical earth)
658    hza = 180 - np.arcsin( r_earth / ( r_earth + alt ) ) / pi180
659
660    ## Ellipsoidal Earth
661    # # Eccentricity of ellipsoid
662    # ecc = f * (2-f)
663    # # Local (i.e. prime vertical) radius of curvature at latitude
664    # N = r_earth / np.sqrt( 1 - ecc**2 * np.sin(lat*pi180)**2 )
665    # # Horizon zenith angle, degrees
666    # hza = 180 - np.arcsin( N / (N+alt) ) / pi180
667
668    return hza
669
670def refraction_angle( true_elevation_angle, pressure=101325., temperature_celsius=10. ):
671    '''Atmospheric refraction angle for light passing through Earth's atmosphere
672
673    The apparent locations in the sky of objects outsides Earth's atmosphere 
674    differs from their true locations due to atmospheric refraction. 
675    (e.g. sun and moon when they rise and set)
676    The apparent elevation of an object above the horizon is
677    apparent elevation angle = (true elevation angle) + (refraction angle)
678    
679    The equations here are from Saemundsson/Bennett, whose calculations use
680    a typical vertical profile of atmospheric density (i.e. temperature and pressure).
681    The profiles can be rescaled to a particular surface temperature and pressure
682    to approximately account for varying atmospheric conditions.
683    Accurate refraction calculations should use fully specified vertical profile
684    of temperature and pressure, which cannot be done here.
685
686    Parameters
687    ----------
688    true_elevation_angle : float
689        degrees above horizon of sun or other object
690    pressure : float (default=101325)
691        surface atmospheric pressure (Pa)
692    temperature_celsius : float (default=10)
693        surface atmospheric temperature (C)
694
695    Returns
696    -------
697    angle : float
698        refraction angle in degrees. Value is zero when apparent elevation is below horizon
699    '''
700    # Refraction angle, arcminutes
701    R = 1.02 / np.tan( ( true_elevation_angle + 10.3 / (true_elevation_angle + 5.11) ) * pi180 )
702    # Account for temperature and pressure, arcminutes
703    R = R * pressure / 101325 * 283 / ( 273 + temperature_celsius )
704    # Convert arcminutes -> degrees
705    R /= 60
706
707    # Result must be positive
708    R = np.maximum(R,0)
709
710    # Refraction defined only when the apparent elevation angle is positive
711    # Set refraction to zero when the apparent elevation is below horizon
712    refraction_angle = np.where( true_elevation_angle + R <= 0, 0, R)
713
714    return refraction_angle
715
716def _to_timestamp(time_in):
717    '''Convert input to Pandas Timestamp or Series of datetime64 
718    
719    Parameters
720    ----------
721    time_in : datetime-like or array
722        time to be converted
723
724    Returns
725    -------
726    time_out : pandas.Timestamp or pandas.Series of datetime64
727    '''
728    if hasattr(time_in,'dt'):
729        time_out = time_in
730    # elif isinstance(time_in, pd.DatetimeIndex ):
731    #     # Unnecessary; DatetimeIndex will work fine in the else cases
732    #     time_out = pd.Series(time_in)
733    #     tz = time_out.dt.tz
734    else:
735        try:
736            # Convert list of times
737            time_out = pd.Series(pd.DatetimeIndex(time_in))
738        except TypeError:
739            # Single datetime or str
740            time_out = pd.Timestamp(time_in)
741
742    return time_out
743
744def _to_timestamp_utc( datetime_in ):
745
746    # Ensure input is a timestamp
747    datetime_in = _to_timestamp( datetime_in )
748
749    # Convert to UTC, then strip timezone
750    try:
751        try:
752            tz_in = datetime_in.dt.tz
753            datetimeUTC = datetime_in.dt.tz_convert('UTC').tz.tz_localize(None)
754        except AttributeError:
755            tz_in = datetime_in.tzinfo
756            datetimeUTC = datetime_in.tz_convert('UTC').tz_localize(None)
757    except TypeError:
758        # No timezone info, so assume it is already UTC
759        datetimeUTC = datetime_in
760        tz_in = None
761
762    return datetimeUTC, tz_in
763
764# Aliases for functions
765sza = solar_zenith_angle
766saa = solar_azimuth_angle
767sea = solar_elevation_angle
768# Additional aliases for backwards compatibility
769equationOfTime = equation_of_time
770solarDeclination = solar_declination
pi180 = 0.017453292519943295
def insolation_toa(lat, lon, datetime, **kwargs):
19def insolation_toa( lat, lon, datetime, **kwargs ):
20    '''Insolation at top of the atmosphere, accounting for solar zenith angle
21    
22    Parameters
23    ----------
24    **kwargs passed to `solar_zenith_angle`
25    
26    Returns
27    -------
28    Insolation : float
29        radiation flux density accounting for solar zenith angle, W/m2
30    '''
31
32    solar_pos = solar_position( datetime )
33
34    S = solar_constant(datetime, solar_pos=solar_pos)
35    sza = solar_zenith_angle(lat, lon, datetime, **kwargs, solar_pos=solar_pos )
36
37    return S * np.cos(sza)

Insolation at top of the atmosphere, accounting for solar zenith angle

Parameters
Returns
  • Insolation (float): radiation flux density accounting for solar zenith angle, W/m2
def solar_azimuth_angle(lat, lon, datetime):
39def solar_azimuth_angle( lat, lon, datetime ):
40    '''Solar azimuth angle (degrees) for a latitude, longitude, date and time
41    
42    SAA is degrees clockwise from north.
43    
44    Parameters
45    ----------
46    lat : float or ndarray
47        latitude in degrees
48    lon : float or ndarray
49        longitudes in degrees
50    datetime : datetime-like or str
51        date and time. Include time zone or UTC will be assumed
52
53    Returns
54    -------
55    saa : float or ndarray
56        solar azimuth angle in degrees (clockwise from north)
57    '''
58    # Convert to pandas Timestamp, if needed
59    datetime = _to_timestamp(datetime)
60
61    # Subsolar point, latitude longitude, degrees
62    solar_lat = solar_latitude( datetime )
63    solar_lon = solar_longitude( datetime )
64
65    # Vector pointing toward sun
66    x = np.cos( solar_lat * pi180 ) * np.sin( (solar_lon - lon) * pi180 )
67    y = np.cos( lat*pi180 ) * np.sin( solar_lat*pi180 ) \
68        - np.sin( lat*pi180 ) * np.cos( solar_lat*pi180 ) \
69            * np.cos( (solar_lon - lon) * pi180 )
70
71    # Azimuth angle from north, degrees
72    saa = np.arctan2( x, y ) / pi180
73
74    # Change range [-180,180] to [0,360]
75    return np.mod( saa+360, 360 )

Solar azimuth angle (degrees) for a latitude, longitude, date and time

SAA is degrees clockwise from north.

Parameters
  • lat (float or ndarray): latitude in degrees
  • lon (float or ndarray): longitudes in degrees
  • datetime (datetime-like or str): date and time. Include time zone or UTC will be assumed
Returns
  • saa (float or ndarray): solar azimuth angle in degrees (clockwise from north)
def solar_elevation_angle( lat, lon, alt, datetime, refraction=False, temperature=10.0, pressure=101325.0):
 77def solar_elevation_angle( lat, lon, alt, datetime,
 78                       refraction=False, temperature=10., pressure=101325. ):
 79    '''Solar elevation angle (degrees) above the horizon
 80
 81    The altitude parameter should be the vertical distance 
 82    above the surrounding terrain that defines the horizon,
 83    not necessarily the altitude above sea level or the altitude above ground level.
 84    For example, on a mountain peak that is 4000 m above sea level and 
 85    1500 m above the surrounding plateau, the relevant altitude is 1500 m.
 86    For an observer on the plateau, the relevant altitude is 0 m.
 87
 88    See documentation for `solar_zenith_angle` and `horizon_zenith_angle`.
 89
 90    Parameters
 91    ----------
 92    lat : float or ndarray
 93        latitude in degrees
 94    lon : float or ndarray
 95        longitudes in degrees
 96    alt : float or ndarray
 97        altitude above surrounding terrain that defines the horizon, meters
 98    datetime : datetime-like or str
 99        date and time. Include time zone or UTC will be assumed
100    refraction : bool, optional (default=False)
101        specifies whether to account for atmospheric refraction
102    temperature : float or ndarray, optional (default=10)
103        surface atmospheric temperature (Celsius), only used for refraction calculation
104    pressure : float or ndarray, optional (default=101325)
105        surface atmospheric pressure (Pa), only used for refraction calculation
106    
107    Returns
108    -------
109    sea : float or ndarray
110        solar elevation angle in degrees at the designated locations and times
111        If refraction=False, this is the true solar elevation angle
112        If refraction=True, this is the apparent solar elevation angle
113    
114    '''
115
116    if refraction and np.any(alt):
117        warnings.warn( 'Atmospheric refraction is calculated for surface conditions, '
118                    + 'but an altitude above the surface was specified',
119                     category=UserWarning,
120                     stacklevel=2 )
121
122    sea = horizon_zenith_angle( alt ) \
123         - solar_zenith_angle( lat, lon, datetime, refraction, temperature, pressure )
124
125    return sea

Solar elevation angle (degrees) above the horizon

The altitude parameter should be the vertical distance above the surrounding terrain that defines the horizon, not necessarily the altitude above sea level or the altitude above ground level. For example, on a mountain peak that is 4000 m above sea level and 1500 m above the surrounding plateau, the relevant altitude is 1500 m. For an observer on the plateau, the relevant altitude is 0 m.

See documentation for solar_zenith_angle and horizon_zenith_angle.

Parameters
  • lat (float or ndarray): latitude in degrees
  • lon (float or ndarray): longitudes in degrees
  • alt (float or ndarray): altitude above surrounding terrain that defines the horizon, meters
  • datetime (datetime-like or str): date and time. Include time zone or UTC will be assumed
  • refraction (bool, optional (default=False)): specifies whether to account for atmospheric refraction
  • temperature (float or ndarray, optional (default=10)): surface atmospheric temperature (Celsius), only used for refraction calculation
  • pressure (float or ndarray, optional (default=101325)): surface atmospheric pressure (Pa), only used for refraction calculation
Returns
  • sea (float or ndarray): solar elevation angle in degrees at the designated locations and times If refraction=False, this is the true solar elevation angle If refraction=True, this is the apparent solar elevation angle
def solar_zenith_angle( lat, lon, datetime, refraction=False, temperature=10.0, pressure=101325.0, solar_pos=None):
127def solar_zenith_angle( lat, lon, datetime,
128                        refraction=False, temperature=10., pressure=101325.,
129                        solar_pos=None ):
130    '''Solar zenith angle (degrees) for a given latitude, longitude, date and time.
131    
132    Accounts for equation of time and (optionally) for atmospheric refraction.
133    Altitude of the observer is not accounted for, which can be important when the sun 
134    is near the horizon. 
135    
136    Results are accurate to tenths of a degree, except where altitude is important
137    (< 20 degrees solar elevation)
138
139    Parameters
140    ----------
141    lat : float or ndarray
142        latitude in degrees
143    lon : float or ndarray
144        longitudes in degrees
145    datetime : datetime-like or str
146        date and time. Include time zone or UTC will be assumed
147    refraction : bool, optional (default=False)
148        specifies whether to account for atmospheric refraction
149    temperature : float or ndarray, optional (default=10)
150        surface atmospheric temperature (Celsius), only used for refraction calculation
151    pressure : float or ndarray, optional (default=101325)
152        surface atmospheric pressure (Pa), only used for refraction calculation
153    
154    Returns
155    -------
156    sza : float or ndarray
157        solar zenith angle in degrees at the designated locations and times
158        If refraction=False, this is the true solar zenith angle
159        If refraction=True, this is the apparent solar zenith angle
160    '''
161    # Convert to pandas Timestamp, if needed
162    datetime = _to_timestamp(datetime)
163
164    # Solar declination, degrees
165    if solar_pos is None:
166        dec = solar_declination( datetime )
167    else:
168        dec = solar_pos.declination
169
170    # Hour angle, degrees
171    Ha = solar_hour_angle( lon, datetime, solar_pos )
172
173    # True solar zenith angle, radians
174    sza = np.arccos( np.sin(lat*pi180) * np.sin(dec*pi180) + \
175          np.cos(lat*pi180) * np.cos(dec*pi180) * np.cos(Ha*pi180) )
176
177    # Convert radians -> degrees
178    sza /= pi180
179
180    if refraction:
181        # Subtract refraction angle (degrees) from zenith angle.
182        # SZA is always smaller due to refraction.
183        sza -= refraction_angle( 90-sza, pressure, temperature )
184
185    return sza

Solar zenith angle (degrees) for a given latitude, longitude, date and time.

Accounts for equation of time and (optionally) for atmospheric refraction. Altitude of the observer is not accounted for, which can be important when the sun is near the horizon.

Results are accurate to tenths of a degree, except where altitude is important (< 20 degrees solar elevation)

Parameters
  • lat (float or ndarray): latitude in degrees
  • lon (float or ndarray): longitudes in degrees
  • datetime (datetime-like or str): date and time. Include time zone or UTC will be assumed
  • refraction (bool, optional (default=False)): specifies whether to account for atmospheric refraction
  • temperature (float or ndarray, optional (default=10)): surface atmospheric temperature (Celsius), only used for refraction calculation
  • pressure (float or ndarray, optional (default=101325)): surface atmospheric pressure (Pa), only used for refraction calculation
Returns
  • sza (float or ndarray): solar zenith angle in degrees at the designated locations and times If refraction=False, this is the true solar zenith angle If refraction=True, this is the apparent solar zenith angle
def sunrise_time(*args, **kwargs):
187def sunrise_time( *args, **kwargs ):
188    '''Compute sunrise time
189    
190    See `sun_times` for Parameters.'''
191    result = sun_times( *args, **kwargs )
192    return result[0]

Compute sunrise time

See sun_times for Parameters.

def sunset_time(*args, **kwargs):
194def sunset_time( *args, **kwargs ):
195    '''Compute sunset time
196    
197    See `sun_times` for Parameters.'''
198    result = sun_times( *args, **kwargs )
199    return result[1]

Compute sunset time

See sun_times for Parameters.

def day_length(*args, **kwargs):
201def day_length( *args, **kwargs ):
202    '''Compute length of daylight
203    
204    See `sun_times` for Parameters.'''
205    result = sun_times( *args, **kwargs )
206    return result[2]

Compute length of daylight

See sun_times for Parameters.

def solar_noon(*args, **kwargs):
208def solar_noon( *args, **kwargs ):
209    '''Compute time of solar noon (meridian transit)
210    
211    See `sun_times` for Parameters.'''
212    result = sun_times( *args, **kwargs )
213    return result[3]

Compute time of solar noon (meridian transit)

See sun_times for Parameters.

def solar_constant(datetime, solar_pos=None):
215def solar_constant( datetime, solar_pos=None ):
216    '''Compute solar constant for specific date or dates
217    
218    Parameters
219    ----------
220    datetime : datetime-like
221    solar_pos : tuple (default=None)
222        solar position parameters from a prior call to `solar_position`
223
224    Returns
225    -------
226    S : float
227        Solar direct beam radiation flux density, W/m2
228    '''
229    if solar_pos is None:
230        solar_pos = solar_position( datetime )
231    S = 1361/solar_pos.distance**2
232
233    return S

Compute solar constant for specific date or dates

Parameters
  • datetime (datetime-like):

  • solar_pos (tuple (default=None)): solar position parameters from a prior call to solar_position

Returns
  • S (float): Solar direct beam radiation flux density, W/m2
def solar_declination(datetime, fast=False):
235def solar_declination( datetime, fast=False ):
236    '''Calculate solar declination (degrees) for specified date
237    
238    Implements Eq. 9.68-9.72 from M.Z. Jacobson, Fundamentals of Atmospheric Modeling
239    
240    Parameters
241    ----------
242    datetime : datetime-like or str
243        date and time. Include time zone or UTC will be assumed
244    fast : bool (default=False)
245        Specifies using a faster but less accurate calculation
246
247    Returns
248    -------
249    dec : float
250        solar declination in degrees at the specified date
251    '''
252    # Convert to pandas Timestamp, if needed
253    datetime = _to_timestamp(datetime)
254
255    # Select the accurate or fast calculation
256    accurate = not fast
257
258    if accurate:
259
260        # Solar declination, degrees
261        dec, junk, junk, junk = solar_position( datetime )
262
263    else:
264        # Number of days since beginning of 2000
265        NJD = datetime - np.datetime64('2000-01-01')
266        try:
267            NJD = NJD.dt.days
268        except AttributeError:
269            NJD = NJD.days
270
271        # Obliquity, degrees
272        ob = 23.439 - 4e-7 * NJD
273
274        # Parameters for ecliptic, degrees
275        gm = 357.528 + 0.9856003 * NJD
276        lm = 280.460 + 0.9856474 * NJD
277
278        # Ecliptic longitude of sun, degrees
279        ec = lm + 1.915 * np.sin( gm * pi180 ) + 0.020 * np.sin( 2 * gm * pi180 )
280
281        #Solar declination, degrees
282        dec = np.arcsin( np.sin( ob * pi180 ) * np.sin( ec * pi180 ) ) / pi180
283
284    return dec

Calculate solar declination (degrees) for specified date

Implements Eq. 9.68-9.72 from M.Z. Jacobson, Fundamentals of Atmospheric Modeling

Parameters
  • datetime (datetime-like or str): date and time. Include time zone or UTC will be assumed
  • fast (bool (default=False)): Specifies using a faster but less accurate calculation
Returns
  • dec (float): solar declination in degrees at the specified date
def solar_latitude(datetime):
286def solar_latitude( datetime ):
287    '''Latitude of the subsolar point
288    
289    Parameters
290    ----------
291    datetimeUTC : datetime-like or str
292        date and time. Include time zone or UTC will be assumed
293    
294    Returns
295    -------
296    latitude : float
297        degrees of latitude
298    '''
299    return solar_declination( datetime )

Latitude of the subsolar point

Parameters
  • datetimeUTC (datetime-like or str): date and time. Include time zone or UTC will be assumed
Returns
  • latitude (float): degrees of latitude
def solar_longitude(datetime, solar_pos=None):
301def solar_longitude( datetime, solar_pos=None ):
302    '''Longitude of the subsolar point, degrees
303    
304    Parameters
305    ----------
306    datetimeUTC : datetime-like or str
307        date and time. Include time zone or UTC will be assumed
308    
309    Returns
310    -------
311    longitude : float
312        degrees of longitude
313    '''
314    # Convert to pandas Timestamp, if needed
315    datetimeUTC = _to_timestamp_utc(datetime)
316
317    # Longitude of subsolar point, degrees
318    # Equation of time will be added below
319    try:
320        # Treat as xarray.DataArray or pandas.Series
321        solar_lon = - 15 * ( datetimeUTC.dt.hour +
322                          datetimeUTC.dt.minute / 60 +
323                          datetimeUTC.dt.second / 3600 - 12 )
324    except AttributeError:
325        solar_lon = - 15 * ( datetimeUTC.hour +
326                          datetimeUTC.minute / 60 +
327                          datetimeUTC.second / 3600 - 12 )
328
329    # Add equation of time to the solar longitude, degrees
330    if solar_pos is None:
331        eot = equation_of_time( datetimeUTC, degrees=True )
332    else:
333        eot = solar_pos.equation_of_time
334    solar_lon -= eot
335
336    return solar_lon

Longitude of the subsolar point, degrees

Parameters
  • datetimeUTC (datetime-like or str): date and time. Include time zone or UTC will be assumed
Returns
  • longitude (float): degrees of longitude
def solar_hour_angle(lon, datetime, solar_pos=None):
338def solar_hour_angle( lon, datetime, solar_pos=None ):
339    '''Solar hour angle (degrees) for specified longitude, date and time
340
341    Hour angle is the angular displacement of the sun from the local meridian.
342    It is zero at local noon, negative in the morning, and positive is afternoon.
343    
344    Parameters
345    ----------
346    lon : float
347        longitude in degrees east
348    datetimeUTC : datetime-like or str
349        date and time. Include time zone or UTC will be assumed
350    
351    Returns
352    -------
353    ha : float
354        hour angle in degrees at the specified location and time
355    '''
356
357    # Subsolar longitude, degrees
358    solar_lon = solar_longitude(datetime, solar_pos)
359
360    # Hour angle, degrees
361    Ha = lon - solar_lon
362
363    return Ha

Solar hour angle (degrees) for specified longitude, date and time

Hour angle is the angular displacement of the sun from the local meridian. It is zero at local noon, negative in the morning, and positive is afternoon.

Parameters
  • lon (float): longitude in degrees east
  • datetimeUTC (datetime-like or str): date and time. Include time zone or UTC will be assumed
Returns
  • ha (float): hour angle in degrees at the specified location and time
def equation_of_time(datetime, degrees=False, fast=False):
365def equation_of_time( datetime, degrees=False, fast=False ):
366    '''Equation of time for specified date
367    
368    Accounts for the solar day being slightly different from 24 hours
369
370    Parameters
371    ----------
372    datetime : datetime-like or str
373        date and time. Include time zone or UTC will be assumed
374    degrees : bool (default=False)
375        If True, then return value in compass degrees
376        If False, then return value in minutes of an hour
377    fast : bool (default=False)
378        specifies whether to use a faster, but less accurate calculation
379        
380    Returns
381    -------
382    eot : float
383        equation of time on the specified date, degrees or minutes
384    '''
385    # Convert to pandas Timestamp, if needed
386    datetime = _to_timestamp(datetime)
387
388    # Determine whether to use the fast or accurate calculation
389    accurate = not fast
390
391    if accurate:
392
393        # Equation of time, minutes
394        junk, junk, eot, junk = solar_position( datetime )
395
396    else:
397        # Implements the "alternative equation" from Wikipedia, derived from
398        # https://web.archive.org/web/20120323231813/http://www.green-life-innovators.org/tiki-index.php?page=The%2BLatitude%2Band%2BLongitude%2Bof%2Bthe%2BSun%2Bby%2BDavid%2BWilliams
399        # Results checked against NOAA solar calculator and agree within 10 seconds.
400        # Note: Leap years are not accounted for.
401
402        # Equation of time, accounts for the solar day differing slightly from 24 hr
403        try:
404            doy = datetime.dt.dayofyear
405        except AttributeError:
406            doy = datetime.dayofyear
407        W = 360 / 365.24
408        A = W * (doy+10)
409        B = A + 1.914 * np.sin( W * (doy-2) * pi180 )
410        C = ( A - np.arctan2( np.tan(B*pi180), np.cos(23.44*pi180) ) / pi180 ) / 180
411
412        # Equation of time in minutes of an hour (1440 minutes per day)
413        eot = 720 * ( C - np.round(C) )
414
415    # Equation of time, minutes -> degrees (360 degrees per day)
416    if degrees:
417        eot = eot / 60 * 360 / 24
418
419    return eot

Equation of time for specified date

Accounts for the solar day being slightly different from 24 hours

Parameters
  • datetime (datetime-like or str): date and time. Include time zone or UTC will be assumed
  • degrees (bool (default=False)): If True, then return value in compass degrees If False, then return value in minutes of an hour
  • fast (bool (default=False)): specifies whether to use a faster, but less accurate calculation
Returns
  • eot (float): equation of time on the specified date, degrees or minutes
def solar_position(datetime):
421def solar_position( datetime ):
422    '''Compute position of sun (declination, right ascension, equation of time, distance)) on specified date
423    
424    Calculations follow the NOAA solar calculator spreadsheet
425    Applicable to years 1900-2100.
426
427    Parameters
428    ----------
429    date : datetime-like or str
430        date and time. Include time zone or UTC will be assumed
431    
432    Returns
433    -------
434    declination : float
435        position of the sun relative to Earth's equatorial plane, degrees
436    right_ascension : float
437        position of the sun along Earth's equatorial plane, degrees 
438        relative to the sun's position on the vernal equinox
439    equation_of_time : float
440        equation of time (minutes) between mean solar time and true solar time
441        Divide by 4 minutes per degree to obtain equation of time in degrees
442    distance : float
443        Earth-sun distance in AU (1 AU = 1.495978707e11 m)
444    '''
445    # Ensure time is Timestamp in UTC
446    datetimeUTC, tz_in = _to_timestamp_utc(datetime)
447
448    # Raise warning if any dates are outside date range
449    # recommended for orbital parameters used here
450    if np.logical_or( np.any( datetimeUTC < np.datetime64('1900-01-01') ),
451                      np.any( datetimeUTC > np.datetime64('2100-01-01') ) ):
452        warnings.warn('Solar position accuracy declines for dates outside 1900-2100', \
453                      RuntimeWarning )
454
455    # Number of days since 1 Jan 2000
456    NJD = datetimeUTC - np.datetime64('2000-01-01')
457    try:
458        NJD = NJD.dt.days
459    except AttributeError:
460        NJD = NJD.days
461
462    # Julian day (since 12:00 1 Jan 4713 BCE)
463    NJD += 2451544.50
464
465    # Julian century
466    JC = (NJD-2451545)/36525
467
468    # Earth orbital eccentricity, unitless
469    ec = 0.016708634 - JC*( 0.000042037 + 0.0000001267*JC )
470
471    # Earth mean orbital obliquity, degree
472    mean_ob = 23 + ( 26 + ( (21.448
473                             - JC * (46.815
474                                    + JC * (0.00059 - JC * 0.001813) ) ) )/60 )/60
475
476    # Earth true orbital obliquity, corrected for nutation, degree 
477    ob = mean_ob + 0.00256 * np.cos( (125.04 - 1934.136*JC ) * pi180 )
478
479    # Sun Mean ecliptic longitude, degree
480    mean_ec_lon = np.mod( 280.46646 + JC*( 36000.76983 + JC*0.0003032 ), 360 )
481
482    # Sun Mean anomaly, degree
483    mean_anom = 357.52911 + JC*( 35999.05029 - 0.0001537*JC )
484
485    # Sun Equation of center, degree
486    eq_center = np.sin(mean_anom*pi180) * (1.914602 - JC*( 0.004817 + 0.000014*JC )) \
487                    + np.sin(2*mean_anom*pi180) * (0.019993 - 0.000101*JC) \
488                    + np.sin(3*mean_anom*pi180) * 0.000289
489
490    # Sun True ecliptic longitude, degrees
491    true_ec_lon = mean_ec_lon + eq_center
492
493    # Sun True anomaly, degree
494    true_anom = mean_anom + eq_center
495
496    # Earth-Sun distance, AU
497    distance = (1.000001018 * (1-ec**2) ) / (1 + ec * np.cos( true_anom * pi180 ))
498
499    # Sun Apparent ecliptic longitude, corrected for nutation, degrees
500    ec_lon = true_ec_lon - 0.00569 - 0.00478 * np.sin( (125.04 - 1934.136*JC ) * pi180)
501
502    # Sun Right ascension, deg
503    right_ascension = np.arctan2( np.cos(ob*pi180) * np.sin(ec_lon*pi180),
504                                  np.cos(ec_lon*pi180) ) / pi180
505
506    # Sun Declination, deg
507    declination = np.arcsin( np.sin(ob*pi180) * np.sin(ec_lon*pi180) ) / pi180
508
509    # var y
510    vary = np.tan( ob/2 * pi180 )**2
511
512    # Equation of time, minutes
513    eot = vary * np.sin( 2 * mean_ec_lon * pi180) \
514        - 2 * ec * np.sin( mean_anom * pi180 ) \
515        + 4 * ec * vary * np.sin( mean_anom * pi180 ) * np.cos( 2 * mean_ec_lon * pi180) \
516        - 0.5 * vary**2 * np.sin( 4 * mean_ec_lon * pi180) \
517        - 1.25 * ec**2 * np.sin( 2 * mean_anom * pi180)
518    eot = eot * 4 / pi180
519
520    # Define named tuple to hold result
521    solar_pos_tuple = namedtuple('SolarPosition',
522                        'declination right_ascension equation_of_time distance datetimeUTC')
523    result = solar_pos_tuple(declination, right_ascension, eot, distance, datetimeUTC)
524
525    return result

Compute position of sun (declination, right ascension, equation of time, distance)) on specified date

Calculations follow the NOAA solar calculator spreadsheet Applicable to years 1900-2100.

Parameters
  • date (datetime-like or str): date and time. Include time zone or UTC will be assumed
Returns
  • declination (float): position of the sun relative to Earth's equatorial plane, degrees
  • right_ascension (float): position of the sun along Earth's equatorial plane, degrees relative to the sun's position on the vernal equinox
  • equation_of_time (float): equation of time (minutes) between mean solar time and true solar time Divide by 4 minutes per degree to obtain equation of time in degrees
  • distance (float): Earth-sun distance in AU (1 AU = 1.495978707e11 m)
def sun_times(lat, lon, datetime, tz_out=None, sza_sunrise=90.833, fast=False):
527def sun_times( lat, lon, datetime, tz_out=None, sza_sunrise=90.833, fast=False ):
528    '''Compute times of sunrise, sunset, solar noon, and day length
529    
530    Common options for solar zenith angle at sunrise
531    1. 90.833 for first edge of sun rising, typical (0.567°) refraction (default)
532    2. 90.267 for first edge of sun rising, no refraction
533    3. 90 degrees for center of sun rising, no refraction
534
535    Parameters
536    ----------
537    lat : float or ndarray
538        latitude in degrees
539    lon : float or ndarray
540        longitudes in degrees
541    datetime : datetime-like or str
542        datetime, provide a time zone or UTC will be assumed 
543    tz_out : str, pytz.timezone, datetime.tzinfo or None (default=None)
544        timezone to be used for output times. 
545        If None is provided, then result will be in same time zone as input or UTC
546    sza_sunrise : float (default=90.833)
547        Solar zenith angle at which sunrise and sunset are calculated, degrees
548    fast : bool (default=False)
549        Select a faster but less accurate calculation
550
551    Returns
552    -------
553    sunrise : pandas.DatetimeIndex
554        sunrise time
555    sunset : pandas.DatetimeIndex
556        sunset time
557    day_length : pandas.Timedelta
558        duration of daylight
559    solar_noon : pandas.DatetimeIndex
560        time of meridian transit
561    '''
562    # Convert to pandas Timestamp in UTC, if needed
563    datetimeUTC, tz_in = _to_timestamp_utc(datetime)
564
565    # If no output timezone is specified, use the input time zone
566    if tz_out is None:
567        tz_out = tz_in
568
569    # Select fast or accurate calculation
570    accurate = not fast
571
572    # Solar declination (degrees) and equation of time (minutes)
573    if accurate:
574        dec, junk, eot, junk = solar_position( datetimeUTC )
575    else:
576        dec = solar_declination( datetimeUTC )
577        eot = equation_of_time( datetimeUTC )
578
579    # Sunrise hour angle, degree
580    # Degrees east of the local meridian where sun rises
581    ha_sunrise = np.arccos( np.cos(sza_sunrise*pi180) /
582                           (np.cos(lat*pi180) * np.cos(dec*pi180))
583                           - np.tan(lat*pi180)*np.tan(dec*pi180) ) / pi180
584
585    # Solar noon, local standard time, day fraction
586    solar_noon = (720 - 4*lon - eot ) / 1440
587
588    # Sunrise and sunset, local standard time, day fraction
589    t_sunrise = solar_noon - 4 * ha_sunrise / 1440
590    t_sunset  = solar_noon + 4 * ha_sunrise / 1440
591
592    # Date portion only
593    try:
594        # Convert to UTC
595        dateUTC = datetimeUTC.tz_convert('UTC').normalize()
596    except TypeError:
597        # Assume already in UTC
598        dateUTC = datetimeUTC.tz_localize('UTC').normalize()
599
600    # Convert day fraction -> date time
601    solar_noon = dateUTC + solar_noon * pd.Timedelta( 1, 'day' )
602    t_sunrise  = dateUTC + t_sunrise  * pd.Timedelta( 1, 'day' )
603    t_sunset   = dateUTC + t_sunset   * pd.Timedelta( 1, 'day' )
604
605    # Localize to input timezone
606    try:
607        solar_noon = solar_noon.dt.tz_convert(tz_out)
608        t_sunrise  = t_sunrise.dt.tz_convert(tz_out)
609        t_sunset   = t_sunset.dt.tz_convert(tz_out)
610    except AttributeError:
611        solar_noon = solar_noon.tz_convert(tz_out)
612        t_sunrise  = t_sunrise.tz_convert(tz_out)
613        t_sunset   = t_sunset.tz_convert(tz_out)
614
615    # Sunlight duration, minutes
616    day_length = 8 * ha_sunrise * pd.Timedelta(1, 'minute')
617
618    return t_sunrise, t_sunset, day_length, solar_noon

Compute times of sunrise, sunset, solar noon, and day length

Common options for solar zenith angle at sunrise

  1. 90.833 for first edge of sun rising, typical (0.567°) refraction (default)
  2. 90.267 for first edge of sun rising, no refraction
  3. 90 degrees for center of sun rising, no refraction
Parameters
  • lat (float or ndarray): latitude in degrees
  • lon (float or ndarray): longitudes in degrees
  • datetime (datetime-like or str): datetime, provide a time zone or UTC will be assumed
  • tz_out (str, pytz.timezone, datetime.tzinfo or None (default=None)): timezone to be used for output times. If None is provided, then result will be in same time zone as input or UTC
  • sza_sunrise (float (default=90.833)): Solar zenith angle at which sunrise and sunset are calculated, degrees
  • fast (bool (default=False)): Select a faster but less accurate calculation
Returns
  • sunrise (pandas.DatetimeIndex): sunrise time
  • sunset (pandas.DatetimeIndex): sunset time
  • day_length (pandas.Timedelta): duration of daylight
  • solar_noon (pandas.DatetimeIndex): time of meridian transit
def horizon_zenith_angle(alt):
620def horizon_zenith_angle( alt ):
621    '''Angle from the zenith to the horizon
622    
623    The horizon is the locii of points where a line from the 
624    observation location to the ellipsoid is tangent to the ellipsoid surface.
625    
626    The altitude parameter should be the vertical distance 
627    above the surrounding terrain that defines the horizon,
628    not necessarily the altitude above sea level or the altitude above ground level.
629    For example, on a mountain peak that is 4000 m above sea level and 
630    1500 m above the surrounding plateau, the relevant altitude is 1500 m.
631    For an observer on the plateau, the relevant altitude is 0 m.
632
633    The implementation below assumes a spherical Earth.
634    Results using the WGS84 ellipsoid (see commented code below)
635    differ from the spherical case by << 1°. Terrain,
636    which is neglected here, has a larger effect on the horizon
637    location, so the simpler spherical calculation is appropriate. 
638
639    Parameters
640    ----------
641    lat : float or ndarray
642        latitude in degrees
643    alt : float or ndarray
644        altitude above surrounding terrain that defines the horizon, meters
645        
646    Returns
647    -------
648    hza : float or ndarray
649        horizon zenith angle in degrees
650    '''
651
652    # WGS84 ellipsoid parameters
653    # semi-major radius, m
654    r_earth = 6378137.0
655    # ellipsoidal flattening, unitless
656    f = 1/298.257223563
657
658    # Horizon zenith angle, degrees (spherical earth)
659    hza = 180 - np.arcsin( r_earth / ( r_earth + alt ) ) / pi180
660
661    ## Ellipsoidal Earth
662    # # Eccentricity of ellipsoid
663    # ecc = f * (2-f)
664    # # Local (i.e. prime vertical) radius of curvature at latitude
665    # N = r_earth / np.sqrt( 1 - ecc**2 * np.sin(lat*pi180)**2 )
666    # # Horizon zenith angle, degrees
667    # hza = 180 - np.arcsin( N / (N+alt) ) / pi180
668
669    return hza

Angle from the zenith to the horizon

The horizon is the locii of points where a line from the observation location to the ellipsoid is tangent to the ellipsoid surface.

The altitude parameter should be the vertical distance above the surrounding terrain that defines the horizon, not necessarily the altitude above sea level or the altitude above ground level. For example, on a mountain peak that is 4000 m above sea level and 1500 m above the surrounding plateau, the relevant altitude is 1500 m. For an observer on the plateau, the relevant altitude is 0 m.

The implementation below assumes a spherical Earth. Results using the WGS84 ellipsoid (see commented code below) differ from the spherical case by << 1°. Terrain, which is neglected here, has a larger effect on the horizon location, so the simpler spherical calculation is appropriate.

Parameters
  • lat (float or ndarray): latitude in degrees
  • alt (float or ndarray): altitude above surrounding terrain that defines the horizon, meters
Returns
  • hza (float or ndarray): horizon zenith angle in degrees
def refraction_angle(true_elevation_angle, pressure=101325.0, temperature_celsius=10.0):
671def refraction_angle( true_elevation_angle, pressure=101325., temperature_celsius=10. ):
672    '''Atmospheric refraction angle for light passing through Earth's atmosphere
673
674    The apparent locations in the sky of objects outsides Earth's atmosphere 
675    differs from their true locations due to atmospheric refraction. 
676    (e.g. sun and moon when they rise and set)
677    The apparent elevation of an object above the horizon is
678    apparent elevation angle = (true elevation angle) + (refraction angle)
679    
680    The equations here are from Saemundsson/Bennett, whose calculations use
681    a typical vertical profile of atmospheric density (i.e. temperature and pressure).
682    The profiles can be rescaled to a particular surface temperature and pressure
683    to approximately account for varying atmospheric conditions.
684    Accurate refraction calculations should use fully specified vertical profile
685    of temperature and pressure, which cannot be done here.
686
687    Parameters
688    ----------
689    true_elevation_angle : float
690        degrees above horizon of sun or other object
691    pressure : float (default=101325)
692        surface atmospheric pressure (Pa)
693    temperature_celsius : float (default=10)
694        surface atmospheric temperature (C)
695
696    Returns
697    -------
698    angle : float
699        refraction angle in degrees. Value is zero when apparent elevation is below horizon
700    '''
701    # Refraction angle, arcminutes
702    R = 1.02 / np.tan( ( true_elevation_angle + 10.3 / (true_elevation_angle + 5.11) ) * pi180 )
703    # Account for temperature and pressure, arcminutes
704    R = R * pressure / 101325 * 283 / ( 273 + temperature_celsius )
705    # Convert arcminutes -> degrees
706    R /= 60
707
708    # Result must be positive
709    R = np.maximum(R,0)
710
711    # Refraction defined only when the apparent elevation angle is positive
712    # Set refraction to zero when the apparent elevation is below horizon
713    refraction_angle = np.where( true_elevation_angle + R <= 0, 0, R)
714
715    return refraction_angle

Atmospheric refraction angle for light passing through Earth's atmosphere

The apparent locations in the sky of objects outsides Earth's atmosphere differs from their true locations due to atmospheric refraction. (e.g. sun and moon when they rise and set) The apparent elevation of an object above the horizon is apparent elevation angle = (true elevation angle) + (refraction angle)

The equations here are from Saemundsson/Bennett, whose calculations use a typical vertical profile of atmospheric density (i.e. temperature and pressure). The profiles can be rescaled to a particular surface temperature and pressure to approximately account for varying atmospheric conditions. Accurate refraction calculations should use fully specified vertical profile of temperature and pressure, which cannot be done here.

Parameters
  • true_elevation_angle (float): degrees above horizon of sun or other object
  • pressure (float (default=101325)): surface atmospheric pressure (Pa)
  • temperature_celsius (float (default=10)): surface atmospheric temperature (C)
Returns
  • angle (float): refraction angle in degrees. Value is zero when apparent elevation is below horizon
def sza( lat, lon, datetime, refraction=False, temperature=10.0, pressure=101325.0, solar_pos=None):
127def solar_zenith_angle( lat, lon, datetime,
128                        refraction=False, temperature=10., pressure=101325.,
129                        solar_pos=None ):
130    '''Solar zenith angle (degrees) for a given latitude, longitude, date and time.
131    
132    Accounts for equation of time and (optionally) for atmospheric refraction.
133    Altitude of the observer is not accounted for, which can be important when the sun 
134    is near the horizon. 
135    
136    Results are accurate to tenths of a degree, except where altitude is important
137    (< 20 degrees solar elevation)
138
139    Parameters
140    ----------
141    lat : float or ndarray
142        latitude in degrees
143    lon : float or ndarray
144        longitudes in degrees
145    datetime : datetime-like or str
146        date and time. Include time zone or UTC will be assumed
147    refraction : bool, optional (default=False)
148        specifies whether to account for atmospheric refraction
149    temperature : float or ndarray, optional (default=10)
150        surface atmospheric temperature (Celsius), only used for refraction calculation
151    pressure : float or ndarray, optional (default=101325)
152        surface atmospheric pressure (Pa), only used for refraction calculation
153    
154    Returns
155    -------
156    sza : float or ndarray
157        solar zenith angle in degrees at the designated locations and times
158        If refraction=False, this is the true solar zenith angle
159        If refraction=True, this is the apparent solar zenith angle
160    '''
161    # Convert to pandas Timestamp, if needed
162    datetime = _to_timestamp(datetime)
163
164    # Solar declination, degrees
165    if solar_pos is None:
166        dec = solar_declination( datetime )
167    else:
168        dec = solar_pos.declination
169
170    # Hour angle, degrees
171    Ha = solar_hour_angle( lon, datetime, solar_pos )
172
173    # True solar zenith angle, radians
174    sza = np.arccos( np.sin(lat*pi180) * np.sin(dec*pi180) + \
175          np.cos(lat*pi180) * np.cos(dec*pi180) * np.cos(Ha*pi180) )
176
177    # Convert radians -> degrees
178    sza /= pi180
179
180    if refraction:
181        # Subtract refraction angle (degrees) from zenith angle.
182        # SZA is always smaller due to refraction.
183        sza -= refraction_angle( 90-sza, pressure, temperature )
184
185    return sza

Solar zenith angle (degrees) for a given latitude, longitude, date and time.

Accounts for equation of time and (optionally) for atmospheric refraction. Altitude of the observer is not accounted for, which can be important when the sun is near the horizon.

Results are accurate to tenths of a degree, except where altitude is important (< 20 degrees solar elevation)

Parameters
  • lat (float or ndarray): latitude in degrees
  • lon (float or ndarray): longitudes in degrees
  • datetime (datetime-like or str): date and time. Include time zone or UTC will be assumed
  • refraction (bool, optional (default=False)): specifies whether to account for atmospheric refraction
  • temperature (float or ndarray, optional (default=10)): surface atmospheric temperature (Celsius), only used for refraction calculation
  • pressure (float or ndarray, optional (default=101325)): surface atmospheric pressure (Pa), only used for refraction calculation
Returns
  • sza (float or ndarray): solar zenith angle in degrees at the designated locations and times If refraction=False, this is the true solar zenith angle If refraction=True, this is the apparent solar zenith angle
def saa(lat, lon, datetime):
39def solar_azimuth_angle( lat, lon, datetime ):
40    '''Solar azimuth angle (degrees) for a latitude, longitude, date and time
41    
42    SAA is degrees clockwise from north.
43    
44    Parameters
45    ----------
46    lat : float or ndarray
47        latitude in degrees
48    lon : float or ndarray
49        longitudes in degrees
50    datetime : datetime-like or str
51        date and time. Include time zone or UTC will be assumed
52
53    Returns
54    -------
55    saa : float or ndarray
56        solar azimuth angle in degrees (clockwise from north)
57    '''
58    # Convert to pandas Timestamp, if needed
59    datetime = _to_timestamp(datetime)
60
61    # Subsolar point, latitude longitude, degrees
62    solar_lat = solar_latitude( datetime )
63    solar_lon = solar_longitude( datetime )
64
65    # Vector pointing toward sun
66    x = np.cos( solar_lat * pi180 ) * np.sin( (solar_lon - lon) * pi180 )
67    y = np.cos( lat*pi180 ) * np.sin( solar_lat*pi180 ) \
68        - np.sin( lat*pi180 ) * np.cos( solar_lat*pi180 ) \
69            * np.cos( (solar_lon - lon) * pi180 )
70
71    # Azimuth angle from north, degrees
72    saa = np.arctan2( x, y ) / pi180
73
74    # Change range [-180,180] to [0,360]
75    return np.mod( saa+360, 360 )

Solar azimuth angle (degrees) for a latitude, longitude, date and time

SAA is degrees clockwise from north.

Parameters
  • lat (float or ndarray): latitude in degrees
  • lon (float or ndarray): longitudes in degrees
  • datetime (datetime-like or str): date and time. Include time zone or UTC will be assumed
Returns
  • saa (float or ndarray): solar azimuth angle in degrees (clockwise from north)
def sea( lat, lon, alt, datetime, refraction=False, temperature=10.0, pressure=101325.0):
 77def solar_elevation_angle( lat, lon, alt, datetime,
 78                       refraction=False, temperature=10., pressure=101325. ):
 79    '''Solar elevation angle (degrees) above the horizon
 80
 81    The altitude parameter should be the vertical distance 
 82    above the surrounding terrain that defines the horizon,
 83    not necessarily the altitude above sea level or the altitude above ground level.
 84    For example, on a mountain peak that is 4000 m above sea level and 
 85    1500 m above the surrounding plateau, the relevant altitude is 1500 m.
 86    For an observer on the plateau, the relevant altitude is 0 m.
 87
 88    See documentation for `solar_zenith_angle` and `horizon_zenith_angle`.
 89
 90    Parameters
 91    ----------
 92    lat : float or ndarray
 93        latitude in degrees
 94    lon : float or ndarray
 95        longitudes in degrees
 96    alt : float or ndarray
 97        altitude above surrounding terrain that defines the horizon, meters
 98    datetime : datetime-like or str
 99        date and time. Include time zone or UTC will be assumed
100    refraction : bool, optional (default=False)
101        specifies whether to account for atmospheric refraction
102    temperature : float or ndarray, optional (default=10)
103        surface atmospheric temperature (Celsius), only used for refraction calculation
104    pressure : float or ndarray, optional (default=101325)
105        surface atmospheric pressure (Pa), only used for refraction calculation
106    
107    Returns
108    -------
109    sea : float or ndarray
110        solar elevation angle in degrees at the designated locations and times
111        If refraction=False, this is the true solar elevation angle
112        If refraction=True, this is the apparent solar elevation angle
113    
114    '''
115
116    if refraction and np.any(alt):
117        warnings.warn( 'Atmospheric refraction is calculated for surface conditions, '
118                    + 'but an altitude above the surface was specified',
119                     category=UserWarning,
120                     stacklevel=2 )
121
122    sea = horizon_zenith_angle( alt ) \
123         - solar_zenith_angle( lat, lon, datetime, refraction, temperature, pressure )
124
125    return sea

Solar elevation angle (degrees) above the horizon

The altitude parameter should be the vertical distance above the surrounding terrain that defines the horizon, not necessarily the altitude above sea level or the altitude above ground level. For example, on a mountain peak that is 4000 m above sea level and 1500 m above the surrounding plateau, the relevant altitude is 1500 m. For an observer on the plateau, the relevant altitude is 0 m.

See documentation for solar_zenith_angle and horizon_zenith_angle.

Parameters
  • lat (float or ndarray): latitude in degrees
  • lon (float or ndarray): longitudes in degrees
  • alt (float or ndarray): altitude above surrounding terrain that defines the horizon, meters
  • datetime (datetime-like or str): date and time. Include time zone or UTC will be assumed
  • refraction (bool, optional (default=False)): specifies whether to account for atmospheric refraction
  • temperature (float or ndarray, optional (default=10)): surface atmospheric temperature (Celsius), only used for refraction calculation
  • pressure (float or ndarray, optional (default=101325)): surface atmospheric pressure (Pa), only used for refraction calculation
Returns
  • sea (float or ndarray): solar elevation angle in degrees at the designated locations and times If refraction=False, this is the true solar elevation angle If refraction=True, this is the apparent solar elevation angle
def equationOfTime(datetime, degrees=False, fast=False):
365def equation_of_time( datetime, degrees=False, fast=False ):
366    '''Equation of time for specified date
367    
368    Accounts for the solar day being slightly different from 24 hours
369
370    Parameters
371    ----------
372    datetime : datetime-like or str
373        date and time. Include time zone or UTC will be assumed
374    degrees : bool (default=False)
375        If True, then return value in compass degrees
376        If False, then return value in minutes of an hour
377    fast : bool (default=False)
378        specifies whether to use a faster, but less accurate calculation
379        
380    Returns
381    -------
382    eot : float
383        equation of time on the specified date, degrees or minutes
384    '''
385    # Convert to pandas Timestamp, if needed
386    datetime = _to_timestamp(datetime)
387
388    # Determine whether to use the fast or accurate calculation
389    accurate = not fast
390
391    if accurate:
392
393        # Equation of time, minutes
394        junk, junk, eot, junk = solar_position( datetime )
395
396    else:
397        # Implements the "alternative equation" from Wikipedia, derived from
398        # https://web.archive.org/web/20120323231813/http://www.green-life-innovators.org/tiki-index.php?page=The%2BLatitude%2Band%2BLongitude%2Bof%2Bthe%2BSun%2Bby%2BDavid%2BWilliams
399        # Results checked against NOAA solar calculator and agree within 10 seconds.
400        # Note: Leap years are not accounted for.
401
402        # Equation of time, accounts for the solar day differing slightly from 24 hr
403        try:
404            doy = datetime.dt.dayofyear
405        except AttributeError:
406            doy = datetime.dayofyear
407        W = 360 / 365.24
408        A = W * (doy+10)
409        B = A + 1.914 * np.sin( W * (doy-2) * pi180 )
410        C = ( A - np.arctan2( np.tan(B*pi180), np.cos(23.44*pi180) ) / pi180 ) / 180
411
412        # Equation of time in minutes of an hour (1440 minutes per day)
413        eot = 720 * ( C - np.round(C) )
414
415    # Equation of time, minutes -> degrees (360 degrees per day)
416    if degrees:
417        eot = eot / 60 * 360 / 24
418
419    return eot

Equation of time for specified date

Accounts for the solar day being slightly different from 24 hours

Parameters
  • datetime (datetime-like or str): date and time. Include time zone or UTC will be assumed
  • degrees (bool (default=False)): If True, then return value in compass degrees If False, then return value in minutes of an hour
  • fast (bool (default=False)): specifies whether to use a faster, but less accurate calculation
Returns
  • eot (float): equation of time on the specified date, degrees or minutes
def solarDeclination(datetime, fast=False):
235def solar_declination( datetime, fast=False ):
236    '''Calculate solar declination (degrees) for specified date
237    
238    Implements Eq. 9.68-9.72 from M.Z. Jacobson, Fundamentals of Atmospheric Modeling
239    
240    Parameters
241    ----------
242    datetime : datetime-like or str
243        date and time. Include time zone or UTC will be assumed
244    fast : bool (default=False)
245        Specifies using a faster but less accurate calculation
246
247    Returns
248    -------
249    dec : float
250        solar declination in degrees at the specified date
251    '''
252    # Convert to pandas Timestamp, if needed
253    datetime = _to_timestamp(datetime)
254
255    # Select the accurate or fast calculation
256    accurate = not fast
257
258    if accurate:
259
260        # Solar declination, degrees
261        dec, junk, junk, junk = solar_position( datetime )
262
263    else:
264        # Number of days since beginning of 2000
265        NJD = datetime - np.datetime64('2000-01-01')
266        try:
267            NJD = NJD.dt.days
268        except AttributeError:
269            NJD = NJD.days
270
271        # Obliquity, degrees
272        ob = 23.439 - 4e-7 * NJD
273
274        # Parameters for ecliptic, degrees
275        gm = 357.528 + 0.9856003 * NJD
276        lm = 280.460 + 0.9856474 * NJD
277
278        # Ecliptic longitude of sun, degrees
279        ec = lm + 1.915 * np.sin( gm * pi180 ) + 0.020 * np.sin( 2 * gm * pi180 )
280
281        #Solar declination, degrees
282        dec = np.arcsin( np.sin( ob * pi180 ) * np.sin( ec * pi180 ) ) / pi180
283
284    return dec

Calculate solar declination (degrees) for specified date

Implements Eq. 9.68-9.72 from M.Z. Jacobson, Fundamentals of Atmospheric Modeling

Parameters
  • datetime (datetime-like or str): date and time. Include time zone or UTC will be assumed
  • fast (bool (default=False)): Specifies using a faster but less accurate calculation
Returns
  • dec (float): solar declination in degrees at the specified date