PyHelios 0.1.11
Loading...
Searching...
No Matches
Solar Position Plugin Documentation

DependenciesNone
Python Importfrom pyhelios import SolarPosition
Main ClassSolarPosition

System Requirements

Dependencies None
Platforms Windows, Linux, macOS
GPU Not required

Quick Start

from pyhelios import Context, SolarPosition
from pyhelios.types import vec3
with Context() as context:
# Set date and time
context.setDate(1, 5, 2015) # May 1, 2015
context.setTime(30, 12) # 12:30
# Create SolarPosition with location (UTC offset, latitude, longitude)
with SolarPosition(7, 31.256, 119.947, context) as sun:
# Get sun direction
direction = sun.getSunDirectionVector()
elevation = sun.getSunElevation()
azimuth = sun.getSunAzimuth()
print(f"Sun direction: {direction}")
print(f"Elevation: {elevation} radians")
print(f"Azimuth: {azimuth} radians")
# Calculate solar flux with atmospheric conditions
sun.setAtmosphericConditions(101000, 300, 0.6, 0.05)
flux = sun.getSolarFlux()
diffuse_fraction = sun.getDiffuseFraction()
print(f"Solar flux: {flux} W/m²")
print(f"Diffuse fraction: {diffuse_fraction}")

Introduction

This plugin calculates the position of the sun, and also implements other models for solar fluxes as well as longwave fluxes from the sky. Model theory and equations are given in the sections below.

Class Constructor

Constructors
SolarPosition(context)
SolarPosition(context, utc_offset, latitude, longitude)

The SolarPosition class can be initialized by simply passing a Helios context as an argument to the constructor. This gives the class access to the time and date currently set in the Context. The model must also know certain parameters about the simulated location, in particular the offset from UTC time, latitude, and longitude. A description of these parameters are given in the table below. These can be supplied using the second constructor listed in the table above. If only the Context is supplied to the constructor, the plugin uses Context location settings (if configured).

SolarPosition constructor inputs
Input ParameterDescriptionConventionDefault Behavior
UTCDifference in hours between Coordinated Universal Time (UTC) for a particular location. See the figure below to determine a particular UTC offset.UTC offset value is positive moving West.+8:00
latitudeGeographic coordinate that specifies the north–south position of a point on the Earth's surface in degrees.Latitude is positive in the northern hemisphere.+38.55
longitudeGeographic coordinate that specifies the east-west position of a point on the Earth's surface in degrees.Longitude is positive in the western hemisphere.+121.76

Model Theory

Position of the Sun

The solar position model was implemented following the description in Chapter 1 of Iqbal (1983).

The day angle \(\Gamma\) given as the polar angle of the earth relative to the sun ( \(\Gamma=0\) on Jan. 1) is calculated as

\(\Gamma = 2\pi(DOY-1)/365.25\), (1)

where DOY is the Julian Day of the year.

The solar declination angle is then calculated as

\(\delta = 0.006918 - 0.399912\,\mathrm{cos}(\Gamma) + 0.070257\,\mathrm{sin}(\Gamma)- 0.006758\,\mathrm{cos}(2\Gamma) + 0.000907\,\mathrm{sin}(2\Gamma) - 0.002697\,\mathrm{cos}(3\Gamma) + 0.00148\,\mathrm{sin}(3\Gamma)\). (2)

The equation of time is calculated as

\(EoT = 229.18(0.000075 + 0.001868\,\mathrm{cos}(\Gamma) - 0.032077\,\mathrm{sin}(\Gamma) - 0.014615\,\mathrm{cos}(2\Gamma) - 0.04089\,\mathrm{sin}(2\Gamma))\), (3)

The hour angle is given by

\(h=15(LST-12)\), (4)

with

\(LST=hour+minute/60+TC/60\), (5)

and

\(TC=4(15UTC-longitude)+EoT\), (6)

Finally, the solar elevation angle is given by

\(\theta_s=\mathrm{sin}^{-1}( \mathrm{sin}(latitude)\mathrm{sin}(\delta) + \mathrm{cos}(latitude)\mathrm{cos}(\delta)\mathrm{cos}(h) )\), (7)

and the solar azimuthal angle is given by

\(\phi_s=\mathrm{cos}^{-1}( (\mathrm{sin}(\delta) - \mathrm{sin}(\theta_s)\mathrm{sin}(latitude))/(\mathrm{cos}(\theta)\mathrm{cos}(latitude)))\). (8)

