Module insolf

Expand source code
import numpy as np
from numpy import linalg as LA
from insolation import atmosf


"""
Functions to compute insolation over tilted surfaces or complex terrain.

Javier G. Corripio meteoexploration.com 2023

aspect(dem,dlxy,degrees = False):
    Calculates the aspect or orientation of the slope of every grid cell in a digital elevation model (DEM).

cgrad(dem, dlxy, cArea = False):
    Computes a unit vector normal to every grid cell in a digital elevation model.

daylength(latitude, longitude, jd, tmz):
    Computes duration of day light for a given latitude and Julian Day.

declination(jd):
    Computes declination from Julian Date.

doshade(dem, res, sun_v, num_sweeps=1):
    Computes cast shadows over a DEM
    python implementation from openamundsen package based on the f90 function in R insol
    by Florian Hanzer https://github.com/openamundsen/openamundsen

eqtime(jd):
    Computes the equation of time for a given Julian Date.

hourangle(jd,longitude,timezone):
    Hour angle, internal function for solar position.

hillshading(dem,dlxy,sunv):
    Computes the intensity of illumination over a surface (DEM) according to the position of the sun.

insolation(zenith,jd,height,visibility,RH,tempK,O3,alphag):
    Computes direct and diffuse solar irradiance perpendicular to the beam, for a given zenith angle, Julian Day, altitude and atmospheric conditions.

julian_day(y, m, d, h, min = 0, sec = 0):
    Computes Julian Day  (days np.since January 1, 4713 BCE at noon UTC).

normalvector(slope,aspect):
    Calculates a unit vector normal to a surface defined by slope inclination and slope orientation.

slope(dem,dlxy,degrees = False):
    Calculates the inclination of the slope of every grid cell in a digital elevation model (DEM). Zero is an horizontal surface.

sunpos(sunv):
    Returns a matrix of azimuth and zenith angles of the sun given the unit vectors from the observer to the direction of the sun.

sunr(jd):
    Calculates the Earth radius vector.

sunvector(jd,latitude,longitude,timezone):
    Calculates a unit vector in the direction of the sun from the observer position.

""" 


def aspect(dem,dlxy,degrees = False):
    """
    Calculates the aspect or orientation of the slope of every grid cell in a digital elevation model (DEM)

    Parameters
    ----------
        dem,  ndarray (2D, float), Digital Elevation Model with elevation data.
        dlxy, int, resolution of the DEM, pixel size along x and y coordinates.

    Keywords
    --------
        degrees, boolean, if True return degrees instead of radians.

    Returns
    -------
        aspect, 2D ndarray of floats of the same size as the input dem.
    
    Examples
    --------
        # Calculates the aspect of every grid cell of a DEM in the Pyrennes and plot it
        from insolation import insolf
        import rasterio
        import matplotlib.pyplot as plt
        demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
        dlxy = demP.res[0]
        dem = demP.read(1)
        demaspect = insolf.aspect(dem,dlxy,degrees = True)
        plt.imshow(demaspect, cmap='Greys_r')
        plt.colorbar()
        plt.title("Aspect of every grid cell of a DEM in the Pyrenees")
        plt.show()

    """

    demcgrad = cgrad(dem,dlxy)
    y = demcgrad[1,:,:]
    x = demcgrad[0,:,:]
    aspct = np.arctan2(y,x) + np.pi/2
    aspct[aspct < 0] = aspct[aspct < 0] + 2 * np.pi
    if (degrees):
        aspct = np.degrees(aspct)
    return(aspct)



def cgrad(dem, dlxy, cArea = False):
    """
    Computes a unit vector normal to every grid cell in a digital elevation model.

    Parameters
    ----------
        dem, ndarray (2D, float), Digital Elevation Model with elevation data
        dlxy, int, resolution of the DEM, pixel size along x and y coordinates.
        cArea, boolean, if True returns the surface area of every grid cell instead of the gradient.

    Keywords
    --------
        cArea, boolean, if True returns a 2D matrix with the surface area of every grid cell.

    Returns
    -------
        cellgrunit, ndarray float, a 3D matrix corresponding to the x, y, z coordinates of a unit vector 
        perpendicular to every grid cell. 
        If cArea is True, the result is a 2D matrix with the surface area of every grid cell.

    Examples
    --------
        # Visualize x, y z components of vectors normal to a DEM representing a regular pyramid
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt
        ncols = 100
        nrows = 100
        # you can play with the height to see how the z-component becomes smaller as the height increases and the pyramid becomes steeper
        height = 500  
        nh = 2*height/ncols
        m = np.zeros([ncols,nrows])
        for i in np.arange(ncols):
            for j in np.arange(nrows):
                m[i,j]=height-max(abs(nh*i-height),abs(nh*j-height)) 

        grdm = insolf.cgrad(m,1)
        xcomponent = grdm[0,:,:]
        ycomponent = grdm[1,:,:]
        zcomponent = grdm[2,:,:]
        titletext = ['xcomponent','ycomponent','zcomponent']
        plt.imshow(m, cmap='Greys_r')
        plt.colorbar()
        plt.contour(m)
        plt.title('DEM of a pyramid')
        plt.show()
        print("press 'q' to continue")
        plt.show()
        for p in np.arange(3):
            # plt.imshow(grdm[p,:,:], cmap='Greys_r',vmin=-1, vmax=1)
            plt.imshow(grdm[p,:,:], cmap='Greys_r')
            plt.colorbar()
            plt.contour(m, cmap='Greys_r')
            plt.title(titletext[p])
            print("press 'q' to continue")
            plt.show()

        # Visualize x, y , z components of vector normal to surface for a DEM of the Pyrenees
        # To make the coordinates agree with solar position convention, vector coordinates are positive eastwards, southwards and upward.
        from insolation import insolf
        import rasterio
        import numpy as np
        import matplotlib.pyplot as plt
        # It would be faster if you download the DEM to your local computer
        demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
        dlxy = demP.res[0]
        dem = demP.read(1)
        demgrd = insolf.cgrad(dem,dlxy)
        xcomponent = demgrd[0,:,:]
        ycomponent = demgrd[1,:,:]
        zcomponent = demgrd[2,:,:]
        titletext = ['xcomponent','ycomponent','zcomponent']
        plt.imshow(dem, cmap='Greys_r')
        plt.colorbar()
        plt.contour(dem)
        plt.title('DEM of the Pyrenees')
        print("press 'q' to continue")
        plt.show()
        for p in np.arange(3):
            plt.imshow(demgrd[p,:,:], cmap='Greys_r')
            plt.colorbar()
            plt.title(titletext[p])
            print("press 'q' to contiqnue")
            plt.show()


    """

    dem = dem[::-1,...] # flip dem to conform python x, y order to to insol coordinate system convention
    rows, cols = dem.shape
    cellgr = np.zeros((3, rows, cols))
    mm = dem
    md = mm[:-1, 1:]
    mr = mm[1:, :-1]
    mrd = mm[1:, 1:]
    cellgr[1, :-1, :-1] = -0.5 * dlxy * (mm[:-1, :-1] + md - mr - mrd)  
    cellgr[0, :-1, :-1] = 0.5 * dlxy * (mm[:-1, :-1] - md + mr - mrd)
    cellgr[2, :-1, :-1] = dlxy * dlxy 
    cellgr[:,:,-1]=cellgr[:,:,-2]  
    cellgr[:,-1,:]=cellgr[:,-2,:]
    cellgr = np.flip(cellgr, axis=1) # flip results back
    cellArea = LA.norm(cellgr,axis=0)
    cellgrunit = np.divide(cellgr,cellArea)
    if cArea:
        return(cellArea)
    else:
        return(cellgrunit)




def daylength(latitude, longitude, jd, tmz):
    """ 
    Computes duration of day light for a given latitude and Julian Day.

    Parameters
    ----------
        latitude,  float, latitude in degrees and decimal fraction.
        longitude, float, longitude in degrees and decimal fraction.
        jd, float, Julian Day, see help(insolf.julian_day).
        tmz, float, Timezone, west of Greenwich is negative.

    Returns
    -------
        sunrise, sunset, duration of day light in hours, ndarray of floats of 
        shape (3, n) where n is length of input array.

    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating 
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1), 1–23.  
        https://doi.org/10.1080/713811744

    Notes
    -----
        It considers sunrise and sunset as the time when the center of the sun passes above or below the horizon. 
        It does not take into account limb, summer time, atmospheric refraction or twilight.
        To add, refraction, dip due to observer altitude and more.

    Examples
    --------
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt

        # Day length at Greenwich Observatory on the 21st of March
        insolf.daylength(51.4767,0.0,insolf.julian_day(2023,3,21,12),0)
        # array([ 6.10004075, 18.14060504, 12.04056429])

        # Daylength at Reykjavík on the 21st of June
        insolf.julian_day(2023,6,21,12)
        2460117.0
        insolf.daylength(64.12,-21.87, 2460117.0, 0)
        # array([ 3.26596303, 23.70863056, 20.44266753])

        # Daylength for the whole 2023 year at Reykjavík 
        jd2023noon = np.arange(insolf.julian_day(2023,1,1,12),insolf.julian_day(2023,12,31,12)+1)
        doy = jd2023noon - jd2023noon[0] + 1
        day_length = insolf.daylength(64.12, -21.87, jd2023noon, 0)
        plt.plot(doy,day_length[2,:])
        plt.xlabel('Day of the year')
        plt.ylabel('Day length [h]')
        plt.ylim(0,24)
        plt.title('Daylength over the year at Reykjavík')
        plt.show()

    """

    EqTime = eqtime(jd)
    delta = declination(jd)
    tanlatdel = -np.tan(np.radians(latitude)) * np.tan(np.radians(delta))
    tanlatdel = np.where(tanlatdel > 1, 1, tanlatdel)
    tanlatdel = np.where(tanlatdel < -1, -1, tanlatdel)
    omega = np.arccos(tanlatdel)
    daylen = (2*omega)/(2*np.pi/24.)
    stndmeridian = tmz*15.
    deltaLatTime = longitude-stndmeridian
    deltaLatTime = deltaLatTime * 24/360. 
    sunrise = 12*(1-omega/np.pi)-deltaLatTime-EqTime/60. 
    sunset = 12*(1+omega/np.pi)-deltaLatTime-EqTime/60.
    sunrise = np.where(omega == 0, np.nan, sunrise)
    sunset = np.where(omega == 0, np.nan, sunset)
    return(np.array([sunrise,sunset,daylen]))



def declination(jd):
    """
    Computes declination from Julian Date.

    Parameters
    ----------
        jd, float, Julian date
   
    Returns
    -------
        float, declination in degrees and decimal fraction

    References
    ----------
        https://gml.noaa.gov/grad/solcalc/calcdetails.html

        Meeus, J. 1999. Astronomical Algorithms. Willmann-Bell, Richmond, Virginia, USA.

        Reda, I. and Andreas, A. 2003. Solar Position Algorithm for Solar Radiation Applications. 55 pp.; 
        NREL Report No. TP-560-34302, Revised January 2008. https://www.nrel.gov/docs/fy08osti/34302.pdf

    Examples
    --------
        # Current time declination
        from insolation import insolf
        import datetime
        tt = datetime.datetime.now()
        jd = insolf.julian_day(tt.year,tt.month,tt.day,tt.hour+tt.minute/60.+tt.second/3600.)
        decl = insolf.declination(jd)

    """

    T = (jd - 2451545)/36525
    epsilon = (23 + 26/60 + 21.448/3600) - (46.815/3600) * T - (0.00059/3600) * T**2 + (0.001813/3600) * T**3
    L0 = 280.46645 + 36000.76983 * T + 0.0003032 * T**2
    M = 357.5291 + 35999.0503 * T - 0.0001559 * T**2 - 4.8e-07 * T**3
    e = 0.016708617 - 4.2037e-05 * T - 1.236e-07 * T**2
    C = (1.9146 - 0.004817 * T - 1.4e-05 * T**2) * np.sin(np.radians(M)) + \
    (0.019993 - 0.000101 * T) * np.sin(2 * np.radians(M)) + 0.00029 * np.sin(3 * np.radians(M))
    Theta = L0 + C
    v = M + C
    Omega = 125.04452 - 1934.136261 * T + 0.0020708 * T**2 + (T**3)/450000
    lambdad = Theta - 0.00569 - 0.00478 * np.sin(np.radians(Omega))
    delta = np.arcsin(np.sin(np.radians(epsilon)) * np.sin(np.radians(lambdad)))
    return(np.degrees(delta))



def doshade(dem, res, sun_v, num_sweeps=1):
    """ 
    Computes cast shadows over a DEM
    unorthodox way to call the function to avoid unnecessary imports (numba) if not used.
    python implementation from openamundsen package based on the f90 function in R insol
    by Florian Hanzer https://github.com/openamundsen/openamundsen
    

    Parameters
    ----------
        dem, ndrray, Digital elevation Model 2D array 
        res, int, resolution of the DEM (pixel size)
        sun_v, float array, unit vector in the direction of the sun
        num_sweeps

    Returns
    -------
        2D array of the same shade as dem with 1 for illuminated pixels and 0 for pixels in the shade

    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1),
        1–23. https://doi.org/10.1080/713811744

    Examples
    --------
        # Calculate and plot the cast shadows of a pyramid with the sun at the northwest and 15 degrees elevation
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt
        # define the sun vector: northwest at 15 degrees elevation
        sv = insolf.normalvector(75,315)
        # define broadly a regular pyramid
        ncols = 100
        nrows = 100
        height = 50  
        nh = 2*height/ncols
        m = np.zeros([ncols,nrows])
        for i in np.arange(ncols):
            for j in np.arange(nrows):
                m[i,j]=height-max(abs(nh*i-height),abs(nh*j-height)) 

        # place it on a larger flat area
        mm = np.zeros([500,500])
        mm[200:300,200:300] = m
        # calculate and plot shadows 
        sh = insolf.doshade(mm, 1, sv)
        plt.imshow(sh, cmap='Greys_r')
        plt.colorbar()
        plt.contour(mm)
        plt.title('Shadow of a Pyramid with the sun at 315 azimuth, 15 elevation')
        print("press 'q' to continue")
        plt.show()

    """

    from insolation import shadows
    return(shadows.shadows(dem, res, sun_v, num_sweeps))