Note that \(\mathrm{cos}^{-1}\) gives angles between 0 and \(\pi\), so to get a \(\phi_s\) between 0 and \(2\pi\), we take \(\phi_s=2\pi-\phi_s\) if \(LST>12\).

Direct and Diffuse Solar Flux (REST-2 Model)

Clear-sky solar fluxes are calculated using the 'REST-2' (Reference Evaluation of Solar Transmittance, 2 bands) model of Gueymard (2008). REST-2 is a high-performance broadband radiative transfer model derived from parameterizations of the SMARTS spectral code, and is widely recognized as one of the most accurate clear-sky models available.

The model uses a two-band spectral scheme that separately treats the visible band (290-700 nm) and near-infrared band (700-4000 nm). In the visible band, attenuation is dominated by Rayleigh scattering and aerosol extinction, while the NIR band is primarily affected by water vapor absorption. For each band, the model calculates independent broadband transmittances for:

  • Rayleigh scattering: Molecular scattering by air molecules
  • Uniformly mixed gases: Absorption by CO2 and O2
  • Ozone: UV and visible absorption bands
  • Water vapor: Major absorption in the NIR
  • Aerosols: Scattering and absorption characterized by Ångström turbidity coefficients

Direct beam irradiance is computed from the product of these transmittances, while diffuse irradiance uses a two-layer scattering scheme that accounts for aerosol forward scattering and backscattering from the atmosphere-ground system. The model partitions the total radiative flux into direct and diffuse components suitable for agricultural, solar energy, and climate applications.

Spectral Solar Irradiance (SSolar-GOA Model)

For applications requiring high spectral resolution (e.g., photosynthesis, remote sensing), the plugin implements the SSolar-GOA spectral radiative transfer model from Cachorro et al. (2022). This model computes bottom-of-atmosphere spectral irradiance from 300-2600 nm at 1 nm resolution.

The SSolar-GOA model uses the Wehrli (1985) extraterrestrial solar spectrum corrected for Earth-Sun distance, then applies atmospheric transmittances for:

  • Rayleigh scattering: Molecular scattering using Bates (1984) optical depth formula
  • Aerosol extinction: Ångström turbidity law with Ambartsumian solution for scattering
  • Water vapor absorption: Empirical model for bands at 940, 1130, 1380, and 1870 nm
  • Ozone absorption: UV Hartley band (200-310 nm) and visible Chappuis band (400-700 nm)
  • Oxygen absorption: Primarily the A-band at 760 nm

The model also accounts for surface-atmosphere multiple reflections based on surface albedo and atmospheric spherical albedo.

Parameter Derivation: The implementation uses the same atmospheric inputs as the REST-2 model (pressure, temperature, humidity, turbidity). Additional parameters are derived automatically: precipitable water (Viswanadham 1981), ozone column (van Heuklon 1979), Ångström alpha (1.3), surface albedo (0.2), aerosol single scattering albedo (0.90), and asymmetry parameter (0.85).

Output Format: Results are stored in Context global data as vectors of (wavelength, irradiance) pairs (vec2) with user-defined labels. Three spectral components are computed: global irradiance on horizontal surface, direct irradiance normal to sun direction, and diffuse irradiance on horizontal surface (all in W/m²/nm).

Ambient Longwave Flux

The longwave radiation flux emanating from the clear-sky is modeled following Prata (1996).

The model surmounts to calculating the effective emissivity of the sky as a function of precipitable water in the atmosphere

\(\epsilon_s = 1-(1+u)\mathrm{exp}\left(-\left(1.2+3u\right)^{0.5}\right)\),

where \(u\) is the wator vapor path length (cm of precipitable water) which can be estimated following Viswanadham (1981) for example.

The downwelling longwave radiation flux on a horizontal surface is given by

\(R_L=\epsilon_s\sigma T_a^4\),

where \(\sigma=5.67\times10^{-8}\) W/m2-K4, and \(T_a\) is the air temperature in Kelvin measured near the ground (say 2 m height).

Using the SolarPosition Plug-in

Getting the Direction of the Sun

The direction of the sun can be queried in one of several ways: a Cartesian unit vector pointing in the direction of the sun, a spherical coordinate describing the direction of the sun, the elevation angle of the sun, the zenithal angle of the sun, and the azimuthal angle of the sun. The functions to query these quantities are given in the table below. Each of these functions calculates the solar direction based on the current time and date set in the Context (see setTime() "setTime()" and setDate() "setDate()"), and the UTC, latitude, and longitude specified in the SolarPosition constructor.

Direction QuantityFunction
Unit vector pointing toward the sun.getSunDirectionVector()
Spherical coordinate vector pointing toward the sun.getSunDirectionSpherical()
Elevation angle of the sun (radians).getSunElevation()
Zenithal angle of the sun (radians).getSunZenith()
Azimuthal angle of the sun (radians).getSunAzimuth()

Below is an example of how to use the SolarPosition plugin to calculate the sun angle.

from pyhelios import Context, SolarPosition
from pyhelios.types import vec3
with Context() as context:
# Set the current time and date
context.setDate(1, 5, 2015) # May 1, 2015
context.setTime(30, 12) # 12:30
# Initialize the SolarPosition class with coordinates
# Arguments: context, utc_offset, latitude, longitude
with SolarPosition(context, 7, 31.256, 119.947) as sun:
# Get the sun position
direction = sun.getSunDirectionVector() # unit vector
elevation = sun.getSunElevation() # elevation angle (radians)
azimuth = sun.getSunAzimuth() # azimuthal angle (radians)
print(f"Direction: {direction}")
print(f"Elevation: {elevation} radians")
print(f"Azimuth: {azimuth} radians")

Specifying Atmospheric Conditions

The SolarPosition plugin requires atmospheric parameters to calculate solar flux and related quantities. The Python API uses setAtmosphericConditions() to set atmospheric parameters once, then calls parameter-free flux methods. This approach is clean, reduces code repetition, and aligns with the C++ plugin API.

from pyhelios import Context, SolarPosition
import math
with Context() as context:
# Set the current time and date
context.setDate(1, 5, 2015) # May 1, 2015
context.setTime(30, 12) # 12:30
# Initialize the SolarPosition class
with SolarPosition(context, 7, 31.256, 119.947) as sun:
# Set atmospheric conditions once
sun.setAtmosphericConditions(
pressure_Pa=101000, # Atmospheric pressure (Pa)
temperature_K=300, # Temperature (K)
humidity_rel=0.6, # Relative humidity (0-1)
turbidity=0.05 # Turbidity coefficient
)
# Call parameter-free methods (no repetition!)
R = sun.getSolarFlux()
zenith = sun.getSunZenith()
R_horiz = R * math.cos(zenith)
f_diff = sun.getDiffuseFraction()
R_dir = R * (1.0 - f_diff)
print(f"Total flux: {R:.2f} W/m²")
print(f"Horizontal flux: {R_horiz:.2f} W/m²")
print(f"Diffuse fraction: {f_diff:.3f}")
print(f"Direct flux: {R_dir:.2f} W/m²")

Understanding the Turbidity Parameter

The turbidity parameter used in the SolarPosition plugin is Ångström's aerosol turbidity coefficient (β), which represents the aerosol optical depth (AOD) at 500 nm reference wavelength. This parameter quantifies the amount of aerosols (dust, pollution, haze) in the atmosphere that scatter and absorb solar radiation.

Important: This turbidity definition is NOT the same as "Linke turbidity" (TL), which is commonly used in some other solar radiation models. Linke turbidity typically ranges from 2-6, while Ångström turbidity (AOD) typically ranges from 0.02-0.4. The two are related but use different scales.

The turbidity value is used in the Ångström turbidity formula:

\[ \tau_{aerosol}(\lambda) = \beta \left(\frac{\lambda_{ref}}{\lambda}\right)^{\alpha} \]

where β is the turbidity parameter (AOD at 500 nm), λ is wavelength, λref = 500 nm, and α is the Ångström exponent (typically ~1.3).

Guidance for selecting turbidity values:

  • 0.02: Very clear sky (remote/clean atmosphere) - default value
  • 0.03-0.05: Clear sky (typical for rural areas)
  • 0.1: Light haze or light pollution
  • 0.2-0.3: Hazy conditions (urban/polluted atmosphere)
  • >0.4: Very hazy or heavily polluted atmosphere

Higher turbidity values result in:

  • Reduced direct solar radiation
  • Increased fraction of diffuse radiation
  • Whitening of the sky (reduced blue color)
  • Enhanced circumsolar brightening (bright region around the sun)