def eqtime(jd):
    """
    Computes the equation of time for a given Julian Date.

    Parameters
    ----------
        jd, float, Julian date.

    Returns
    -------
        float, equation of time in minutes.

    References
    ----------
        https://gml.noaa.gov/grad/solcalc/calcdetails.html

        Meeus, J. 1999. Astronomical Algorithms. Willmann-Bell, Richmond, Virginia, USA.

        Reda, I. and Andreas, A. 2003. Solar Position Algorithm for Solar Radiation Applications. 55 pp.; 
            NREL Report No. TP-560-34302, Revised January 2008. https://www.nrel.gov/docs/fy08osti/34302.pdf
    
    Examples
    --------
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt

        # plot the equation of time for 2023 at daily intervals
        jd2023noon = np.arange(insolf.julian_day(2023,1,1,12),insolf.julian_day(2023,12,31,12)+1)
        plt.plot(insolf.eqtime(jd2023noon))
        plt.axhline(y=0.0, linestyle='-', color='k', linewidth=0.5)
        plt.xlabel('Day of the year')
        plt.ylabel('Equation of time [minutes]')
        plt.show()

        # Analema
        plt.plot(insolf.eqtime(jd2023noon), insolf.declination(jd2023noon))
        plt.show()

        # Analema viewed from Greenwich Observatory
        latGwch = 51.4791
        x = 180 + insolf.eqtime(jd2023noon) * 15/60
        y = 90 - latGwch + insolf.declination(jd2023noon)
        # plt.plot(x,y)
        plt.scatter(x,y,s=40, facecolors='none', edgecolors='k')
        plt.xlabel("Azimuth")
        plt.ylabel("Elevation")
        plt.ylim(0,90)
        plt.title('Equation of time for 2023 at daily intervals')
        plt.show()

        # Add the solstices and equinoxes (nearest day, see Meeus ch. 26 for more precision)
        decl = insolf.declination(jd2023noon)
        wintersolstice = np.argmin(decl)
        summersolstice = np.argmax(decl)
        # equinoxes when decl is closest to zero, find spring of=n first half of the year and then autumn (for N hemisphere)
        springequinox = np.argmin(np.abs(decl[0:180]))
        autumnequinox = 180 + np.argmin(np.abs(decl[180:365]))
        nodeseqx = ([springequinox,summersolstice,autumnequinox,wintersolstice])
        plt.scatter(x,y,s=40, facecolors='none', edgecolors='k')
        plt.scatter(x[nodeseqx],y[nodeseqx], s=80, facecolors='c', edgecolors='k')
        plt.xlabel("Azimuth")
        plt.ylabel("Elevation")
        plt.ylim(0,90)
        plt.title('Analema, equinoxes and solstices from Greenwich')
        plt.show()

    """

    jdc = (jd - 2451545.0)/36525.0
    sec = 21.448 - jdc*(46.8150 + jdc*(0.00059 - jdc*(0.001813)))
    e0 = 23.0 + (26.0 + (sec/60.0))/60.0  
    ecc = 0.016708634 - jdc * (0.000042037 + 0.0000001267 * jdc)
    oblcorr = e0 + 0.00256 * np.cos(np.radians(125.04 - 1934.136 * jdc))   
    y = (np.tan(np.radians(oblcorr)/2))**2
    l0 = 280.46646 + jdc * (36000.76983 + jdc*(0.0003032))
    l0 = (l0-360*(l0//360))%360
    rl0 = np.radians(l0)
    gmas = 357.52911 + jdc * (35999.05029 - 0.0001537 * jdc)
    gmas = np.radians(gmas)
    EqTime = y*np.sin(2*rl0)-2.0*ecc*np.sin(gmas)+4.0*ecc*y*np.sin(gmas)*np.cos(2*rl0)-0.5*y**2*np.sin(4*rl0)-1.25*ecc**2*np.sin(2*gmas)
    return(np.degrees(EqTime)*4)


def hourangle(jd,longitude,timezone):
    """
    Hour angle, internal function for solar position.

    Parameters
    ----------
          jd, float, Julian date
          longitude, float, longitude in decimal degrees
          timezone, float, timezone in hours, west of Greenwich is negative

    """

    hour = ((jd-np.floor(jd))*24+12) % 24
    eqtimeval = eqtime(jd)
    stndmeridian = timezone*15                  
    deltalontime = longitude-stndmeridian       
    deltalontime = deltalontime * 24.0/360.0    
    omegar = np.pi*( ( (hour + deltalontime + eqtimeval/60)/12.0 ) - 1.0) 
    return(omegar)



def hillshading(dem,dlxy,sunv):
    """
    Computes the intensity of illumination over a surface (DEM) according to the position of the sun.

    Parameters
    ----------
        dem, ndrray, Digital elevation Model 2D array 
        dlxy, int, resolution of the DEM (pixel size)
        sunv, float array, unit vector in the direction of the sun

    Returns
    -------
        hsh, ndarray of floats of the same dimensions as dem Illumination intensity ranging from 0 to 1

    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1),
        1–23. https://doi.org/10.1080/713811744        

    Examples
    --------
        from insolation import insolf
        import rasterio
        import numpy as np
        import matplotlib.pyplot as plt
        demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
        dlxy = demP.res[0]
        dem = demP.read(1)
        # Sun at azimuth 315, 35 degrees elevation
        sv = insolf.normalvector(55,315)
        grd = insolf.cgrad(dem,dlxy)
        hsh = grd[0,:,:]*sv[0] + grd[1,:,:]*sv[1] + grd[2,:,:]*sv[2]
        # remove negative incidence angles (self shading) 
        hsh = (hsh+np.abs(hsh))/2
        plt.imshow(hsh, cmap='Greys_r',vmin=0, vmax=1)
        plt.colorbar()
        plt.show()
        # Add cast shadows
        sh = insolf.doshade(dem, dlxy, sv)
        plt.imshow(hsh*sh, cmap='Greys_r',vmin=0, vmax=1)
        plt.colorbar()
        plt.show() 

    """

    demcgrad = cgrad(dem,dlxy)
    hsh = demcgrad[0,:,:]*sunv[0] + demcgrad[1,:,:]*sunv[1] + demcgrad[2,:,:]*sunv[2]
    hsh = (hsh + abs(hsh))/2.0      # set to zero self-shading pixels
    return(hsh) 



def insolation(zenith,jd,height,visibility,RH,tempK,O3,alphag):
    """
    Computes direct and diffuse solar irradiance perpendicular to the beam, for a given 
    zenith angle, Julian Day, altitude and atmospheric conditions.

    Parameters
    ----------
          zenith       Zenith angle in degrees
          jd           Julian Day
          height       Altitude above sea level
          visibility   Visibility [km]
          RH           Relative humidity [%]
          tempK        Air temperature [K]
          O3           Ozone thickness [m]
          alphag       Albedo of the surrounding terrain [0 to 1]

    Returns
    -------
        array-like, first element is direct radiation, second element is diffuse radiation.
                    If any input is an array First row is direct radiation and second row diffuse radiation

    References
    ----------
        Bird, R. E. and Hulstrom, R. L. (1981a) Review, evaluation and improvements of direct irradiance models, 
        Trans. ASME J. Solar Energy Eng. 103, 182-192.

        Bird, R. E. and Hulstrom, R. L. (1981b) A simplified clear sky model for direct 
        and diffuse insolation on horizontal surfaces, 
        Technical Report SERI/TR-642-761, Solar Research Institute, Golden, Colorado.
        
        Iqbal, M. (1983) An Introduction to Solar Radiation, Academic Press, Toronto.

    Examples
    --------
        from insolation import insolf
        import datetime
        import rasterio
        import numpy as np
        import matplotlib.pyplot as plt
        from tabulate import tabulate

        insolf.insolation(30,2456007,3200,28,60,278.15,0.02,0.2)
        # Daily insolation in Spring
        # Find nearest hour to sunrise and sunset
        TIMESTAMP = "2023-03-21 12:00:00"
        tt = datetime.datetime.strptime(TIMESTAMP,'%Y-%m-%d %H:%M:%S')
        jd = insolf.julian_day(tt.year,tt.month,tt.day,tt.hour+tt.minute/60.+tt.second/3600.)
        latitude = 42.675
        longitude = 0.033
        timezone = 1
        sr,ss,dl = insolf.daylength(42.675,0.033, jd, timezone)
        sr = np.ceil(sr)
        ss = np.floor(ss)
        sr2ss = np.arange(sr,ss)
        jdrng = insolf.julian_day(tt.year,tt.month,tt.day,sr2ss)
        sunv = insolf.sunvector(jdrng,latitude,longitude,timezone)
        azimuth,zenith = insolf.sunpos(sunv)
        height = 2000
        visibility = 60
        RH = 55
        tempK = 285.0
        O3 = 0.02
        alphag = 0.2
        Idir,Idiff = insolf.insolation(zenith,jdrng,height,visibility,RH,tempK,O3,alphag)
        Hnormal = np.array([0,0,1])
        IdirHoriz = Idir * np.dot(np.transpose(sunv),Hnormal)
        print("Hourly direct normal, direct horizontal and diffuse insolation at 43.7N, 0E, Spring")
        print(tabulate({"Hour": sr2ss,"I_direct_normal": Idir, "I_direct_horizontal": IdirHoriz, "I_diff": Idiff}, headers="keys"))
          Hour    I_direct_normal    I_direct_horizontal    I_diff
        ------  -----------------  ---------------------  --------
             8            487.849                83.0165   58.1299
             9            723.68                253.228    87.8123
            10            829.915               419.907   101.989
            11            884.963               555.478   110.169
            12            912.945               645.265   114.711
            13            922.682               680.859   116.373
            14            916.784               659.104   115.362
            15            893.716               581.95    111.559
            16            846.622               456.52    104.389
            17            756.851               295.762    92.0956
            18            567.277               122.506    68.1983
        # plot the results for the whole day
        dayh = np.arange(0,24,5/60)     # every 5 minutes
        jdrng = insolf.julian_day(tt.year,tt.month,tt.day,dayh)
        sunv = insolf.sunvector(jdrng,latitude,longitude,timezone)
        azimuth,zenith = insolf.sunpos(sunv)
        Idir,Idiff = insolf.insolation(zenith,jdrng,height,visibility,RH,tempK,O3,alphag)
        IdirHoriz = Idir * np.dot(np.transpose(sunv),Hnormal)
        plt.figure(figsize=(10,6))
        plt.plot(dayh,Idir,'r', label='Direct Normal')
        plt.plot(dayh,IdirHoriz,'y', label='Direct Horizontal')
        plt.plot(dayh,Idiff,'c', label='Diffuse')
        plt.legend(loc="upper left")
        plt.grid(visible=True,linestyle="-.")
        plt.xlabel('Time')
        plt.ylabel('Insolation Wm$^{-2}$')
        plt.title("Direct normal, direct horizontal and diffuse insolation at 43.7N, 0E, Spring")
        plt.ylim(0,1380)
        plt.show()

        # Alternatively, loop over every hour
        # Compute insolation on a DEM of the pyrenees:
        demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
        # demP = rasterio.open('dempyrenees.tif')
        dlxy = demP.res[0]
        dem = demP.read(1)
        I_tot = np.zeros(dem.shape)
        for i in np.arange(len(dayh)):
            sunv = insolf.sunvector(jdrng[i],latitude,longitude,timezone)
            azimuth,zenith = insolf.sunpos(sunv)
            hsh = insolf.hillshading(dem,dlxy,sunv)
            Idir,Idiff = insolf.insolation(zenith,jdrng[i],height,visibility,RH,tempK,O3,alphag)
            # Global insolation in MJ/m^2
            W2MJ = 3600e-6 # Watts/m^2 to MJ/m^2 hourly computation
            I_tot = I_tot + W2MJ*Idir*hsh + W2MJ*Idiff   # The diffuse fraction should consider the skyview factor for a propper calculation... to be implemented

        plt.imshow(I_tot, cmap='plasma')
        plt.colorbar()
        plt.title("Dayly insolation over the Pyrenees in Spring $MJ^{-2}$")
        plt.show()

    """

    Isc = 1361.0            # solar constant (Wm**(-2)) (1)
    zenith = np.where(zenith > 90, 90, zenith)
    theta = np.radians(zenith)
    ssctalb = 0.9  # single scattering albedo (aerosols)(Iqbal, 1983)
    Fc = 0.84      # ratio of forward to total energy scattered (Iqbal, 1983)
    Pz = atmosf.z2p(height)
    Mr = 1.0/(np.cos(theta)+0.15*((93.885-zenith)**(-1.253)))
    Ma = Mr*Pz/1013.25
    #** Use Lowe(1977) Lowe's polynomials for vapor pressure
    wvap_s =  atmosf.wvapsat(tempK)
    #Wprec = 0.493*(RH/100.0)*wvap_s/tempK   #precipitable water in cm Leckner (1978)
    Wprec = 46.5*(RH/100.0)*wvap_s/tempK  #Prata 1996
    rho2 = (1/sunr(jd))**2
    TauR = np.exp((-.09030*(Ma**0.84) )*(1.0+Ma-(Ma**1.01)) )
    TauO = 1.0-( ( 0.1611*(O3*Mr)*(1.0+139.48*(O3*Mr))**(-0.3035) ) - 0.002715*(O3*Mr)*( 1.0+0.044*(O3*Mr)+0.0003*(O3*Mr)**2 )**(-1))
    TauG = np.exp(-0.0127*(Ma**0.26))
    TauW = 1.0-2.4959*(Wprec*Mr)*( (1.0+79.034*(Wprec*Mr))**0.6828 + 6.385*(Wprec*Mr) )**(-1)
    TauA = ( 0.97-1.265*(visibility**(-0.66)) )**(Ma**0.9)   #Machler, 1983
    TauTotal = TauR*TauO*TauG*TauW*TauA   
    In = 0.9751*rho2*Isc*TauTotal
    tauaa = 1.0-(1.0-ssctalb)*(1.0-Ma+Ma**1.06)*(1.0-TauA)
    # tauaa = 1 - (1 - ssctalb) * (1 - Ma + Ma^1.06) * (1 - TauA)
    Idr = 0.79*rho2*Isc*np.cos(theta)*TauO*TauG*TauW*tauaa*0.5*(1.0-TauR)/(1.0-Ma+Ma**(1.02))
    tauas = (TauA)/tauaa
    Ida = 0.79*rho2*Isc*np.cos(theta)*TauO*TauG*TauW*tauaa*Fc*(1.0-tauas)/(1.0-Ma+Ma**1.02)
    alpha_atmos = 0.0685+(1.0-Fc)*(1.0-tauas)
    Idm = (In*np.cos(theta)+Idr+Ida)*alphag*alpha_atmos/(1.0-alphag*alpha_atmos)
    Id = Idr+Ida+Idm
    In = np.where(zenith >= 90, 0, In)   # Set In=0 if after sunset
    Id = np.where(zenith >= 90, 0, Id)
    return(np.array([In,Id]))





def julian_day(Y, m, d, H, min = 0, sec = 0):
   """
   Computes Julian Day (days since January 1, 4713 BCE at noon UTC)
   from 1990 edition of the U.S. Naval Observatory's Almanac for Computers Valid AD 1801 - 2099



   Parameters
   ----------
        Y (int)        Year, format yyyy
        m (short int)  Month number.
        d (short int)  Day-of-month.
        H (float)      Hour-of-day and decimal fraction.

   Returns
   -------
        float, Julian Day (days since January 1, 4713 BCE at noon UTC)

   References
   ----------

        https://adsabs.harvard.edu/full/1983IAPPP..13...16F
        https://aa.usno.navy.mil/software/novaspy_intro
        https://aa.usno.navy.mil/faq/JD_formula
        check results 
        https://aa.usno.navy.mil/data/JulianDate

   Examples
   --------
        from insolation import insolf
        import datetime
        TIMESTAMP = "2023-09-23 06:00:00"
        tt = datetime.datetime.strptime(TIMESTAMP,'%Y-%m-%d %H:%M:%S')
        print(insolf.julian_day(tt.year,tt.month,tt.day,tt.hour+tt.minute/60.+tt.second/3600.))
        2460210.75
        insolf.julian_day(2023,21,3,12)  # Check the month/day order, it will accept months > 12 without warning
        2460556.0
        insolf.julian_day(2024,9,3,12)
        2460557.0

   """

   H = H + min/60.0 + sec/3600.0
   tjd = 367*Y - (7*(Y+(m+9)//12))//4 + (275*m)//9 + d + 1721013.5 + H/24.0 
   return(tjd)



def normalvector(slope,aspect):
    """
    Calculates a unit vector normal to a surface defined by slope inclination and slope orientation.

    Parameters
    ----------
        slope (float)       slope inclination in degrees
        aspect (float)      slope orientation in degrees

    Returns
    -------
        float vector
    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1),
        1–23. https://doi.org/10.1080/713811744 

    Examples
    --------
        from insolation import insolf
        import numpy as np

        # horizontal surface
        insolf.normalvector(0,0)
        # array([ 0., -0.,  1.])

        # surface 45 degrees south
        insolf.normalvector(45,180)
        # array([8.65956056e-17, 7.07106781e-01, 7.07106781e-01])
        
    """

    sloper = np.radians(slope) 
    aspectr = np.radians(aspect)
    nvx = np.sin(aspectr)*np.sin(sloper)
    nvy = -np.cos(aspectr)*np.sin(sloper)
    nvz = np.cos(sloper)
    return(np.array([nvx,nvy,nvz]))
   


def slope(dem,dlxy,degrees = False):
    """
    Calculates the inclination of the slope of every grid cell in a digital elevation model (DEM). Zero is an horizontal surface.

    Parameters
    ----------
        dem,  ndarray (2D, float), Digital Elevation Model with elevation data.
        dlxy, int, resolution of the DEM, pixel size along x and y coordinates.

    Keywords
    --------
        degrees, boolean, if True return degrees instead of radians.

    Returns
    -------
        slp, 2D ndarray of floats of the same size as the input dem.
   
    Examples
    --------
        # Calculates the slope of every grid cell of a DEM in the Pyrennes and plot it
        from insolation import insolf
        import rasterio
        import matplotlib.pyplot as plt
        demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
        dlxy = demP.res[0]
        dem = demP.read(1)
        demslope = insolf.slope(dem,dlxy,degrees = True)
        plt.imshow(demslope, cmap='Greys')
        plt.colorbar()
        plt.show()

    """

    demcgrad = cgrad(dem,dlxy)
    z = demcgrad[2,:,:]
    slp = np.arccos(z)
    if (degrees):
        slp = np.degrees(slp)
    slp = slp
    return(slp)


def sunpos(sunv, degrees=True):
    """
    Returns a matrix of azimuth and zenith angles of the sun given the unit vectors from the observer to the direction of the sun.

    Parameters
    ----------
        sunv (float or array of floats)    coordinates x, y, z of the unit vector in the direction of the sun

    Returns
    -------
        array of azimuth and zenith angles

    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1),
        1–23. https://doi.org/10.1080/713811744 

    Examples
    --------
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt

        # Julian Day hourly intervals at spring equinox
        jdrng = insolf.julian_day(2013,3,21,np.arange(7, 20))

        # sun path over the day at sprint equinox in the Pyrenees
        latitude = 42.675
        longitude = 0.033
        tmz = 1
        azimuth, zenith = insolf.sunpos(insolf.sunvector(jdrng,latitude,longitude,tmz))
        elevation = 90-zenith
        plt.scatter(azimuth,elevation,s=240, facecolors='y', edgecolors='y')
        plt.show()

    """

    azimuth = np.pi - np.arctan2(sunv[0],sunv[1] )
    zenith = np.arccos(sunv[2])
    if degrees:
        azimuth = np.degrees(azimuth)
        zenith = np.degrees(zenith)

    return(np.array([azimuth,zenith]))


def sunr(jd):
    """
    Calculates the Earth radius vector.

    Parameters
    ----------
        jd   Julian Day.

    Returns
    -------
        float Earth Radius Vector in Astronomical Units (AU).


    References
    ----------
        https://gml.noaa.gov/grad/solcalc/calcdetails.html

        Meeus, J. 1999. Astronomical Algorithms. Willmann-Bell, Richmond, Virginia, USA.

        Reda, I. and Andreas, A. 2003. Solar Position Algorithm for Solar Radiation Applications. 55 pp.; 
        NREL Report No. TP-560-34302, Revised January 2008. https://www.nrel.gov/docs/fy08osti/34302.pdf 

    Examples
    --------
        # Plot the variation of the earth radius vector over two years
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt
        ndays = np.arange(730)
        jd_nexty = insolf.julian_day(2013,1,11,12) + ndays
        sun_r = insolf.sunr(jd_nexty)
        plt.plot(ndays,sun_r)
        plt.axhline(y=1.0, linestyle='-', color='k', linewidth=0.5)
        plt.show()

    """

    jdc=(jd - 2451545.0)/36525.0
    ecc=0.016708634-jdc*(0.000042037+0.0000001267*jdc)
    gmas=357.52911+jdc*(35999.05029-0.0001537*jdc)
    gmasr=np.radians(gmas)
    seqc=np.sin(gmasr)*(1.914602-jdc*(0.004817+0.000014*jdc))+np.sin(2*gmas)*(0.019993-0.000101*jdc)+ np.sin(3*gmasr)*0.000289
    sta=gmas+seqc
    sunrv=(1.000001018 * (1 - ecc**2)) / (1 + ecc * np.cos(np.radians(sta)))
    return(sunrv)



def sunvector(jd,latitude,longitude,timezone):
    """
    Calculates a unit vector in the direction of the sun from the observer position.

    Parameters
    ----------
        jd (float)          Julian date
        latitude (float)    Latitude of observer in degrees and decimal fraction
        longitude (float)   Longitude of observer in degrees and decimal fraction
        timezone (float)    Timezone in hours, west of Greenwich is negative

    Returns
    -------
        float vector or array of vectors.

    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1),
        1–23. https://doi.org/10.1080/713811744 

    Examples
    --------
        from insolation import insolf
        from mpl_toolkits import mplot3d
        import numpy as np
        import matplotlib.pyplot as plt
        # Current solar vector at Greenwich Observatory on the 21st of June, 2023 at midday
        latitude = 51.4778
        longitude = -0.0017
        tmz = 0
        jd = insolf.julian_day(2023,6,21,12)
        insolf.sunvector(jd,latitude,longitude,0)
        # Path of the sun over Greenwich in summer, z axis in degrees of elevation
        jdrng = insolf.julian_day(2023,6,21,np.arange(4,20.5,0.5))
        sv = insolf.sunvector(jdrng,latitude,longitude,0)
        fig = plt.figure()
        ax = plt.axes(projection ='3d')
        x = sv[0,:]
        y = sv[1,:]
        z = 90 - np.degrees(np.arccos(sv[2,:]))
        ax.plot3D(x, y, z, 'orange')
        ax.set_title('Sun position from Greenwich on the 21st of June')
        plt.show()

    """

    omegar = hourangle(jd,longitude,timezone)
    deltar = np.radians(declination(jd))
    lambdar = np.radians(latitude) 
    svx = -np.sin(omegar)*np.cos(deltar)
    svy = np.sin(lambdar)*np.cos(omegar)*np.cos(deltar)-np.cos(lambdar)*np.sin(deltar)
    svz = np.cos(lambdar)*np.cos(omegar)*np.cos(deltar)+np.sin(lambdar)*np.sin(deltar) 
    return(np.array([svx,svy,svz]))

Functions

def aspect(dem, dlxy, degrees=False)

Calculates the aspect or orientation of the slope of every grid cell in a digital elevation model (DEM)

Parameters

dem,  ndarray (2D, float), Digital Elevation Model with elevation data.
dlxy, int, resolution of the DEM, pixel size along x and y coordinates.

Keywords

degrees, boolean, if True return degrees instead of radians.

Returns

aspect, 2D ndarray of floats of the same size as the input dem.

Examples

# Calculates the aspect of every grid cell of a DEM in the Pyrennes and plot it
from insolation import insolf
import rasterio
import matplotlib.pyplot as plt
demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
dlxy = demP.res[0]
dem = demP.read(1)
demaspect = insolf.aspect(dem,dlxy,degrees = True)
plt.imshow(demaspect, cmap='Greys_r')
plt.colorbar()
plt.title("Aspect of every grid cell of a DEM in the Pyrenees")
plt.show()
Expand source code
def aspect(dem,dlxy,degrees = False):
    """
    Calculates the aspect or orientation of the slope of every grid cell in a digital elevation model (DEM)

    Parameters
    ----------
        dem,  ndarray (2D, float), Digital Elevation Model with elevation data.
        dlxy, int, resolution of the DEM, pixel size along x and y coordinates.

    Keywords
    --------
        degrees, boolean, if True return degrees instead of radians.

    Returns
    -------
        aspect, 2D ndarray of floats of the same size as the input dem.
    
    Examples
    --------
        # Calculates the aspect of every grid cell of a DEM in the Pyrennes and plot it
        from insolation import insolf
        import rasterio
        import matplotlib.pyplot as plt
        demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
        dlxy = demP.res[0]
        dem = demP.read(1)
        demaspect = insolf.aspect(dem,dlxy,degrees = True)
        plt.imshow(demaspect, cmap='Greys_r')
        plt.colorbar()
        plt.title("Aspect of every grid cell of a DEM in the Pyrenees")
        plt.show()

    """

    demcgrad = cgrad(dem,dlxy)
    y = demcgrad[1,:,:]
    x = demcgrad[0,:,:]
    aspct = np.arctan2(y,x) + np.pi/2
    aspct[aspct < 0] = aspct[aspct < 0] + 2 * np.pi
    if (degrees):
        aspct = np.degrees(aspct)
    return(aspct)
def cgrad(dem, dlxy, cArea=False)

Computes a unit vector normal to every grid cell in a digital elevation model.

Parameters

dem, ndarray (2D, float), Digital Elevation Model with elevation data
dlxy, int, resolution of the DEM, pixel size along x and y coordinates.
cArea, boolean, if True returns the surface area of every grid cell instead of the gradient.

Keywords

cArea, boolean, if True returns a 2D matrix with the surface area of every grid cell.

Returns

cellgrunit, ndarray float, a 3D matrix corresponding to the x, y, z coordinates of a unit vector 
perpendicular to every grid cell. 
If cArea is True, the result is a 2D matrix with the surface area of every grid cell.

Examples

# Visualize x, y z components of vectors normal to a DEM representing a regular pyramid
from insolation import insolf
import numpy as np
import matplotlib.pyplot as plt
ncols = 100
nrows = 100
# you can play with the height to see how the z-component becomes smaller as the height increases and the pyramid becomes steeper
height = 500  
nh = 2*height/ncols
m = np.zeros([ncols,nrows])
for i in np.arange(ncols):
    for j in np.arange(nrows):
        m[i,j]=height-max(abs(nh*i-height),abs(nh*j-height))

grdm = insolf.cgrad(m,1)
xcomponent = grdm[0,:,:]
ycomponent = grdm[1,:,:]
zcomponent = grdm[2,:,:]
titletext = ['xcomponent','ycomponent','zcomponent']
plt.imshow(m, cmap='Greys_r')
plt.colorbar()
plt.contour(m)
plt.title('DEM of a pyramid')
plt.show()
print("press 'q' to continue")
plt.show()
for p in np.arange(3):
    # plt.imshow(grdm[p,:,:], cmap='Greys_r',vmin=-1, vmax=1)
    plt.imshow(grdm[p,:,:], cmap='Greys_r')
    plt.colorbar()
    plt.contour(m, cmap='Greys_r')
    plt.title(titletext[p])
    print("press 'q' to continue")
    plt.show()

# Visualize x, y , z components of vector normal to surface for a DEM of the Pyrenees
# To make the coordinates agree with solar position convention, vector coordinates are positive eastwards, southwards and upward.
from insolation import insolf
import rasterio
import numpy as np
import matplotlib.pyplot as plt
# It would be faster if you download the DEM to your local computer
demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
dlxy = demP.res[0]
dem = demP.read(1)
demgrd = insolf.cgrad(dem,dlxy)
xcomponent = demgrd[0,:,:]
ycomponent = demgrd[1,:,:]
zcomponent = demgrd[2,:,:]
titletext = ['xcomponent','ycomponent','zcomponent']
plt.imshow(dem, cmap='Greys_r')
plt.colorbar()
plt.contour(dem)
plt.title('DEM of the Pyrenees')
print("press 'q' to continue")
plt.show()
for p in np.arange(3):
    plt.imshow(demgrd[p,:,:], cmap='Greys_r')
    plt.colorbar()
    plt.title(titletext[p])
    print("press 'q' to contiqnue")
    plt.show()
Expand source code
def cgrad(dem, dlxy, cArea = False):
    """
    Computes a unit vector normal to every grid cell in a digital elevation model.

    Parameters
    ----------
        dem, ndarray (2D, float), Digital Elevation Model with elevation data
        dlxy, int, resolution of the DEM, pixel size along x and y coordinates.
        cArea, boolean, if True returns the surface area of every grid cell instead of the gradient.

    Keywords
    --------
        cArea, boolean, if True returns a 2D matrix with the surface area of every grid cell.

    Returns
    -------
        cellgrunit, ndarray float, a 3D matrix corresponding to the x, y, z coordinates of a unit vector 
        perpendicular to every grid cell. 
        If cArea is True, the result is a 2D matrix with the surface area of every grid cell.

    Examples
    --------
        # Visualize x, y z components of vectors normal to a DEM representing a regular pyramid
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt
        ncols = 100
        nrows = 100
        # you can play with the height to see how the z-component becomes smaller as the height increases and the pyramid becomes steeper
        height = 500  
        nh = 2*height/ncols
        m = np.zeros([ncols,nrows])
        for i in np.arange(ncols):
            for j in np.arange(nrows):
                m[i,j]=height-max(abs(nh*i-height),abs(nh*j-height)) 

        grdm = insolf.cgrad(m,1)
        xcomponent = grdm[0,:,:]
        ycomponent = grdm[1,:,:]
        zcomponent = grdm[2,:,:]
        titletext = ['xcomponent','ycomponent','zcomponent']
        plt.imshow(m, cmap='Greys_r')
        plt.colorbar()
        plt.contour(m)
        plt.title('DEM of a pyramid')
        plt.show()
        print("press 'q' to continue")
        plt.show()
        for p in np.arange(3):
            # plt.imshow(grdm[p,:,:], cmap='Greys_r',vmin=-1, vmax=1)
            plt.imshow(grdm[p,:,:], cmap='Greys_r')
            plt.colorbar()
            plt.contour(m, cmap='Greys_r')
            plt.title(titletext[p])
            print("press 'q' to continue")
            plt.show()

        # Visualize x, y , z components of vector normal to surface for a DEM of the Pyrenees
        # To make the coordinates agree with solar position convention, vector coordinates are positive eastwards, southwards and upward.
        from insolation import insolf
        import rasterio
        import numpy as np
        import matplotlib.pyplot as plt
        # It would be faster if you download the DEM to your local computer
        demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
        dlxy = demP.res[0]
        dem = demP.read(1)
        demgrd = insolf.cgrad(dem,dlxy)
        xcomponent = demgrd[0,:,:]
        ycomponent = demgrd[1,:,:]
        zcomponent = demgrd[2,:,:]
        titletext = ['xcomponent','ycomponent','zcomponent']
        plt.imshow(dem, cmap='Greys_r')
        plt.colorbar()
        plt.contour(dem)
        plt.title('DEM of the Pyrenees')
        print("press 'q' to continue")
        plt.show()
        for p in np.arange(3):
            plt.imshow(demgrd[p,:,:], cmap='Greys_r')
            plt.colorbar()
            plt.title(titletext[p])
            print("press 'q' to contiqnue")
            plt.show()


    """

    dem = dem[::-1,...] # flip dem to conform python x, y order to to insol coordinate system convention
    rows, cols = dem.shape
    cellgr = np.zeros((3, rows, cols))
    mm = dem
    md = mm[:-1, 1:]
    mr = mm[1:, :-1]
    mrd = mm[1:, 1:]
    cellgr[1, :-1, :-1] = -0.5 * dlxy * (mm[:-1, :-1] + md - mr - mrd)  
    cellgr[0, :-1, :-1] = 0.5 * dlxy * (mm[:-1, :-1] - md + mr - mrd)
    cellgr[2, :-1, :-1] = dlxy * dlxy 
    cellgr[:,:,-1]=cellgr[:,:,-2]  
    cellgr[:,-1,:]=cellgr[:,-2,:]
    cellgr = np.flip(cellgr, axis=1) # flip results back
    cellArea = LA.norm(cellgr,axis=0)
    cellgrunit = np.divide(cellgr,cellArea)
    if cArea:
        return(cellArea)
    else:
        return(cellgrunit)
def daylength(latitude, longitude, jd, tmz)

Computes duration of day light for a given latitude and Julian Day.

Parameters

latitude,  float, latitude in degrees and decimal fraction.
longitude, float, longitude in degrees and decimal fraction.
jd, float, Julian Day, see help(insolf.julian_day).
tmz, float, Timezone, west of Greenwich is negative.

Returns

sunrise, sunset, duration of day light in hours, ndarray of floats of 
shape (3, n) where n is length of input array.

References

Corripio, J. G. (2003). Vectorial algebra algorithms for calculating 
terrain parameters from DEMs and solar radiation modelling in mountainous
terrain. International Journal of Geographical Information Science, 17(1), 1–23.  
<https://doi.org/10.1080/713811744>

Notes

It considers sunrise and sunset as the time when the center of the sun passes above or below the horizon. 
It does not take into account limb, summer time, atmospheric refraction or twilight.
To add, refraction, dip due to observer altitude and more.

Examples

from insolation import insolf
import numpy as np
import matplotlib.pyplot as plt

# Day length at Greenwich Observatory on the 21st of March
insolf.daylength(51.4767,0.0,insolf.julian_day(2023,3,21,12),0)
# array([ 6.10004075, 18.14060504, 12.04056429])

# Daylength at Reykjavík on the 21st of June
insolf.julian_day(2023,6,21,12)
2460117.0
insolf.daylength(64.12,-21.87, 2460117.0, 0)
# array([ 3.26596303, 23.70863056, 20.44266753])

# Daylength for the whole 2023 year at Reykjavík 
jd2023noon = np.arange(insolf.julian_day(2023,1,1,12),insolf.julian_day(2023,12,31,12)+1)
doy = jd2023noon - jd2023noon[0] + 1
day_length = insolf.daylength(64.12, -21.87, jd2023noon, 0)
plt.plot(doy,day_length[2,:])
plt.xlabel('Day of the year')
plt.ylabel('Day length [h]')
plt.ylim(0,24)
plt.title('Daylength over the year at Reykjavík')
plt.show()
Expand source code
def daylength(latitude, longitude, jd, tmz):
    """ 
    Computes duration of day light for a given latitude and Julian Day.

    Parameters
    ----------
        latitude,  float, latitude in degrees and decimal fraction.
        longitude, float, longitude in degrees and decimal fraction.
        jd, float, Julian Day, see help(insolf.julian_day).
        tmz, float, Timezone, west of Greenwich is negative.

    Returns
    -------
        sunrise, sunset, duration of day light in hours, ndarray of floats of 
        shape (3, n) where n is length of input array.

    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating 
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1), 1–23.  
        https://doi.org/10.1080/713811744

    Notes
    -----
        It considers sunrise and sunset as the time when the center of the sun passes above or below the horizon. 
        It does not take into account limb, summer time, atmospheric refraction or twilight.
        To add, refraction, dip due to observer altitude and more.

    Examples
    --------
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt

        # Day length at Greenwich Observatory on the 21st of March
        insolf.daylength(51.4767,0.0,insolf.julian_day(2023,3,21,12),0)
        # array([ 6.10004075, 18.14060504, 12.04056429])

        # Daylength at Reykjavík on the 21st of June
        insolf.julian_day(2023,6,21,12)
        2460117.0
        insolf.daylength(64.12,-21.87, 2460117.0, 0)
        # array([ 3.26596303, 23.70863056, 20.44266753])

        # Daylength for the whole 2023 year at Reykjavík 
        jd2023noon = np.arange(insolf.julian_day(2023,1,1,12),insolf.julian_day(2023,12,31,12)+1)
        doy = jd2023noon - jd2023noon[0] + 1
        day_length = insolf.daylength(64.12, -21.87, jd2023noon, 0)
        plt.plot(doy,day_length[2,:])
        plt.xlabel('Day of the year')
        plt.ylabel('Day length [h]')
        plt.ylim(0,24)
        plt.title('Daylength over the year at Reykjavík')
        plt.show()

    """

    EqTime = eqtime(jd)
    delta = declination(jd)
    tanlatdel = -np.tan(np.radians(latitude)) * np.tan(np.radians(delta))
    tanlatdel = np.where(tanlatdel > 1, 1, tanlatdel)
    tanlatdel = np.where(tanlatdel < -1, -1, tanlatdel)
    omega = np.arccos(tanlatdel)
    daylen = (2*omega)/(2*np.pi/24.)
    stndmeridian = tmz*15.
    deltaLatTime = longitude-stndmeridian
    deltaLatTime = deltaLatTime * 24/360. 
    sunrise = 12*(1-omega/np.pi)-deltaLatTime-EqTime/60. 
    sunset = 12*(1+omega/np.pi)-deltaLatTime-EqTime/60.
    sunrise = np.where(omega == 0, np.nan, sunrise)
    sunset = np.where(omega == 0, np.nan, sunset)
    return(np.array([sunrise,sunset,daylen]))
def declination(jd)

Computes declination from Julian Date.

Parameters

jd, float, Julian date

Returns

float, declination in degrees and decimal fraction

References

<https://gml.noaa.gov/grad/solcalc/calcdetails.html>

Meeus, J. 1999. Astronomical Algorithms. Willmann-Bell, Richmond, Virginia, USA.

Reda, I. and Andreas, A. 2003. Solar Position Algorithm for Solar Radiation Applications. 55 pp.; 
NREL Report No. TP-560-34302, Revised January 2008. <https://www.nrel.gov/docs/fy08osti/34302.pdf>

Examples

# Current time declination
from insolation import insolf
import datetime
tt = datetime.datetime.now()
jd = insolf.julian_day(tt.year,tt.month,tt.day,tt.hour+tt.minute/60.+tt.second/3600.)
decl = insolf.declination(jd)
Expand source code
def declination(jd):
    """
    Computes declination from Julian Date.

    Parameters
    ----------
        jd, float, Julian date
   
    Returns
    -------
        float, declination in degrees and decimal fraction

    References
    ----------
        https://gml.noaa.gov/grad/solcalc/calcdetails.html

        Meeus, J. 1999. Astronomical Algorithms. Willmann-Bell, Richmond, Virginia, USA.

        Reda, I. and Andreas, A. 2003. Solar Position Algorithm for Solar Radiation Applications. 55 pp.; 
        NREL Report No. TP-560-34302, Revised January 2008. https://www.nrel.gov/docs/fy08osti/34302.pdf

    Examples
    --------
        # Current time declination
        from insolation import insolf
        import datetime
        tt = datetime.datetime.now()
        jd = insolf.julian_day(tt.year,tt.month,tt.day,tt.hour+tt.minute/60.+tt.second/3600.)
        decl = insolf.declination(jd)

    """

    T = (jd - 2451545)/36525
    epsilon = (23 + 26/60 + 21.448/3600) - (46.815/3600) * T - (0.00059/3600) * T**2 + (0.001813/3600) * T**3
    L0 = 280.46645 + 36000.76983 * T + 0.0003032 * T**2
    M = 357.5291 + 35999.0503 * T - 0.0001559 * T**2 - 4.8e-07 * T**3
    e = 0.016708617 - 4.2037e-05 * T - 1.236e-07 * T**2
    C = (1.9146 - 0.004817 * T - 1.4e-05 * T**2) * np.sin(np.radians(M)) + \
    (0.019993 - 0.000101 * T) * np.sin(2 * np.radians(M)) + 0.00029 * np.sin(3 * np.radians(M))
    Theta = L0 + C
    v = M + C
    Omega = 125.04452 - 1934.136261 * T + 0.0020708 * T**2 + (T**3)/450000
    lambdad = Theta - 0.00569 - 0.00478 * np.sin(np.radians(Omega))
    delta = np.arcsin(np.sin(np.radians(epsilon)) * np.sin(np.radians(lambdad)))
    return(np.degrees(delta))
def doshade(dem, res, sun_v, num_sweeps=1)

Computes cast shadows over a DEM unorthodox way to call the function to avoid unnecessary imports (numba) if not used. python implementation from openamundsen package based on the f90 function in R insol by Florian Hanzer https://github.com/openamundsen/openamundsen

Parameters

dem, ndrray, Digital elevation Model 2D array 
res, int, resolution of the DEM (pixel size)
sun_v, float array, unit vector in the direction of the sun
num_sweeps

Returns

2D array of the same shade as dem with 1 for illuminated pixels and 0 for pixels in the shade

References

Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
terrain parameters from DEMs and solar radiation modelling in mountainous
terrain. International Journal of Geographical Information Science, 17(1),
1–23. <https://doi.org/10.1080/713811744>

Examples

# Calculate and plot the cast shadows of a pyramid with the sun at the northwest and 15 degrees elevation
from insolation import insolf
import numpy as np
import matplotlib.pyplot as plt
# define the sun vector: northwest at 15 degrees elevation
sv = insolf.normalvector(75,315)
# define broadly a regular pyramid
ncols = 100
nrows = 100
height = 50  
nh = 2*height/ncols
m = np.zeros([ncols,nrows])
for i in np.arange(ncols):
    for j in np.arange(nrows):
        m[i,j]=height-max(abs(nh*i-height),abs(nh*j-height))

# place it on a larger flat area
mm = np.zeros([500,500])
mm[200:300,200:300] = m
# calculate and plot shadows 
sh = insolf.doshade(mm, 1, sv)
plt.imshow(sh, cmap='Greys_r')
plt.colorbar()
plt.contour(mm)
plt.title('Shadow of a Pyramid with the sun at 315 azimuth, 15 elevation')
print("press 'q' to continue")
plt.show()
Expand source code
def doshade(dem, res, sun_v, num_sweeps=1):
    """ 
    Computes cast shadows over a DEM
    unorthodox way to call the function to avoid unnecessary imports (numba) if not used.
    python implementation from openamundsen package based on the f90 function in R insol
    by Florian Hanzer https://github.com/openamundsen/openamundsen
    

    Parameters
    ----------
        dem, ndrray, Digital elevation Model 2D array 
        res, int, resolution of the DEM (pixel size)
        sun_v, float array, unit vector in the direction of the sun
        num_sweeps

    Returns
    -------
        2D array of the same shade as dem with 1 for illuminated pixels and 0 for pixels in the shade

    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1),
        1–23. https://doi.org/10.1080/713811744

    Examples
    --------
        # Calculate and plot the cast shadows of a pyramid with the sun at the northwest and 15 degrees elevation
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt
        # define the sun vector: northwest at 15 degrees elevation
        sv = insolf.normalvector(75,315)
        # define broadly a regular pyramid
        ncols = 100
        nrows = 100
        height = 50  
        nh = 2*height/ncols
        m = np.zeros([ncols,nrows])
        for i in np.arange(ncols):
            for j in np.arange(nrows):
                m[i,j]=height-max(abs(nh*i-height),abs(nh*j-height)) 

        # place it on a larger flat area
        mm = np.zeros([500,500])
        mm[200:300,200:300] = m
        # calculate and plot shadows 
        sh = insolf.doshade(mm, 1, sv)
        plt.imshow(sh, cmap='Greys_r')
        plt.colorbar()
        plt.contour(mm)
        plt.title('Shadow of a Pyramid with the sun at 315 azimuth, 15 elevation')
        print("press 'q' to continue")
        plt.show()

    """

    from insolation import shadows
    return(shadows.shadows(dem, res, sun_v, num_sweeps))
def eqtime(jd)

Computes the equation of time for a given Julian Date.

Parameters

jd, float, Julian date.

Returns

float, equation of time in minutes.

References

<https://gml.noaa.gov/grad/solcalc/calcdetails.html>

Meeus, J. 1999. Astronomical Algorithms. Willmann-Bell, Richmond, Virginia, USA.

Reda, I. and Andreas, A. 2003. Solar Position Algorithm for Solar Radiation Applications. 55 pp.; 
    NREL Report No. TP-560-34302, Revised January 2008. <https://www.nrel.gov/docs/fy08osti/34302.pdf>

Examples

from insolation import insolf
import numpy as np
import matplotlib.pyplot as plt

# plot the equation of time for 2023 at daily intervals
jd2023noon = np.arange(insolf.julian_day(2023,1,1,12),insolf.julian_day(2023,12,31,12)+1)
plt.plot(insolf.eqtime(jd2023noon))
plt.axhline(y=0.0, linestyle='-', color='k', linewidth=0.5)
plt.xlabel('Day of the year')
plt.ylabel('Equation of time [minutes]')
plt.show()

# Analema
plt.plot(insolf.eqtime(jd2023noon), insolf.declination(jd2023noon))
plt.show()

# Analema viewed from Greenwich Observatory
latGwch = 51.4791
x = 180 + insolf.eqtime(jd2023noon) * 15/60
y = 90 - latGwch + insolf.declination(jd2023noon)
# plt.plot(x,y)
plt.scatter(x,y,s=40, facecolors='none', edgecolors='k')
plt.xlabel("Azimuth")
plt.ylabel("Elevation")
plt.ylim(0,90)
plt.title('Equation of time for 2023 at daily intervals')
plt.show()

# Add the solstices and equinoxes (nearest day, see Meeus ch. 26 for more precision)
decl = insolf.declination(jd2023noon)
wintersolstice = np.argmin(decl)
summersolstice = np.argmax(decl)
# equinoxes when decl is closest to zero, find spring of=n first half of the year and then autumn (for N hemisphere)
springequinox = np.argmin(np.abs(decl[0:180]))
autumnequinox = 180 + np.argmin(np.abs(decl[180:365]))
nodeseqx = ([springequinox,summersolstice,autumnequinox,wintersolstice])
plt.scatter(x,y,s=40, facecolors='none', edgecolors='k')
plt.scatter(x[nodeseqx],y[nodeseqx], s=80, facecolors='c', edgecolors='k')
plt.xlabel("Azimuth")
plt.ylabel("Elevation")
plt.ylim(0,90)
plt.title('Analema, equinoxes and solstices from Greenwich')
plt.show()
Expand source code
def eqtime(jd):
    """
    Computes the equation of time for a given Julian Date.

    Parameters
    ----------
        jd, float, Julian date.

    Returns
    -------
        float, equation of time in minutes.

    References
    ----------
        https://gml.noaa.gov/grad/solcalc/calcdetails.html

        Meeus, J. 1999. Astronomical Algorithms. Willmann-Bell, Richmond, Virginia, USA.

        Reda, I. and Andreas, A. 2003. Solar Position Algorithm for Solar Radiation Applications. 55 pp.; 
            NREL Report No. TP-560-34302, Revised January 2008. https://www.nrel.gov/docs/fy08osti/34302.pdf
    
    Examples
    --------
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt

        # plot the equation of time for 2023 at daily intervals
        jd2023noon = np.arange(insolf.julian_day(2023,1,1,12),insolf.julian_day(2023,12,31,12)+1)
        plt.plot(insolf.eqtime(jd2023noon))
        plt.axhline(y=0.0, linestyle='-', color='k', linewidth=0.5)
        plt.xlabel('Day of the year')
        plt.ylabel('Equation of time [minutes]')
        plt.show()

        # Analema
        plt.plot(insolf.eqtime(jd2023noon), insolf.declination(jd2023noon))
        plt.show()

        # Analema viewed from Greenwich Observatory
        latGwch = 51.4791
        x = 180 + insolf.eqtime(jd2023noon) * 15/60
        y = 90 - latGwch + insolf.declination(jd2023noon)
        # plt.plot(x,y)
        plt.scatter(x,y,s=40, facecolors='none', edgecolors='k')
        plt.xlabel("Azimuth")
        plt.ylabel("Elevation")
        plt.ylim(0,90)
        plt.title('Equation of time for 2023 at daily intervals')
        plt.show()

        # Add the solstices and equinoxes (nearest day, see Meeus ch. 26 for more precision)
        decl = insolf.declination(jd2023noon)
        wintersolstice = np.argmin(decl)
        summersolstice = np.argmax(decl)
        # equinoxes when decl is closest to zero, find spring of=n first half of the year and then autumn (for N hemisphere)
        springequinox = np.argmin(np.abs(decl[0:180]))
        autumnequinox = 180 + np.argmin(np.abs(decl[180:365]))
        nodeseqx = ([springequinox,summersolstice,autumnequinox,wintersolstice])
        plt.scatter(x,y,s=40, facecolors='none', edgecolors='k')
        plt.scatter(x[nodeseqx],y[nodeseqx], s=80, facecolors='c', edgecolors='k')
        plt.xlabel("Azimuth")
        plt.ylabel("Elevation")
        plt.ylim(0,90)
        plt.title('Analema, equinoxes and solstices from Greenwich')
        plt.show()

    """

    jdc = (jd - 2451545.0)/36525.0
    sec = 21.448 - jdc*(46.8150 + jdc*(0.00059 - jdc*(0.001813)))
    e0 = 23.0 + (26.0 + (sec/60.0))/60.0  
    ecc = 0.016708634 - jdc * (0.000042037 + 0.0000001267 * jdc)
    oblcorr = e0 + 0.00256 * np.cos(np.radians(125.04 - 1934.136 * jdc))   
    y = (np.tan(np.radians(oblcorr)/2))**2
    l0 = 280.46646 + jdc * (36000.76983 + jdc*(0.0003032))
    l0 = (l0-360*(l0//360))%360
    rl0 = np.radians(l0)
    gmas = 357.52911 + jdc * (35999.05029 - 0.0001537 * jdc)
    gmas = np.radians(gmas)
    EqTime = y*np.sin(2*rl0)-2.0*ecc*np.sin(gmas)+4.0*ecc*y*np.sin(gmas)*np.cos(2*rl0)-0.5*y**2*np.sin(4*rl0)-1.25*ecc**2*np.sin(2*gmas)
    return(np.degrees(EqTime)*4)
def hillshading(dem, dlxy, sunv)

Computes the intensity of illumination over a surface (DEM) according to the position of the sun.

Parameters

dem, ndrray, Digital elevation Model 2D array 
dlxy, int, resolution of the DEM (pixel size)
sunv, float array, unit vector in the direction of the sun

Returns

hsh, ndarray of floats of the same dimensions as dem Illumination intensity ranging from 0 to 1

References

Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
terrain parameters from DEMs and solar radiation modelling in mountainous
terrain. International Journal of Geographical Information Science, 17(1),
1–23. <https://doi.org/10.1080/713811744>

Examples

from insolation import insolf
import rasterio
import numpy as np
import matplotlib.pyplot as plt
demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
dlxy = demP.res[0]
dem = demP.read(1)
# Sun at azimuth 315, 35 degrees elevation
sv = insolf.normalvector(55,315)
grd = insolf.cgrad(dem,dlxy)
hsh = grd[0,:,:]*sv[0] + grd[1,:,:]*sv[1] + grd[2,:,:]*sv[2]
# remove negative incidence angles (self shading) 
hsh = (hsh+np.abs(hsh))/2
plt.imshow(hsh, cmap='Greys_r',vmin=0, vmax=1)
plt.colorbar()
plt.show()
# Add cast shadows
sh = insolf.doshade(dem, dlxy, sv)
plt.imshow(hsh*sh, cmap='Greys_r',vmin=0, vmax=1)
plt.colorbar()
plt.show()
Expand source code
def hillshading(dem,dlxy,sunv):
    """
    Computes the intensity of illumination over a surface (DEM) according to the position of the sun.

    Parameters
    ----------
        dem, ndrray, Digital elevation Model 2D array 
        dlxy, int, resolution of the DEM (pixel size)
        sunv, float array, unit vector in the direction of the sun

    Returns
    -------
        hsh, ndarray of floats of the same dimensions as dem Illumination intensity ranging from 0 to 1

    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1),
        1–23. https://doi.org/10.1080/713811744        

    Examples
    --------
        from insolation import insolf
        import rasterio
        import numpy as np
        import matplotlib.pyplot as plt
        demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
        dlxy = demP.res[0]
        dem = demP.read(1)
        # Sun at azimuth 315, 35 degrees elevation
        sv = insolf.normalvector(55,315)
        grd = insolf.cgrad(dem,dlxy)
        hsh = grd[0,:,:]*sv[0] + grd[1,:,:]*sv[1] + grd[2,:,:]*sv[2]
        # remove negative incidence angles (self shading) 
        hsh = (hsh+np.abs(hsh))/2
        plt.imshow(hsh, cmap='Greys_r',vmin=0, vmax=1)
        plt.colorbar()
        plt.show()
        # Add cast shadows
        sh = insolf.doshade(dem, dlxy, sv)
        plt.imshow(hsh*sh, cmap='Greys_r',vmin=0, vmax=1)
        plt.colorbar()
        plt.show() 

    """

    demcgrad = cgrad(dem,dlxy)
    hsh = demcgrad[0,:,:]*sunv[0] + demcgrad[1,:,:]*sunv[1] + demcgrad[2,:,:]*sunv[2]
    hsh = (hsh + abs(hsh))/2.0      # set to zero self-shading pixels
    return(hsh) 
def hourangle(jd, longitude, timezone)

Hour angle, internal function for solar position.

Parameters

  jd, float, Julian date
  longitude, float, longitude in decimal degrees
  timezone, float, timezone in hours, west of Greenwich is negative
Expand source code
def hourangle(jd,longitude,timezone):
    """
    Hour angle, internal function for solar position.

    Parameters
    ----------
          jd, float, Julian date
          longitude, float, longitude in decimal degrees
          timezone, float, timezone in hours, west of Greenwich is negative

    """

    hour = ((jd-np.floor(jd))*24+12) % 24
    eqtimeval = eqtime(jd)
    stndmeridian = timezone*15                  
    deltalontime = longitude-stndmeridian       
    deltalontime = deltalontime * 24.0/360.0    
    omegar = np.pi*( ( (hour + deltalontime + eqtimeval/60)/12.0 ) - 1.0) 
    return(omegar)
def insolation(zenith, jd, height, visibility, RH, tempK, O3, alphag)

Computes direct and diffuse solar irradiance perpendicular to the beam, for a given zenith angle, Julian Day, altitude and atmospheric conditions.

Parameters

  zenith       Zenith angle in degrees
  jd           Julian Day
  height       Altitude above sea level
  visibility   Visibility [km]
  RH           Relative humidity [%]
  tempK        Air temperature [K]
  O3           Ozone thickness [m]
  alphag       Albedo of the surrounding terrain [0 to 1]

Returns

array-like, first element is direct radiation, second element is diffuse radiation.
            If any input is an array First row is direct radiation and second row diffuse radiation

References

Bird, R. E. and Hulstrom, R. L. (1981a) Review, evaluation and improvements of direct irradiance models, 
Trans. ASME J. Solar Energy Eng. 103, 182-192.

Bird, R. E. and Hulstrom, R. L. (1981b) A simplified clear sky model for direct 
and diffuse insolation on horizontal surfaces, 
Technical Report SERI/TR-642-761, Solar Research Institute, Golden, Colorado.

Iqbal, M. (1983) An Introduction to Solar Radiation, Academic Press, Toronto.

Examples

from insolation import insolf
import datetime
import rasterio
import numpy as np
import matplotlib.pyplot as plt
from tabulate import tabulate

insolf.insolation(30,2456007,3200,28,60,278.15,0.02,0.2)
# Daily insolation in Spring
# Find nearest hour to sunrise and sunset
TIMESTAMP = "2023-03-21 12:00:00"
tt = datetime.datetime.strptime(TIMESTAMP,'%Y-%m-%d %H:%M:%S')
jd = insolf.julian_day(tt.year,tt.month,tt.day,tt.hour+tt.minute/60.+tt.second/3600.)
latitude = 42.675
longitude = 0.033
timezone = 1
sr,ss,dl = insolf.daylength(42.675,0.033, jd, timezone)
sr = np.ceil(sr)
ss = np.floor(ss)
sr2ss = np.arange(sr,ss)
jdrng = insolf.julian_day(tt.year,tt.month,tt.day,sr2ss)
sunv = insolf.sunvector(jdrng,latitude,longitude,timezone)
azimuth,zenith = insolf.sunpos(sunv)
height = 2000
visibility = 60
RH = 55
tempK = 285.0
O3 = 0.02
alphag = 0.2
Idir,Idiff = insolf.insolation(zenith,jdrng,height,visibility,RH,tempK,O3,alphag)
Hnormal = np.array([0,0,1])
IdirHoriz = Idir * np.dot(np.transpose(sunv),Hnormal)
print("Hourly direct normal, direct horizontal and diffuse insolation at 43.7N, 0E, Spring")
print(tabulate({"Hour": sr2ss,"I_direct_normal": Idir, "I_direct_horizontal": IdirHoriz, "I_diff": Idiff}, headers="keys"))
  Hour    I_direct_normal    I_direct_horizontal    I_diff
------  -----------------  ---------------------  --------
     8            487.849                83.0165   58.1299
     9            723.68                253.228    87.8123
    10            829.915               419.907   101.989
    11            884.963               555.478   110.169
    12            912.945               645.265   114.711
    13            922.682               680.859   116.373
    14            916.784               659.104   115.362
    15            893.716               581.95    111.559
    16            846.622               456.52    104.389
    17            756.851               295.762    92.0956
    18            567.277               122.506    68.1983
# plot the results for the whole day
dayh = np.arange(0,24,5/60)     # every 5 minutes
jdrng = insolf.julian_day(tt.year,tt.month,tt.day,dayh)
sunv = insolf.sunvector(jdrng,latitude,longitude,timezone)
azimuth,zenith = insolf.sunpos(sunv)
Idir,Idiff = insolf.insolation(zenith,jdrng,height,visibility,RH,tempK,O3,alphag)
IdirHoriz = Idir * np.dot(np.transpose(sunv),Hnormal)
plt.figure(figsize=(10,6))
plt.plot(dayh,Idir,'r', label='Direct Normal')
plt.plot(dayh,IdirHoriz,'y', label='Direct Horizontal')
plt.plot(dayh,Idiff,'c', label='Diffuse')
plt.legend(loc="upper left")
plt.grid(visible=True,linestyle="-.")
plt.xlabel('Time')
plt.ylabel('Insolation Wm$^{-2}$')
plt.title("Direct normal, direct horizontal and diffuse insolation at 43.7N, 0E, Spring")
plt.ylim(0,1380)
plt.show()

# Alternatively, loop over every hour
# Compute insolation on a DEM of the pyrenees:
demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
# demP = rasterio.open('dempyrenees.tif')
dlxy = demP.res[0]
dem = demP.read(1)
I_tot = np.zeros(dem.shape)
for i in np.arange(len(dayh)):
    sunv = insolf.sunvector(jdrng[i],latitude,longitude,timezone)
    azimuth,zenith = insolf.sunpos(sunv)
    hsh = insolf.hillshading(dem,dlxy,sunv)
    Idir,Idiff = insolf.insolation(zenith,jdrng[i],height,visibility,RH,tempK,O3,alphag)
    # Global insolation in MJ/m^2
    W2MJ = 3600e-6 # Watts/m^2 to MJ/m^2 hourly computation
    I_tot = I_tot + W2MJ*Idir*hsh + W2MJ*Idiff   # The diffuse fraction should consider the skyview factor for a propper calculation... to be implemented

plt.imshow(I_tot, cmap='plasma')
plt.colorbar()
plt.title("Dayly insolation over the Pyrenees in Spring $MJ^{-2}$")
plt.show()
Expand source code
def insolation(zenith,jd,height,visibility,RH,tempK,O3,alphag):
    """
    Computes direct and diffuse solar irradiance perpendicular to the beam, for a given 
    zenith angle, Julian Day, altitude and atmospheric conditions.

    Parameters
    ----------
          zenith       Zenith angle in degrees
          jd           Julian Day
          height       Altitude above sea level
          visibility   Visibility [km]
          RH           Relative humidity [%]
          tempK        Air temperature [K]
          O3           Ozone thickness [m]
          alphag       Albedo of the surrounding terrain [0 to 1]

    Returns
    -------
        array-like, first element is direct radiation, second element is diffuse radiation.
                    If any input is an array First row is direct radiation and second row diffuse radiation

    References
    ----------
        Bird, R. E. and Hulstrom, R. L. (1981a) Review, evaluation and improvements of direct irradiance models, 
        Trans. ASME J. Solar Energy Eng. 103, 182-192.

        Bird, R. E. and Hulstrom, R. L. (1981b) A simplified clear sky model for direct 
        and diffuse insolation on horizontal surfaces, 
        Technical Report SERI/TR-642-761, Solar Research Institute, Golden, Colorado.
        
        Iqbal, M. (1983) An Introduction to Solar Radiation, Academic Press, Toronto.

    Examples
    --------
        from insolation import insolf
        import datetime
        import rasterio
        import numpy as np
        import matplotlib.pyplot as plt
        from tabulate import tabulate

        insolf.insolation(30,2456007,3200,28,60,278.15,0.02,0.2)
        # Daily insolation in Spring
        # Find nearest hour to sunrise and sunset
        TIMESTAMP = "2023-03-21 12:00:00"
        tt = datetime.datetime.strptime(TIMESTAMP,'%Y-%m-%d %H:%M:%S')
        jd = insolf.julian_day(tt.year,tt.month,tt.day,tt.hour+tt.minute/60.+tt.second/3600.)
        latitude = 42.675
        longitude = 0.033
        timezone = 1
        sr,ss,dl = insolf.daylength(42.675,0.033, jd, timezone)
        sr = np.ceil(sr)
        ss = np.floor(ss)
        sr2ss = np.arange(sr,ss)
        jdrng = insolf.julian_day(tt.year,tt.month,tt.day,sr2ss)
        sunv = insolf.sunvector(jdrng,latitude,longitude,timezone)
        azimuth,zenith = insolf.sunpos(sunv)
        height = 2000
        visibility = 60
        RH = 55
        tempK = 285.0
        O3 = 0.02
        alphag = 0.2
        Idir,Idiff = insolf.insolation(zenith,jdrng,height,visibility,RH,tempK,O3,alphag)
        Hnormal = np.array([0,0,1])
        IdirHoriz = Idir * np.dot(np.transpose(sunv),Hnormal)
        print("Hourly direct normal, direct horizontal and diffuse insolation at 43.7N, 0E, Spring")
        print(tabulate({"Hour": sr2ss,"I_direct_normal": Idir, "I_direct_horizontal": IdirHoriz, "I_diff": Idiff}, headers="keys"))
          Hour    I_direct_normal    I_direct_horizontal    I_diff
        ------  -----------------  ---------------------  --------
             8            487.849                83.0165   58.1299
             9            723.68                253.228    87.8123
            10            829.915               419.907   101.989
            11            884.963               555.478   110.169
            12            912.945               645.265   114.711
            13            922.682               680.859   116.373
            14            916.784               659.104   115.362
            15            893.716               581.95    111.559
            16            846.622               456.52    104.389
            17            756.851               295.762    92.0956
            18            567.277               122.506    68.1983
        # plot the results for the whole day
        dayh = np.arange(0,24,5/60)     # every 5 minutes
        jdrng = insolf.julian_day(tt.year,tt.month,tt.day,dayh)
        sunv = insolf.sunvector(jdrng,latitude,longitude,timezone)
        azimuth,zenith = insolf.sunpos(sunv)
        Idir,Idiff = insolf.insolation(zenith,jdrng,height,visibility,RH,tempK,O3,alphag)
        IdirHoriz = Idir * np.dot(np.transpose(sunv),Hnormal)
        plt.figure(figsize=(10,6))
        plt.plot(dayh,Idir,'r', label='Direct Normal')
        plt.plot(dayh,IdirHoriz,'y', label='Direct Horizontal')
        plt.plot(dayh,Idiff,'c', label='Diffuse')
        plt.legend(loc="upper left")
        plt.grid(visible=True,linestyle="-.")
        plt.xlabel('Time')
        plt.ylabel('Insolation Wm$^{-2}$')
        plt.title("Direct normal, direct horizontal and diffuse insolation at 43.7N, 0E, Spring")
        plt.ylim(0,1380)
        plt.show()

        # Alternatively, loop over every hour
        # Compute insolation on a DEM of the pyrenees:
        demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
        # demP = rasterio.open('dempyrenees.tif')
        dlxy = demP.res[0]
        dem = demP.read(1)
        I_tot = np.zeros(dem.shape)
        for i in np.arange(len(dayh)):
            sunv = insolf.sunvector(jdrng[i],latitude,longitude,timezone)
            azimuth,zenith = insolf.sunpos(sunv)
            hsh = insolf.hillshading(dem,dlxy,sunv)
            Idir,Idiff = insolf.insolation(zenith,jdrng[i],height,visibility,RH,tempK,O3,alphag)
            # Global insolation in MJ/m^2
            W2MJ = 3600e-6 # Watts/m^2 to MJ/m^2 hourly computation
            I_tot = I_tot + W2MJ*Idir*hsh + W2MJ*Idiff   # The diffuse fraction should consider the skyview factor for a propper calculation... to be implemented

        plt.imshow(I_tot, cmap='plasma')
        plt.colorbar()
        plt.title("Dayly insolation over the Pyrenees in Spring $MJ^{-2}$")
        plt.show()

    """

    Isc = 1361.0            # solar constant (Wm**(-2)) (1)
    zenith = np.where(zenith > 90, 90, zenith)
    theta = np.radians(zenith)
    ssctalb = 0.9  # single scattering albedo (aerosols)(Iqbal, 1983)
    Fc = 0.84      # ratio of forward to total energy scattered (Iqbal, 1983)
    Pz = atmosf.z2p(height)
    Mr = 1.0/(np.cos(theta)+0.15*((93.885-zenith)**(-1.253)))
    Ma = Mr*Pz/1013.25
    #** Use Lowe(1977) Lowe's polynomials for vapor pressure
    wvap_s =  atmosf.wvapsat(tempK)
    #Wprec = 0.493*(RH/100.0)*wvap_s/tempK   #precipitable water in cm Leckner (1978)
    Wprec = 46.5*(RH/100.0)*wvap_s/tempK  #Prata 1996
    rho2 = (1/sunr(jd))**2
    TauR = np.exp((-.09030*(Ma**0.84) )*(1.0+Ma-(Ma**1.01)) )
    TauO = 1.0-( ( 0.1611*(O3*Mr)*(1.0+139.48*(O3*Mr))**(-0.3035) ) - 0.002715*(O3*Mr)*( 1.0+0.044*(O3*Mr)+0.0003*(O3*Mr)**2 )**(-1))
    TauG = np.exp(-0.0127*(Ma**0.26))
    TauW = 1.0-2.4959*(Wprec*Mr)*( (1.0+79.034*(Wprec*Mr))**0.6828 + 6.385*(Wprec*Mr) )**(-1)
    TauA = ( 0.97-1.265*(visibility**(-0.66)) )**(Ma**0.9)   #Machler, 1983
    TauTotal = TauR*TauO*TauG*TauW*TauA   
    In = 0.9751*rho2*Isc*TauTotal
    tauaa = 1.0-(1.0-ssctalb)*(1.0-Ma+Ma**1.06)*(1.0-TauA)
    # tauaa = 1 - (1 - ssctalb) * (1 - Ma + Ma^1.06) * (1 - TauA)
    Idr = 0.79*rho2*Isc*np.cos(theta)*TauO*TauG*TauW*tauaa*0.5*(1.0-TauR)/(1.0-Ma+Ma**(1.02))
    tauas = (TauA)/tauaa
    Ida = 0.79*rho2*Isc*np.cos(theta)*TauO*TauG*TauW*tauaa*Fc*(1.0-tauas)/(1.0-Ma+Ma**1.02)
    alpha_atmos = 0.0685+(1.0-Fc)*(1.0-tauas)
    Idm = (In*np.cos(theta)+Idr+Ida)*alphag*alpha_atmos/(1.0-alphag*alpha_atmos)
    Id = Idr+Ida+Idm
    In = np.where(zenith >= 90, 0, In)   # Set In=0 if after sunset
    Id = np.where(zenith >= 90, 0, Id)
    return(np.array([In,Id]))
def julian_day(Y, m, d, H, min=0, sec=0)

Computes Julian Day (days since January 1, 4713 BCE at noon UTC) from 1990 edition of the U.S. Naval Observatory's Almanac for Computers Valid AD 1801 - 2099

Parameters

 Y (int)        Year, format yyyy
 m (short int)  Month number.
 d (short int)  Day-of-month.
 H (float)      Hour-of-day and decimal fraction.

Returns

 float, Julian Day (days since January 1, 4713 BCE at noon UTC)

References

 <https://adsabs.harvard.edu/full/1983IAPPP..13...16F>
 <https://aa.usno.navy.mil/software/novaspy_intro>
 <https://aa.usno.navy.mil/faq/JD_formula>
 check results 
 <https://aa.usno.navy.mil/data/JulianDate>

Examples

 from insolation import insolf
 import datetime
 TIMESTAMP = "2023-09-23 06:00:00"
 tt = datetime.datetime.strptime(TIMESTAMP,'%Y-%m-%d %H:%M:%S')
 print(insolf.julian_day(tt.year,tt.month,tt.day,tt.hour+tt.minute/60.+tt.second/3600.))
 2460210.75
 insolf.julian_day(2023,21,3,12)  # Check the month/day order, it will accept months > 12 without warning
 2460556.0
 insolf.julian_day(2024,9,3,12)
 2460557.0
Expand source code
def julian_day(Y, m, d, H, min = 0, sec = 0):
   """
   Computes Julian Day (days since January 1, 4713 BCE at noon UTC)
   from 1990 edition of the U.S. Naval Observatory's Almanac for Computers Valid AD 1801 - 2099



   Parameters
   ----------
        Y (int)        Year, format yyyy
        m (short int)  Month number.
        d (short int)  Day-of-month.
        H (float)      Hour-of-day and decimal fraction.

   Returns
   -------
        float, Julian Day (days since January 1, 4713 BCE at noon UTC)

   References
   ----------

        https://adsabs.harvard.edu/full/1983IAPPP..13...16F
        https://aa.usno.navy.mil/software/novaspy_intro
        https://aa.usno.navy.mil/faq/JD_formula
        check results 
        https://aa.usno.navy.mil/data/JulianDate

   Examples
   --------
        from insolation import insolf
        import datetime
        TIMESTAMP = "2023-09-23 06:00:00"
        tt = datetime.datetime.strptime(TIMESTAMP,'%Y-%m-%d %H:%M:%S')
        print(insolf.julian_day(tt.year,tt.month,tt.day,tt.hour+tt.minute/60.+tt.second/3600.))
        2460210.75
        insolf.julian_day(2023,21,3,12)  # Check the month/day order, it will accept months > 12 without warning
        2460556.0
        insolf.julian_day(2024,9,3,12)
        2460557.0

   """

   H = H + min/60.0 + sec/3600.0
   tjd = 367*Y - (7*(Y+(m+9)//12))//4 + (275*m)//9 + d + 1721013.5 + H/24.0 
   return(tjd)
def normalvector(slope, aspect)

Calculates a unit vector normal to a surface defined by slope inclination and slope orientation.

Parameters

slope (float)       slope inclination in degrees
aspect (float)      slope orientation in degrees

Returns

float vector

References

Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
terrain parameters from DEMs and solar radiation modelling in mountainous
terrain. International Journal of Geographical Information Science, 17(1),
1–23. <https://doi.org/10.1080/713811744>

Examples

from insolation import insolf
import numpy as np

# horizontal surface
insolf.normalvector(0,0)
# array([ 0., -0.,  1.])

# surface 45 degrees south
insolf.normalvector(45,180)
# array([8.65956056e-17, 7.07106781e-01, 7.07106781e-01])
Expand source code
def normalvector(slope,aspect):
    """
    Calculates a unit vector normal to a surface defined by slope inclination and slope orientation.

    Parameters
    ----------
        slope (float)       slope inclination in degrees
        aspect (float)      slope orientation in degrees

    Returns
    -------
        float vector
    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1),
        1–23. https://doi.org/10.1080/713811744 

    Examples
    --------
        from insolation import insolf
        import numpy as np

        # horizontal surface
        insolf.normalvector(0,0)
        # array([ 0., -0.,  1.])

        # surface 45 degrees south
        insolf.normalvector(45,180)
        # array([8.65956056e-17, 7.07106781e-01, 7.07106781e-01])
        
    """

    sloper = np.radians(slope) 
    aspectr = np.radians(aspect)
    nvx = np.sin(aspectr)*np.sin(sloper)
    nvy = -np.cos(aspectr)*np.sin(sloper)
    nvz = np.cos(sloper)
    return(np.array([nvx,nvy,nvz]))
def slope(dem, dlxy, degrees=False)

Calculates the inclination of the slope of every grid cell in a digital elevation model (DEM). Zero is an horizontal surface.

Parameters

dem,  ndarray (2D, float), Digital Elevation Model with elevation data.
dlxy, int, resolution of the DEM, pixel size along x and y coordinates.

Keywords

degrees, boolean, if True return degrees instead of radians.

Returns

slp, 2D ndarray of floats of the same size as the input dem.

Examples

# Calculates the slope of every grid cell of a DEM in the Pyrennes and plot it
from insolation import insolf
import rasterio
import matplotlib.pyplot as plt
demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
dlxy = demP.res[0]
dem = demP.read(1)
demslope = insolf.slope(dem,dlxy,degrees = True)
plt.imshow(demslope, cmap='Greys')
plt.colorbar()
plt.show()
Expand source code
def slope(dem,dlxy,degrees = False):
    """
    Calculates the inclination of the slope of every grid cell in a digital elevation model (DEM). Zero is an horizontal surface.

    Parameters
    ----------
        dem,  ndarray (2D, float), Digital Elevation Model with elevation data.
        dlxy, int, resolution of the DEM, pixel size along x and y coordinates.

    Keywords
    --------
        degrees, boolean, if True return degrees instead of radians.

    Returns
    -------
        slp, 2D ndarray of floats of the same size as the input dem.
   
    Examples
    --------
        # Calculates the slope of every grid cell of a DEM in the Pyrennes and plot it
        from insolation import insolf
        import rasterio
        import matplotlib.pyplot as plt
        demP = rasterio.open('https://meteoexploration.com/insol/data/dempyrenees.tif')
        dlxy = demP.res[0]
        dem = demP.read(1)
        demslope = insolf.slope(dem,dlxy,degrees = True)
        plt.imshow(demslope, cmap='Greys')
        plt.colorbar()
        plt.show()

    """

    demcgrad = cgrad(dem,dlxy)
    z = demcgrad[2,:,:]
    slp = np.arccos(z)
    if (degrees):
        slp = np.degrees(slp)
    slp = slp
    return(slp)
def sunpos(sunv, degrees=True)

Returns a matrix of azimuth and zenith angles of the sun given the unit vectors from the observer to the direction of the sun.

Parameters

sunv (float or array of floats)    coordinates x, y, z of the unit vector in the direction of the sun

Returns

array of azimuth and zenith angles

References

Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
terrain parameters from DEMs and solar radiation modelling in mountainous
terrain. International Journal of Geographical Information Science, 17(1),
1–23. <https://doi.org/10.1080/713811744>

Examples

from insolation import insolf
import numpy as np
import matplotlib.pyplot as plt

# Julian Day hourly intervals at spring equinox
jdrng = insolf.julian_day(2013,3,21,np.arange(7, 20))

# sun path over the day at sprint equinox in the Pyrenees
latitude = 42.675
longitude = 0.033
tmz = 1
azimuth, zenith = insolf.sunpos(insolf.sunvector(jdrng,latitude,longitude,tmz))
elevation = 90-zenith
plt.scatter(azimuth,elevation,s=240, facecolors='y', edgecolors='y')
plt.show()
Expand source code
def sunpos(sunv, degrees=True):
    """
    Returns a matrix of azimuth and zenith angles of the sun given the unit vectors from the observer to the direction of the sun.

    Parameters
    ----------
        sunv (float or array of floats)    coordinates x, y, z of the unit vector in the direction of the sun

    Returns
    -------
        array of azimuth and zenith angles

    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1),
        1–23. https://doi.org/10.1080/713811744 

    Examples
    --------
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt

        # Julian Day hourly intervals at spring equinox
        jdrng = insolf.julian_day(2013,3,21,np.arange(7, 20))

        # sun path over the day at sprint equinox in the Pyrenees
        latitude = 42.675
        longitude = 0.033
        tmz = 1
        azimuth, zenith = insolf.sunpos(insolf.sunvector(jdrng,latitude,longitude,tmz))
        elevation = 90-zenith
        plt.scatter(azimuth,elevation,s=240, facecolors='y', edgecolors='y')
        plt.show()

    """

    azimuth = np.pi - np.arctan2(sunv[0],sunv[1] )
    zenith = np.arccos(sunv[2])
    if degrees:
        azimuth = np.degrees(azimuth)
        zenith = np.degrees(zenith)

    return(np.array([azimuth,zenith]))
def sunr(jd)

Calculates the Earth radius vector.

Parameters

jd   Julian Day.

Returns

float Earth Radius Vector in Astronomical Units (AU).

References

<https://gml.noaa.gov/grad/solcalc/calcdetails.html>

Meeus, J. 1999. Astronomical Algorithms. Willmann-Bell, Richmond, Virginia, USA.

Reda, I. and Andreas, A. 2003. Solar Position Algorithm for Solar Radiation Applications. 55 pp.; 
NREL Report No. TP-560-34302, Revised January 2008. <https://www.nrel.gov/docs/fy08osti/34302.pdf>

Examples

# Plot the variation of the earth radius vector over two years
from insolation import insolf
import numpy as np
import matplotlib.pyplot as plt
ndays = np.arange(730)
jd_nexty = insolf.julian_day(2013,1,11,12) + ndays
sun_r = insolf.sunr(jd_nexty)
plt.plot(ndays,sun_r)
plt.axhline(y=1.0, linestyle='-', color='k', linewidth=0.5)
plt.show()
Expand source code
def sunr(jd):
    """
    Calculates the Earth radius vector.

    Parameters
    ----------
        jd   Julian Day.

    Returns
    -------
        float Earth Radius Vector in Astronomical Units (AU).


    References
    ----------
        https://gml.noaa.gov/grad/solcalc/calcdetails.html

        Meeus, J. 1999. Astronomical Algorithms. Willmann-Bell, Richmond, Virginia, USA.

        Reda, I. and Andreas, A. 2003. Solar Position Algorithm for Solar Radiation Applications. 55 pp.; 
        NREL Report No. TP-560-34302, Revised January 2008. https://www.nrel.gov/docs/fy08osti/34302.pdf 

    Examples
    --------
        # Plot the variation of the earth radius vector over two years
        from insolation import insolf
        import numpy as np
        import matplotlib.pyplot as plt
        ndays = np.arange(730)
        jd_nexty = insolf.julian_day(2013,1,11,12) + ndays
        sun_r = insolf.sunr(jd_nexty)
        plt.plot(ndays,sun_r)
        plt.axhline(y=1.0, linestyle='-', color='k', linewidth=0.5)
        plt.show()

    """

    jdc=(jd - 2451545.0)/36525.0
    ecc=0.016708634-jdc*(0.000042037+0.0000001267*jdc)
    gmas=357.52911+jdc*(35999.05029-0.0001537*jdc)
    gmasr=np.radians(gmas)
    seqc=np.sin(gmasr)*(1.914602-jdc*(0.004817+0.000014*jdc))+np.sin(2*gmas)*(0.019993-0.000101*jdc)+ np.sin(3*gmasr)*0.000289
    sta=gmas+seqc
    sunrv=(1.000001018 * (1 - ecc**2)) / (1 + ecc * np.cos(np.radians(sta)))
    return(sunrv)
def sunvector(jd, latitude, longitude, timezone)

Calculates a unit vector in the direction of the sun from the observer position.

Parameters

jd (float)          Julian date
latitude (float)    Latitude of observer in degrees and decimal fraction
longitude (float)   Longitude of observer in degrees and decimal fraction
timezone (float)    Timezone in hours, west of Greenwich is negative

Returns

float vector or array of vectors.

References

Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
terrain parameters from DEMs and solar radiation modelling in mountainous
terrain. International Journal of Geographical Information Science, 17(1),
1–23. <https://doi.org/10.1080/713811744>

Examples

from insolation import insolf
from mpl_toolkits import mplot3d
import numpy as np
import matplotlib.pyplot as plt
# Current solar vector at Greenwich Observatory on the 21st of June, 2023 at midday
latitude = 51.4778
longitude = -0.0017
tmz = 0
jd = insolf.julian_day(2023,6,21,12)
insolf.sunvector(jd,latitude,longitude,0)
# Path of the sun over Greenwich in summer, z axis in degrees of elevation
jdrng = insolf.julian_day(2023,6,21,np.arange(4,20.5,0.5))
sv = insolf.sunvector(jdrng,latitude,longitude,0)
fig = plt.figure()
ax = plt.axes(projection ='3d')
x = sv[0,:]
y = sv[1,:]
z = 90 - np.degrees(np.arccos(sv[2,:]))
ax.plot3D(x, y, z, 'orange')
ax.set_title('Sun position from Greenwich on the 21st of June')
plt.show()
Expand source code
def sunvector(jd,latitude,longitude,timezone):
    """
    Calculates a unit vector in the direction of the sun from the observer position.

    Parameters
    ----------
        jd (float)          Julian date
        latitude (float)    Latitude of observer in degrees and decimal fraction
        longitude (float)   Longitude of observer in degrees and decimal fraction
        timezone (float)    Timezone in hours, west of Greenwich is negative

    Returns
    -------
        float vector or array of vectors.

    References
    ----------
        Corripio, J. G. (2003). Vectorial algebra algorithms for calculating
        terrain parameters from DEMs and solar radiation modelling in mountainous
        terrain. International Journal of Geographical Information Science, 17(1),
        1–23. https://doi.org/10.1080/713811744 

    Examples
    --------
        from insolation import insolf
        from mpl_toolkits import mplot3d
        import numpy as np
        import matplotlib.pyplot as plt
        # Current solar vector at Greenwich Observatory on the 21st of June, 2023 at midday
        latitude = 51.4778
        longitude = -0.0017
        tmz = 0
        jd = insolf.julian_day(2023,6,21,12)
        insolf.sunvector(jd,latitude,longitude,0)
        # Path of the sun over Greenwich in summer, z axis in degrees of elevation
        jdrng = insolf.julian_day(2023,6,21,np.arange(4,20.5,0.5))
        sv = insolf.sunvector(jdrng,latitude,longitude,0)
        fig = plt.figure()
        ax = plt.axes(projection ='3d')
        x = sv[0,:]
        y = sv[1,:]
        z = 90 - np.degrees(np.arccos(sv[2,:]))
        ax.plot3D(x, y, z, 'orange')
        ax.set_title('Sun position from Greenwich on the 21st of June')
        plt.show()

    """

    omegar = hourangle(jd,longitude,timezone)
    deltar = np.radians(declination(jd))
    lambdar = np.radians(latitude) 
    svx = -np.sin(omegar)*np.cos(deltar)
    svy = np.sin(lambdar)*np.cos(omegar)*np.cos(deltar)-np.cos(lambdar)*np.sin(deltar)
    svz = np.cos(lambdar)*np.cos(omegar)*np.cos(deltar)+np.sin(lambdar)*np.sin(deltar) 
    return(np.array([svx,svy,svz]))