Atmospheric condition parameters
ParameterDescriptionValidationExample Value
pressure_PaAtmospheric pressure in Pascals (near the ground)Must be > 0101,325 Pa (1 atm)
temperature_KAir temperature in Kelvin (near the ground)Must be > 0300 K (27°C)
humidity_relAir relative humidity (near the ground)Must be 0-10.6 (60%)
turbidityÅngström's aerosol turbidity coefficient (β), which represents the aerosol optical depth (AOD) at 500 nm reference wavelength. Note: This is NOT Linke turbidity, which uses a different scale (typically 2-6). Typical values: 0.02 (very clear sky), 0.05 (clear sky), 0.1 (light haze), 0.2-0.3 (hazy), >0.4 (very hazy/polluted). Higher values indicate more aerosols in the atmosphere, which reduces direct solar flux and increases diffuse fraction.Must be ≥ 00.02 (default clear sky)

Getting the Solar Flux

The solar flux can be calculated using the REST-2 model of Gueymard (2008) using the getSolarFlux() function. IT IS CRITICAL TO NOTE THAT THE CALCULATED FLUX IS FOR A SURFACE PERPENDICULAR TO THE SUN DIRECTION. To get the flux on a horizontal surface, multiply by the cosine of the solar zenith angle.

Methods are available to get the incoming solar radiation flux perpendicular to the direction of the sun 1) for the entire solar spectrum (getSolarFlux()), 2) for the PAR band (getSolarFluxPAR()), and 3) for the NIR band (getSolarFluxNIR()).

The very similar function getDiffuseFraction() calculates the fraction of the total flux that is diffuse. The fraction that is direct is simply one minus the diffuse fraction.

Example code for using these solar flux functions is given below.

from pyhelios import Context, SolarPosition
import math
with Context() as context:
# Set the current time and date
context.setDate(1, 5, 2015) # May 1, 2015
context.setTime(30, 12) # 12:30
# Initialize the SolarPosition class
with SolarPosition(context, 7, 31.256, 119.947) as sun:
# Define atmospheric conditions
pressure_Pa = 101000 # pressure
temperature_K = 300 # temperature
humidity_rel = 0.6 # humidity
turbidity = 0.05 # turbidity
# Get the sun position
zenith = sun.getSunZenith() # zenithal angle (radians)
# Calculate solar flux with atmospheric parameters
R = sun.getSolarFlux(pressure_Pa, temperature_K, humidity_rel, turbidity)
R_horiz = R * math.cos(zenith) # flux on horizontal surface
f_diff = sun.getDiffuseFraction(pressure_Pa, temperature_K, humidity_rel, turbidity)
R_dir = R * (1.0 - f_diff) # direct component of flux (W/m²)

Calibrating the turbidity using weather station (radiometer) data

The predicted solar flux may not perfectly match local predicted solar fluxes due to uncertainty in the local turbidity value. There is a built-in routine to calibrate the turbidity based on measured radiative fluxes.

For the calibration, you must load radiation flux data into a timeseries within the Context. There must be at least one clear-sky day in the timeseries data, and the radiative fluxes must be for the entire solar spectrum in units of W/m2. You can then use the calibrateTurbidityFromTimeseries() method. This method takes one argument, which is a string corresponding to the timeseries variable name containing the radiation flux data.

Incorporating the effects of clouds

The REST2 model for solar fluxes was developed for clear-sky conditions and cannot directly be used when clouds are present. If incident solar radiation data is available (e.g., from a weather station), this can be used to calibrate the model to account for the possible presence of clouds. A simple model is described below for doing so.

Consider \(R_{meas,h}\) to be the measured all-wave incoming solar radiation flux on a horizontal plane (clear or cloudy conditions), and \(R_{clear}\) to be the predicted all-wave incoming solar radiation flux predicted by the REST2 model for clear-sky conditions perpendicular to the direction of the sun. This flux can be projected onto the horizontal plane according to

\[ R_{clear,h} = R_{clear}\mathrm{cos}\,\theta_s. \]

The diffuse fraction can be approximated as

\[ f_{diff} = 1-\frac{R_{meas,h} - R_{clear,h}}{R_{clear,h}}, \]

where it is enforced that \(0\leq f_{diff} \leq 1\). The resulting flux that is output from the model is (flux perpendicular to the sun)

\[ R_{model} = R_{clear}\frac{R_{meas,h}}{R_{clear,h}}. \]

In order to enable flux calibration for cloudy conditions, you must 1) Load timeseries data containing the measured all-wave solar radiation flux. This data must cover the entire period of the simulation. 2) Call enableCloudCalibration(), which requires a string corresponding to the timeseries data label.

Note: Full timeseries data management (loadTabularTimeseriesData, queryTimeseriesData) is currently available in the C++ API only. The Python API supports enableCloudCalibration() and calibrateTurbidityFromTimeseries() methods, but you must manage timeseries data loading yourself (e.g., using pandas).

Below is a simplified Python example showing cloud calibration workflow:

from pyhelios import Context, SolarPosition
import pandas as pd
import math
# Load weather data using pandas
weather_data = pd.read_csv("/path/to/weatherdatafile.txt")
with Context() as context:
# Initialize the SolarPosition class
with SolarPosition(context, 7, 31.256, 119.947) as sun:
# Enable cloud calibration
# Note: This requires Context timeseries support (C++ only currently)
# sun.enableCloudCalibration("R_tot_Wm2")
# Calibrate turbidity (if timeseries loaded in Context)
# turbidity = sun.calibrateTurbidityFromTimeseries("R_tot_Wm2")
# For Python, manually iterate through weather data
for index, row in weather_data.iterrows():
# Set time for this data point
# (parse date/time from row data)
# Extract atmospheric conditions
Tair_K = row['Tair_C'] + 273.15
humidity_rel = row['humidity_rel']
Patm_Pa = row['Patm_Pa']
turbidity = 0.05 # Or use calibrated value
# Set atmospheric conditions for this timestep
sun.setAtmosphericConditions(Patm_Pa, Tair_K, humidity_rel, turbidity)
# Calculate solar flux (no parameter repetition)
R = sun.getSolarFlux()
f_diff = sun.getDiffuseFraction()
R_dir = R * (1.0 - f_diff) # direct component
R_diff = R * f_diff # diffuse component

An example of the above model applied to actual direct-diffuse partitioned radiation data using a shadowband radiometer is shown below. It should be emphasized that the above model is a relatively simple approximation that produces reasonable fluxes, but more accurate predictions are possible and require much more complicated models.

Getting Spectral Solar Irradiance

For applications requiring wavelength-resolved irradiance (e.g., photosynthesis models with wavelength-dependent quantum yield, remote sensing, hyperspectral image simulation), the calculateGlobalSolarSpectrum() method computes high-resolution spectral irradiance using the SSolar-GOA model.

The spectral irradiance methods automatically derive atmospheric parameters (water vapor, ozone column) from standard atmospheric models. Results are stored in Context global data for use by other plugins (e.g., radiation plugin for ray tracing with spectral sources).

Note: The Python API for spectral calculations differs from C++. Atmospheric conditions must be provided via getSolarFlux() and related methods. The spectral calculation methods derive additional parameters automatically.

Example code for calculating spectral irradiance:

from pyhelios import Context, SolarPosition
with Context() as context:
# Set current time, date, and location
context.setDate(16, 7, 2023) # July 16, 2023
context.setTime(12, 0) # Solar noon
# Initialize SolarPosition with location
# Arguments: context, utc_offset, latitude, longitude
with SolarPosition(context, 0, 36.93, 3.33) as sun:
# Calculate global solar spectrum at 1 nm resolution (default)
sun.calculateGlobalSolarSpectrum("clear_sky")
# Or specify a coarser resolution (e.g., 10 nm)
sun.calculateGlobalSolarSpectrum("clear_sky_10nm", 10.0)
# Retrieve spectral data from Context using the same label
global_spectrum = context.getGlobalData("clear_sky")
# Use spectral data (wavelength in nm, irradiance in W/m²/nm)
for point in global_spectrum:
wavelength_nm = point.x
irradiance = point.y # W/m²/nm on horizontal surface
# ... use wavelength-resolved irradiance
# Similarly for direct and diffuse components:
sun.calculateDirectSolarSpectrum("direct_beam")
sun.calculateDiffuseSolarSpectrum("sky_diffuse")

Three separate methods are available for the different spectral components:

Each method accepts an optional resolution parameter (default 1 nm) allowing wavelength downsampling. For example, resolution_nm=10.0 produces 231 wavelengths instead of the native 2301.

Getting the Sky Longwave Flux

The downwelling longwave radiation flux from the sky can be calculated using the getAmbientLongwaveFlux() function. This function is based on the Prata (1996) model and returns the clear-sky downwelling longwave radiation flux on a horizontal surface in W/m2.

from pyhelios import Context, SolarPosition
with Context() as context:
with SolarPosition(context) as sun:
# Set atmospheric conditions (includes temperature and humidity)
sun.setAtmosphericConditions(101325, 288.15, 0.6, 0.05)
# Get longwave flux (uses temperature and humidity from atmospheric conditions)
lw_flux = sun.getAmbientLongwaveFlux()
print(f"Longwave flux: {lw_flux:.2f} W/m²")