2SolarPosition - High-level interface for solar position and radiation calculations
4This module provides a Python interface to the SolarPosition Helios plugin,
5offering comprehensive solar angle calculations, radiation modeling, and
6time-dependent solar functions for atmospheric physics and plant modeling.
11from contextlib
import contextmanager
12from pathlib
import Path
13from typing
import List, Tuple, Optional, Union
14from .wrappers
import USolarPositionWrapper
as solar_wrapper
15from .Context
import Context, check_context_alive
16from .plugins.registry
import get_plugin_registry
17from .exceptions
import HeliosError
18from .assets
import get_asset_manager
19from .wrappers.DataTypes
import Time, Date, vec3, SphericalCoord
21logger = logging.getLogger(__name__)
26_PRAGUE_DATASET =
'lib/prague_sky_model/PragueSkyModelReduced.dat'
27_OZONE_CLIMATOLOGY =
'ozone_climatology/sbuv_total_ozone_climatology.txt'
28_EXTRATERRESTRIAL_SPECTRUM =
'ssolar_goa/wehrli85.txt'
29_ATMOSPHERE_LUT =
'atmosphere_lut/atmosphere_lut.bin'
30_THERMAL_ATMOSPHERE_LUT =
'thermal_atmosphere_lut/thermal_atmosphere_lut.bin'
33_SPECTRAL_MODEL_ASSETS = (_OZONE_CLIMATOLOGY, _EXTRATERRESTRIAL_SPECTRUM, _ATMOSPHERE_LUT)
35_THERMAL_MODEL_ASSETS = (_OZONE_CLIMATOLOGY, _THERMAL_ATMOSPHERE_LUT)
41 Context manager that temporarily changes working directory to where SolarPosition assets are located.
43 SolarPosition C++ code opens its data files (the Prague sky model dataset, the ozone climatology,
44 the extraterrestrial solar spectrum and the atmospheric look-up tables) through hardcoded relative
45 paths such as "plugins/solarposition/lib/prague_sky_model/PragueSkyModelReduced.dat", so they are
46 only resolvable when the process is running from the build directory. This manager temporarily
47 changes to the build directory where the files actually live.
50 required_assets: Paths, relative to the "plugins/solarposition" asset directory, of the data
51 files the enclosed call reads.
54 RuntimeError: If the build directory or a required data file is not found, indicating a
57 asset_manager = get_asset_manager()
58 working_dir = asset_manager._get_helios_build_path()
60 if working_dir
and working_dir.exists():
61 solarposition_assets = working_dir /
'plugins' /
'solarposition'
64 current_dir = Path(__file__).parent
65 packaged_build = current_dir /
'assets' /
'build'
67 if packaged_build.exists():
68 working_dir = packaged_build
69 solarposition_assets = working_dir /
'plugins' /
'solarposition'
72 repo_root = current_dir.parent
73 build_lib_dir = repo_root /
'pyhelios_build' /
'build' /
'lib'
74 working_dir = build_lib_dir.parent
75 solarposition_assets = working_dir /
'plugins' /
'solarposition'
77 if not build_lib_dir.exists():
79 f
"PyHelios build directory not found at {build_lib_dir}. "
80 f
"Run: build_scripts/build_helios --clean"
83 for asset
in required_assets:
84 asset_path = solarposition_assets / asset
85 if not asset_path.exists():
86 if asset == _PRAGUE_DATASET:
88 f
"Prague sky model dataset not found at {asset_path}. "
89 f
"This indicates a build system error. The build script should copy the "
90 f
"~26 MB dataset to this location. "
91 f
"Rebuild with: build_scripts/build_helios --clean"
94 f
"SolarPosition data file not found at {asset_path}. "
95 f
"This indicates a build system error. The build script should copy the "
96 f
"solarposition plugin assets to this location. "
97 f
"Rebuild with: build_scripts/build_helios --clean"
100 original_dir = os.getcwd()
102 os.chdir(working_dir)
103 logger.debug(f
"Changed working directory to {working_dir} for SolarPosition asset access")
106 os.chdir(original_dir)
107 logger.debug(f
"Restored working directory to {original_dir}")
111 """Validate the label and sensor direction shared by the sensor atmosphere methods."""
112 if not isinstance(label, str):
113 raise ValueError(f
"Label must be a string, got {type(label).__name__}")
115 raise ValueError(
"Label cannot be empty")
116 if not isinstance(direction_to_sensor, vec3):
117 raise ValueError(f
"direction_to_sensor must be a vec3, got {type(direction_to_sensor).__name__}")
118 if direction_to_sensor.x == 0.0
and direction_to_sensor.y == 0.0
and direction_to_sensor.z == 0.0:
119 raise ValueError(
"direction_to_sensor must be a non-zero vector")
123 """Exception raised for SolarPosition-specific errors"""
129 High-level interface for solar position calculations and radiation modeling.
131 SolarPosition provides comprehensive solar angle calculations, radiation flux
132 modeling, sunrise/sunset time calculations, and atmospheric turbidity calibration.
133 The plugin automatically uses Context time/date for calculations or can be
134 initialized with explicit coordinates.
136 This class requires the native Helios library built with SolarPosition support.
137 Use context managers for proper resource cleanup.
140 Basic usage with Context coordinates:
141 >>> with Context() as context:
142 ... context.setDate(2023, 6, 21) # Summer solstice
143 ... context.setTime(12, 0) # Solar noon
144 ... with SolarPosition(context) as solar:
145 ... elevation = solar.getSunElevation()
146 ... print(f"Sun elevation: {elevation:.1f}°")
148 Usage with explicit coordinates:
149 >>> with Context() as context:
150 ... # Davis, California coordinates
151 ... with SolarPosition(context, utc_offset=-8, latitude=38.5, longitude=-121.7) as solar:
152 ... azimuth = solar.getSunAzimuth()
153 ... flux = solar.getSolarFlux(101325, 288.15, 0.6, 0.1)
154 ... print(f"Solar flux: {flux:.1f} W/m²")
157 def __init__(self, context: Context, utc_offset: Optional[float] =
None,
158 latitude: Optional[float] =
None, longitude: Optional[float] =
None):
160 Initialize SolarPosition with a Helios context.
163 context: Active Helios Context instance
164 utc_offset: UTC time offset in hours (-14 to +12). Helios counts the offset
165 positive moving West, which inverts the real-world UTC-12..UTC+14
166 span, so the range is asymmetric. If provided with
167 latitude/longitude, creates plugin with explicit coordinates.
168 latitude: Latitude in degrees (-90 to +90). Required if utc_offset provided.
169 longitude: Longitude in degrees (-180 to +180). Required if utc_offset provided.
172 SolarPositionError: If plugin not available in current build
173 ValueError: If coordinate parameters are invalid or incomplete
174 RuntimeError: If plugin initialization fails
177 If coordinates are not provided, the plugin uses Context location settings.
178 Solar calculations depend on Context time/date - use context.setTime() and
179 context.setDate() to set the simulation time before calculations.
182 registry = get_plugin_registry()
183 if not registry.is_plugin_available(
'solarposition'):
185 "SolarPosition not available in current Helios library. "
186 "SolarPosition plugin availability depends on build configuration.\n"
188 "System requirements:\n"
189 " - Platforms: Windows, Linux, macOS\n"
190 " - Dependencies: None\n"
191 " - GPU: Not required\n"
193 "If you're seeing this error, the SolarPosition plugin may not be "
194 "properly compiled into your Helios library. Please rebuild PyHelios:\n"
195 " build_scripts/build_helios --clean"
199 if utc_offset
is not None or latitude
is not None or longitude
is not None:
201 if utc_offset
is None or latitude
is None or longitude
is None:
203 "If specifying coordinates, all three parameters must be provided: "
204 "utc_offset, latitude, longitude"
211 if utc_offset < -14.0
or utc_offset > 12.0:
212 raise ValueError(f
"UTC offset must be between -14 and +12 hours, got: {utc_offset}")
213 if latitude < -90.0
or latitude > 90.0:
214 raise ValueError(f
"Latitude must be between -90 and +90 degrees, got: {latitude}")
215 if longitude < -180.0
or longitude > 180.0:
216 raise ValueError(f
"Longitude must be between -180 and +180 degrees, got: {longitude}")
220 self.
_solar_pos = solar_wrapper.createSolarPositionWithCoordinates(
221 context.getNativePtr(), utc_offset, latitude, longitude
226 self.
_solar_pos = solar_wrapper.createSolarPosition(context.getNativePtr())
232 """Raise if the owning Context has been destroyed (see Context.check_context_alive)."""
233 check_context_alive(getattr(self,
"context",
None),
"SolarPosition")
236 """Context manager entry"""
239 def __exit__(self, exc_type, exc_val, exc_tb):
240 """Context manager exit - cleanup resources"""
241 if hasattr(self,
'_solar_pos')
and self.
_solar_pos:
242 solar_wrapper.destroySolarPosition(self.
_solar_pos)
246 """Destructor to ensure C++ resources freed even without 'with' statement."""
247 if hasattr(self,
'_solar_pos')
and self.
_solar_pos is not None:
249 solar_wrapper.destroySolarPosition(self.
_solar_pos)
251 except Exception
as e:
253 warnings.warn(f
"Error in SolarPosition.__del__: {e}")
257 humidity_rel: float, turbidity: float) ->
None:
259 Set atmospheric conditions for subsequent flux calculations (modern API).
261 This method sets global atmospheric conditions in the Context that are used
262 by parameter-free flux methods (modern API). Once set, you can call getSolarFlux(),
263 getSolarFluxPAR(), etc. without passing atmospheric parameters.
266 pressure_Pa: Atmospheric pressure in Pascals (e.g., 101325 for sea level)
267 temperature_K: Temperature in Kelvin (e.g., 288.15 for 15°C)
268 humidity_rel: Relative humidity as fraction (0.0-1.0). The spectral solar irradiance,
269 sensor atmosphere and thermal atmosphere methods derive the precipitable water
270 from it and require a value greater than 0.
271 turbidity: Ångström's aerosol turbidity coefficient (β), the aerosol optical depth
272 at 1 µm (must be >= 0). The aerosol optical depth at 550 nm is about 2.18 β and
273 that at 500 nm about 2.46 β, so an aerosol optical depth measured at 500 nm
274 corresponds to β of about 0.41 times that value. This is not Linke turbidity.
277 ValueError: If atmospheric parameters are out of valid ranges
278 SolarPositionError: If operation fails
281 This is the modern API pattern. Atmospheric conditions are stored in Context
282 global data and reused by all parameter-free flux methods until changed.
285 >>> # Modern API (set once, use many times)
286 >>> with Context() as context:
287 ... with SolarPosition(context) as solar:
288 ... solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
289 ... flux = solar.getSolarFlux() # No parameters needed
290 ... par = solar.getSolarFluxPAR() # Uses same conditions
291 ... diffuse = solar.getDiffuseFraction() # Uses same conditions
294 if pressure_Pa < 0.0:
295 raise ValueError(f
"Atmospheric pressure must be non-negative, got: {pressure_Pa}")
296 if temperature_K < 0.0:
297 raise ValueError(f
"Temperature must be non-negative, got: {temperature_K}")
298 if humidity_rel < 0.0
or humidity_rel > 1.0:
299 raise ValueError(f
"Relative humidity must be between 0 and 1, got: {humidity_rel}")
301 raise ValueError(f
"Turbidity must be non-negative, got: {turbidity}")
305 solar_wrapper.setAtmosphericConditions(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
306 except Exception
as e:
311 Get currently set atmospheric conditions from Context.
314 Tuple of (pressure_Pa, temperature_K, humidity_rel, turbidity), where turbidity is
315 Ångström's aerosol turbidity coefficient (β), the aerosol optical depth at 1 µm
318 SolarPositionError: If operation fails
321 If atmospheric conditions have not been set via setAtmosphericConditions(),
322 returns default values: (101325 Pa, 300 K, 0.5, 0.02)
325 >>> pressure, temp, humidity, turbidity = solar.getAtmosphericConditions()
326 >>> print(f"Pressure: {pressure} Pa, Temp: {temp} K")
330 return solar_wrapper.getAtmosphericConditions(self.
_solar_pos)
331 except Exception
as e:
337 Get the sun elevation angle in radians.
340 Sun elevation angle in radians (0 = horizon, pi/2 = zenith)
343 SolarPositionError: If calculation fails
347 >>> elevation = solar.getSunElevation()
348 >>> print(f"Sun is {math.degrees(elevation):.1f}° above horizon")
352 return solar_wrapper.getSunElevation(self.
_solar_pos)
353 except Exception
as e:
358 Get the sun zenith angle in radians.
361 Sun zenith angle in radians (0 = zenith, pi/2 = horizon)
364 SolarPositionError: If calculation fails
368 >>> zenith = solar.getSunZenith()
369 >>> print(f"Sun zenith angle: {math.degrees(zenith):.1f}°")
373 return solar_wrapper.getSunZenith(self.
_solar_pos)
374 except Exception
as e:
379 Get the sun azimuth angle in radians.
382 Sun azimuth angle in radians (0 = North, pi/2 = East, pi = South,
386 SolarPositionError: If calculation fails
390 >>> azimuth = solar.getSunAzimuth()
391 >>> print(f"Sun azimuth: {math.degrees(azimuth):.1f}° (compass bearing)")
395 return solar_wrapper.getSunAzimuth(self.
_solar_pos)
396 except Exception
as e:
402 Get the sun direction as a 3D unit vector.
405 vec3 representing the sun direction vector (x, y, z)
408 SolarPositionError: If calculation fails
411 >>> direction = solar.getSunDirectionVector()
412 >>> print(f"Sun direction vector: ({direction.x:.3f}, {direction.y:.3f}, {direction.z:.3f})")
416 direction_list = solar_wrapper.getSunDirectionVector(self.
_solar_pos)
417 return vec3(direction_list[0], direction_list[1], direction_list[2])
418 except Exception
as e:
423 Get the sun direction as spherical coordinates.
426 SphericalCoord with radius=1, elevation and azimuth in radians
429 SolarPositionError: If calculation fails
432 >>> spherical = solar.getSunDirectionSpherical()
433 >>> print(f"Spherical: r={spherical.radius}, elev={spherical.elevation:.3f}, az={spherical.azimuth:.3f}")
437 spherical_list = solar_wrapper.getSunDirectionSpherical(self.
_solar_pos)
439 radius=spherical_list[0],
440 elevation=spherical_list[1],
441 azimuth=spherical_list[2]
443 except Exception
as e:
448 Override the computed solar position with a prescribed sun direction.
450 By default the sun position is computed from the date, time and location
451 set in the Context. Calling this method overrides that calculation, so
452 all subsequent sun queries (elevation, zenith, azimuth, direction
453 vectors) and flux calculations use the prescribed direction instead.
456 sundirection: SphericalCoord giving the direction of the sun.
457 Elevation and azimuth are in radians.
460 ValueError: If sundirection is not a SphericalCoord
461 SolarPositionError: If the override fails
464 >>> from pyhelios.types import SphericalCoord
466 >>> solar.setSunDirection(SphericalCoord(1.0, math.radians(45), math.radians(180)))
467 >>> math.degrees(solar.getSunElevation())
470 if not isinstance(sundirection, SphericalCoord):
472 f
"Sun direction must be a SphericalCoord, got {type(sundirection).__name__}"
477 solar_wrapper.setSunDirection(
480 sundirection.elevation,
481 sundirection.azimuth,
483 except Exception
as e:
487 def getSolarFlux(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
488 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
490 Calculate total solar flux (supports legacy and modern APIs).
492 This method supports both legacy and modern APIs:
493 - **Legacy API**: Pass all 4 atmospheric parameters explicitly
494 - **Modern API**: Pass no parameters, uses atmospheric conditions from setAtmosphericConditions()
497 pressure_Pa: Atmospheric pressure in Pascals (e.g., 101325 for sea level) [optional]
498 temperature_K: Temperature in Kelvin (e.g., 288.15 for 15°C) [optional]
499 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
500 turbidity: Ångström's aerosol turbidity coefficient (β), the aerosol optical depth at 1 µm [optional]
503 Total solar flux in W/m²
506 ValueError: If some parameters provided but not all, or if values are invalid
507 SolarPositionError: If calculation fails or atmospheric conditions not set (modern API)
510 The model uses the total column ozone of getOzoneColumn(). Where the ozone climatology
511 has no data (high-latitude winter months, or poleward of 80 degrees) the calculation
512 fails until the ozone column is set with setOzoneColumn(). The same applies to
513 getSolarFluxPAR(), getSolarFluxNIR() and getDiffuseFraction().
516 Legacy API (backward compatible):
517 >>> flux = solar.getSolarFlux(101325, 288.15, 0.6, 0.1)
519 Modern API (cleaner, reuses atmospheric state):
520 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
521 >>> flux = solar.getSolarFlux() # No parameters needed
524 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
525 humidity_rel
is not None, turbidity
is not None]
527 if all(params_provided):
532 return solar_wrapper.getSolarFlux(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
533 except Exception
as e:
536 elif not any(params_provided):
541 return solar_wrapper.getSolarFluxFromState(self.
_solar_pos)
542 except Exception
as e:
544 f
"Failed to calculate solar flux from atmospheric state: {e}\n"
545 "Hint: Call setAtmosphericConditions() first to use parameter-free API, "
546 "or provide all 4 atmospheric parameters for legacy API."
552 "Either provide all atmospheric parameters (pressure_Pa, temperature_K, humidity_rel, turbidity) "
553 "or provide none to use atmospheric conditions from setAtmosphericConditions(). "
554 "Partial parameter sets are not supported."
557 def getSolarFluxPAR(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
558 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
560 Calculate PAR (Photosynthetically Active Radiation) solar flux.
562 Supports both legacy (parameter-based) and modern (state-based) APIs.
565 pressure_Pa: Atmospheric pressure in Pascals [optional]
566 temperature_K: Temperature in Kelvin [optional]
567 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
568 turbidity: Ångström's aerosol turbidity coefficient (β), the aerosol optical depth at 1 µm [optional]
571 PAR solar flux in W/m² (wavelength range ~400-700 nm)
574 ValueError: If some parameters provided but not all
575 SolarPositionError: If calculation fails
578 Legacy: par_flux = solar.getSolarFluxPAR(101325, 288.15, 0.6, 0.1)
579 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
580 par_flux = solar.getSolarFluxPAR()
582 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
583 humidity_rel
is not None, turbidity
is not None]
585 if all(params_provided):
589 return solar_wrapper.getSolarFluxPAR(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
590 except Exception
as e:
592 elif not any(params_provided):
596 return solar_wrapper.getSolarFluxPARFromState(self.
_solar_pos)
597 except Exception
as e:
599 f
"Failed to calculate PAR flux from atmospheric state: {e}\n"
600 "Hint: Call setAtmosphericConditions() first."
603 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
605 def getSolarFluxNIR(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
606 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
608 Calculate NIR (Near-Infrared) solar flux.
610 Supports both legacy (parameter-based) and modern (state-based) APIs.
613 pressure_Pa: Atmospheric pressure in Pascals [optional]
614 temperature_K: Temperature in Kelvin [optional]
615 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
616 turbidity: Ångström's aerosol turbidity coefficient (β), the aerosol optical depth at 1 µm [optional]
619 NIR solar flux in W/m² (wavelength range >700 nm)
622 ValueError: If some parameters provided but not all
623 SolarPositionError: If calculation fails
626 Legacy: nir_flux = solar.getSolarFluxNIR(101325, 288.15, 0.6, 0.1)
627 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
628 nir_flux = solar.getSolarFluxNIR()
630 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
631 humidity_rel
is not None, turbidity
is not None]
633 if all(params_provided):
637 return solar_wrapper.getSolarFluxNIR(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
638 except Exception
as e:
640 elif not any(params_provided):
644 return solar_wrapper.getSolarFluxNIRFromState(self.
_solar_pos)
645 except Exception
as e:
647 f
"Failed to calculate NIR flux from atmospheric state: {e}\n"
648 "Hint: Call setAtmosphericConditions() first."
651 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
653 def getDiffuseFraction(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
654 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
656 Calculate the diffuse fraction of solar radiation.
658 Supports both legacy (parameter-based) and modern (state-based) APIs.
661 pressure_Pa: Atmospheric pressure in Pascals [optional]
662 temperature_K: Temperature in Kelvin [optional]
663 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
664 turbidity: Ångström's aerosol turbidity coefficient (β), the aerosol optical depth at 1 µm [optional]
667 Diffuse fraction as ratio (0.0-1.0) where:
668 - 0.0 = all direct radiation
669 - 1.0 = all diffuse radiation
672 ValueError: If some parameters provided but not all
673 SolarPositionError: If calculation fails
676 Legacy: diffuse = solar.getDiffuseFraction(101325, 288.15, 0.6, 0.1)
677 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
678 diffuse = solar.getDiffuseFraction()
680 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
681 humidity_rel
is not None, turbidity
is not None]
683 if all(params_provided):
687 return solar_wrapper.getDiffuseFraction(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
688 except Exception
as e:
690 elif not any(params_provided):
694 return solar_wrapper.getDiffuseFractionFromState(self.
_solar_pos)
695 except Exception
as e:
697 f
"Failed to calculate diffuse fraction from atmospheric state: {e}\n"
698 "Hint: Call setAtmosphericConditions() first."
701 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
704 humidity_rel: Optional[float] =
None) -> float:
706 Calculate the ambient (sky) longwave radiation flux.
708 This method supports both legacy and modern APIs:
709 - **Legacy API**: Pass temperature and humidity explicitly
710 - **Modern API**: Pass no parameters, uses atmospheric conditions from setAtmosphericConditions()
713 temperature_K: Temperature in Kelvin [optional]
714 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
717 Ambient longwave flux in W/m²
720 ValueError: If one parameter provided but not the other
721 SolarPositionError: If calculation fails
724 The longwave flux model is based on Prata (1996).
725 Returns downwelling longwave radiation flux on a horizontal surface, integrated over
726 all longwave wavelengths. It is the diffuse flux to give an emission band without
727 wavelength bounds. For the sky flux within a thermal band with wavelength bounds,
728 use getThermalSkyFlux().
732 >>> lw_flux = solar.getAmbientLongwaveFlux(288.15, 0.6)
734 Modern API (uses temperature and humidity from setAtmosphericConditions):
735 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
736 >>> lw_flux = solar.getAmbientLongwaveFlux()
738 params_provided = [temperature_K
is not None, humidity_rel
is not None]
740 if all(params_provided):
747 saved_conditions = solar_wrapper.getAtmosphericConditions(self.
_solar_pos)
751 solar_wrapper.setAtmosphericConditions(self.
_solar_pos,
758 result = solar_wrapper.getAmbientLongwaveFluxFromState(self.
_solar_pos)
761 solar_wrapper.setAtmosphericConditions(self.
_solar_pos, *saved_conditions)
765 except Exception
as e:
768 elif not any(params_provided):
772 return solar_wrapper.getAmbientLongwaveFluxFromState(self.
_solar_pos)
773 except Exception
as e:
775 f
"Failed to calculate ambient longwave flux from atmospheric state: {e}\n"
776 "Hint: Call setAtmosphericConditions() first to use parameter-free API, "
777 "or provide temperature_K and humidity_rel for legacy API."
783 "Either provide both temperature_K and humidity_rel, "
784 "or provide neither to use atmospheric conditions from setAtmosphericConditions()."
790 Calculate sunrise time for the current date and location.
793 Time object with sunrise time (hour, minute, second)
796 SolarPositionError: If calculation fails
799 >>> sunrise = solar.getSunriseTime()
800 >>> print(f"Sunrise: {sunrise}") # Prints as HH:MM:SS
804 hour, minute, second = solar_wrapper.getSunriseTime(self.
_solar_pos)
805 return Time(hour, minute, second)
806 except Exception
as e:
811 Calculate sunset time for the current date and location.
814 Time object with sunset time (hour, minute, second)
817 SolarPositionError: If calculation fails
820 >>> sunset = solar.getSunsetTime()
821 >>> print(f"Sunset: {sunset}") # Prints as HH:MM:SS
825 hour, minute, second = solar_wrapper.getSunsetTime(self.
_solar_pos)
826 return Time(hour, minute, second)
827 except Exception
as e:
833 Calibrate atmospheric turbidity using timeseries data.
836 timeseries_label: Label of timeseries data in Context. The data should
837 be global shortwave radiation flux on a horizontal plane in W/m^2,
838 and should contain at least one day of clear-sky conditions.
841 The calibrated turbidity value
844 ValueError: If timeseries label is invalid
845 SolarPositionError: If calibration fails
848 This method does not itself apply the calibrated value. Pass the
849 returned turbidity to setAtmosphericConditions() to use it.
852 >>> turbidity = solar.calibrateTurbidityFromTimeseries("solar_irradiance")
853 >>> solar.setAtmosphericConditions(101325, 293.15, 0.5, turbidity)
855 if not timeseries_label:
856 raise ValueError(
"Timeseries label cannot be empty")
861 return solar_wrapper.calibrateTurbidityFromTimeseries(self.
_solar_pos, timeseries_label)
862 except Exception
as e:
867 Enable cloud calibration using timeseries data.
870 timeseries_label: Label of cloud timeseries data in Context
873 ValueError: If timeseries label is invalid
874 SolarPositionError: If calibration setup fails
877 >>> solar.enableCloudCalibration("cloud_cover")
879 if not timeseries_label:
880 raise ValueError(
"Timeseries label cannot be empty")
884 solar_wrapper.enableCloudCalibration(self.
_solar_pos, timeseries_label)
885 except Exception
as e:
890 Disable cloud calibration.
893 SolarPositionError: If operation fails
896 >>> solar.disableCloudCalibration()
900 solar_wrapper.disableCloudCalibration(self.
_solar_pos)
901 except Exception
as e:
907 Enable Prague Sky Model for physically-based sky radiance calculations.
909 The Prague Sky Model provides high-quality spectral and angular sky radiance
910 distribution for accurate diffuse radiation modeling. It accounts for Rayleigh
911 and Mie scattering to produce realistic sky radiance patterns across the
912 360-1480 nm spectral range.
915 SolarPositionError: If operation fails
918 After enabling, call updatePragueSkyModel() to compute and store spectral-angular
919 parameters in Context global data. Requires ~27 MB data file:
920 plugins/solarposition/lib/prague_sky_model/PragueSkyModelReduced.dat
923 >>> with Context() as context:
924 ... with SolarPosition(context) as solar:
925 ... solar.enablePragueSkyModel()
926 ... solar.updatePragueSkyModel()
931 solar_wrapper.enablePragueSkyModel(self.
_solar_pos)
932 except Exception
as e:
937 Check if Prague Sky Model is currently enabled.
940 True if Prague Sky Model has been enabled via enablePragueSkyModel(), False otherwise
943 SolarPositionError: If operation fails
946 >>> if solar.isPragueSkyModelEnabled():
947 ... print("Prague Sky Model is active")
951 return solar_wrapper.isPragueSkyModelEnabled(self.
_solar_pos)
952 except Exception
as e:
957 Update Prague Sky Model and store spectral-angular parameters in Context.
959 This is a computationally intensive operation (~1100 model queries with OpenMP
960 parallelization) that computes sky radiance distribution for current atmospheric
961 and solar conditions. Use pragueSkyModelNeedsUpdate() for lazy evaluation to
962 avoid unnecessary updates.
965 ground_albedo: Ground surface albedo (default: 0.33 for typical soil/vegetation)
968 SolarPositionError: If update fails
971 Reads turbidity (Ångström's β, the aerosol optical depth at 1 µm) from Context
972 atmospheric conditions and converts the aerosol optical depth at 550 nm to the
973 visibility that parameterizes the Prague model, limited to its range of
974 20-131.8 km: turbidities below about 0.029 (including the default of 0.02) use the
975 cleanest Prague atmosphere and those above about 0.21 the haziest. Stores results in Context
976 global data as "prague_sky_spectral_params" (1350 floats: 225 wavelengths × 6 params),
977 "prague_sky_sun_direction", "prague_sky_visibility_km", "prague_sky_ground_albedo",
978 and "prague_sky_valid" flag.
981 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
982 >>> solar.updatePragueSkyModel(ground_albedo=0.25)
987 solar_wrapper.updatePragueSkyModel(self.
_solar_pos, ground_albedo)
988 except Exception
as e:
992 sun_tolerance: float = 0.01,
993 turbidity_tolerance: float = 0.02,
994 albedo_tolerance: float = 0.05) -> bool:
996 Check if Prague Sky Model needs updating based on changed conditions.
998 Enables lazy evaluation to avoid expensive Prague updates when conditions haven't
999 changed significantly. Compares current state against cached values.
1002 ground_albedo: Current ground albedo (default: 0.33)
1003 sun_tolerance: Threshold for sun direction changes (default: 0.01 ≈ 0.57°)
1004 turbidity_tolerance: Relative threshold for turbidity (default: 0.02 = 2%)
1005 albedo_tolerance: Threshold for albedo changes (default: 0.05 = 5%)
1008 True if updatePragueSkyModel() should be called, False if cached data is valid
1011 SolarPositionError: If check fails
1014 Reads turbidity from Context atmospheric conditions for comparison.
1017 >>> if solar.pragueSkyModelNeedsUpdate():
1018 ... solar.updatePragueSkyModel()
1022 return solar_wrapper.pragueSkyModelNeedsUpdate(self.
_solar_pos, ground_albedo,
1023 sun_tolerance, turbidity_tolerance,
1025 except Exception
as e:
1031 Calculate direct beam solar spectrum.
1033 Computes the spectral irradiance of the direct solar beam on a surface normal to
1034 the sun direction from 300 to 2600 nm, with a spectral model that follows SSolar-GOA
1035 (Cachorro et al. 2022). Results are stored in Context global data as a vector of
1036 (wavelength, irradiance) pairs.
1039 label: Label to store the spectrum data in Context global data
1040 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
1041 Lower values give finer spectral resolution but require
1042 more computation. Default is 1.0 nm.
1045 ValueError: If label is empty or resolution is out of valid range
1046 SolarPositionError: If calculation fails
1049 - Requires Context time/date to be set for accurate solar position
1050 - Atmospheric conditions are taken from setAtmosphericConditions(), the ozone column
1051 from getOzoneColumn() and the ground albedo from setGroundAlbedo(); cloud
1052 calibration is applied if enabled with enableCloudCalibration()
1053 - The spectrum is referred to by its label, e.g. in RadiationModel.setSourceSpectrum()
1054 or RadiationModel.setDiffuseSpectrum(); context.getGlobalDataSize(label) gives its
1055 number of wavelengths
1056 - Fails if the sun is at or below the horizon, if the relative humidity is not
1057 greater than zero, or if no ozone column was set and the climatology has no data
1058 for the location and date (see getOzoneColumn())
1061 >>> with Context() as context:
1062 ... context.setDate(2023, 6, 21)
1063 ... context.setTime(12, 0)
1064 ... with SolarPosition(context) as solar:
1065 ... solar.calculateDirectSolarSpectrum("direct_spectrum", resolution_nm=5.0)
1066 ... n_wavelengths = context.getGlobalDataSize("direct_spectrum") # 461
1069 raise ValueError(
"Label cannot be empty")
1070 if resolution_nm < 1.0
or resolution_nm > 2300.0:
1071 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
1076 solar_wrapper.calculateDirectSolarSpectrum(self.
_solar_pos, label, resolution_nm)
1077 except Exception
as e:
1082 Calculate diffuse solar spectrum.
1084 Computes the diffuse (sky) spectral irradiance on a horizontal surface from 300 to
1085 2600 nm, including light reflected by the ground and scattered back down by the
1086 atmosphere, with a spectral model that follows SSolar-GOA (Cachorro et al. 2022).
1087 Results are stored in Context global data as a vector of (wavelength, irradiance) pairs.
1090 label: Label to store the spectrum data in Context global data
1091 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
1092 Lower values give finer spectral resolution but require
1093 more computation. Default is 1.0 nm.
1096 ValueError: If label is empty or resolution is out of valid range
1097 SolarPositionError: If calculation fails
1100 - Requires Context time/date to be set for accurate solar position
1101 - Atmospheric conditions are taken from setAtmosphericConditions(), the ozone column
1102 from getOzoneColumn() and the ground albedo from setGroundAlbedo(); cloud
1103 calibration is applied if enabled with enableCloudCalibration()
1104 - The spectrum is referred to by its label, e.g. in RadiationModel.setSourceSpectrum()
1105 or RadiationModel.setDiffuseSpectrum(); context.getGlobalDataSize(label) gives its
1106 number of wavelengths
1107 - Fails if the sun is at or below the horizon, if the relative humidity is not
1108 greater than zero, or if no ozone column was set and the climatology has no data
1109 for the location and date (see getOzoneColumn())
1110 - Diffuse radiation results from atmospheric scattering (Rayleigh, aerosol)
1113 >>> with Context() as context:
1114 ... context.setDate(2023, 6, 21)
1115 ... context.setTime(12, 0)
1116 ... with SolarPosition(context) as solar:
1117 ... solar.calculateDiffuseSolarSpectrum("diffuse_spectrum", resolution_nm=5.0)
1118 ... n_wavelengths = context.getGlobalDataSize("diffuse_spectrum") # 461
1121 raise ValueError(
"Label cannot be empty")
1122 if resolution_nm < 1.0
or resolution_nm > 2300.0:
1123 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
1128 solar_wrapper.calculateDiffuseSolarSpectrum(self.
_solar_pos, label, resolution_nm)
1129 except Exception
as e:
1134 Calculate global (total) solar spectrum.
1136 Computes the global (direct plus diffuse) spectral irradiance on a horizontal surface
1137 from 300 to 2600 nm, with a spectral model that follows SSolar-GOA (Cachorro et al. 2022).
1138 Results are stored in Context global data as a vector of (wavelength, irradiance) pairs.
1141 label: Label to store the spectrum data in Context global data
1142 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
1143 Lower values give finer spectral resolution but require
1144 more computation. Default is 1.0 nm.
1147 ValueError: If label is empty or resolution is out of valid range
1148 SolarPositionError: If calculation fails
1151 - Requires Context time/date to be set for accurate solar position
1152 - Atmospheric conditions are taken from setAtmosphericConditions(), the ozone column
1153 from getOzoneColumn() and the ground albedo from setGroundAlbedo(); cloud
1154 calibration is applied if enabled with enableCloudCalibration()
1155 - The spectrum is referred to by its label, e.g. in RadiationModel.setSourceSpectrum()
1156 or RadiationModel.setDiffuseSpectrum(); context.getGlobalDataSize(label) gives its
1157 number of wavelengths
1158 - Fails if the sun is at or below the horizon, if the relative humidity is not
1159 greater than zero, or if no ozone column was set and the climatology has no data
1160 for the location and date (see getOzoneColumn())
1161 - Global spectrum = direct beam + diffuse (sky) radiation
1162 - Most useful for plant canopy modeling and photosynthesis calculations
1165 >>> with Context() as context:
1166 ... context.setDate(2023, 6, 21)
1167 ... context.setTime(12, 0)
1168 ... with SolarPosition(context) as solar:
1169 ... solar.calculateGlobalSolarSpectrum("global_spectrum", resolution_nm=10.0)
1170 ... n_wavelengths = context.getGlobalDataSize("global_spectrum") # 231
1173 raise ValueError(
"Label cannot be empty")
1174 if resolution_nm < 1.0
or resolution_nm > 2300.0:
1175 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
1180 solar_wrapper.calculateGlobalSolarSpectrum(self.
_solar_pos, label, resolution_nm)
1181 except Exception
as e:
1187 Set the total column ozone, overriding the climatological value.
1189 The ozone column is used by the broadband solar flux model (getSolarFlux() family), the
1190 spectral solar irradiance model and the sensor atmosphere models. It is stored in Context
1191 global data as "atmosphere_ozone_DU".
1194 ozone_DU: Total column ozone in Dobson units (must be > 0)
1197 ValueError: If ozone_DU is not a positive number
1198 SolarPositionError: If operation fails
1201 >>> solar.setOzoneColumn(310.0) # e.g., from a local measurement
1203 if isinstance(ozone_DU, bool)
or not isinstance(ozone_DU, (int, float)):
1204 raise ValueError(f
"Ozone column must be a number, got {type(ozone_DU).__name__}")
1205 if not ozone_DU > 0.0:
1206 raise ValueError(f
"Ozone column must be positive, got: {ozone_DU}")
1210 solar_wrapper.setOzoneColumn(self.
_solar_pos, ozone_DU)
1211 except Exception
as e:
1216 Get the total column ozone used by the solar radiation models.
1218 Returns the ozone column set with setOzoneColumn(), if any. Otherwise returns the monthly
1219 zonal-mean climatology of the NASA SBUV Merged Ozone Data Set averaged over 2005-2024,
1220 interpolated linearly in latitude between the centers of its 5-degree latitude bands and in
1221 the day of the year between the middles of the months.
1224 Total column ozone in Dobson units
1227 SolarPositionError: If no ozone column was set and the climatology has no data for the
1228 location and date. The climatology has no data in high-latitude winter months or
1229 poleward of 80 degrees; set the ozone column with setOzoneColumn() there.
1232 Next to a latitude band or month without data, the value of the band or month containing
1233 the location or date is used rather than interpolating toward the missing one.
1236 >>> ozone_DU = solar.getOzoneColumn()
1241 return solar_wrapper.getOzoneColumn(self.
_solar_pos)
1242 except Exception
as e:
1247 Set the albedo of the ground surrounding the scene.
1249 The ground albedo sets how much light is reflected back and forth between the ground and
1250 the atmosphere, which increases the global and diffuse irradiance, most strongly at short
1251 wavelengths. It is stored in Context global data as "atmosphere_ground_albedo".
1254 albedo: Broadband ground albedo (must be between 0 and 1)
1257 ValueError: If albedo is not a number between 0 and 1
1258 SolarPositionError: If operation fails
1261 The ground albedo affects the global and diffuse spectra of calculateGlobalSolarSpectrum()
1262 and calculateDiffuseSolarSpectrum(), and the ground irradiance and adjacency radiance of
1263 calculateSensorAtmosphereSpectra(); without cloud calibration it does not affect the direct
1264 beam. It does not affect the broadband getSolarFlux() family of methods, and the Prague sky
1265 model takes its ground albedo as an argument of updatePragueSkyModel().
1268 >>> solar.setGroundAlbedo(0.8) # e.g., fresh snow
1270 if isinstance(albedo, bool)
or not isinstance(albedo, (int, float)):
1271 raise ValueError(f
"Ground albedo must be a number, got {type(albedo).__name__}")
1272 if not 0.0 <= albedo <= 1.0:
1273 raise ValueError(f
"Ground albedo must be between 0 and 1, got: {albedo}")
1277 solar_wrapper.setGroundAlbedo(self.
_solar_pos, albedo)
1278 except Exception
as e:
1283 Get the albedo of the ground surrounding the scene.
1286 Ground albedo set by setGroundAlbedo(), or 0.2 if it has not been set
1289 SolarPositionError: If operation fails
1292 >>> albedo = solar.getGroundAlbedo()
1297 except Exception
as e:
1302 resolution_nm: float = 1.0) ->
None:
1304 Calculate the spectral atmospheric quantities needed to simulate imagery from a sensor
1305 above the atmosphere (e.g., a satellite).
1307 Radiance reaching the sensor is modeled as L_sensor = L_path + T_dir_up * L_surface + L_adj,
1308 where L_surface is the radiance leaving the scene. The atmosphere is described by a look-up
1309 table computed with the 6S radiative transfer code (continental aerosol), which gives both
1310 the upward path to the sensor and the downward solar illumination of the scene. For the two
1311 to be consistent, illuminate the scene with the "_direct_irradiance" and
1312 "_diffuse_irradiance" spectra stored by this method rather than those of
1313 calculateDirectSolarSpectrum() and calculateDiffuseSolarSpectrum().
1315 The following spectra are stored in Context global data as vectors of vec2
1316 (wavelength_nm, value), each labeled with the given label followed by a suffix:
1318 - "_path_radiance": radiance scattered by the atmosphere into the sensor without reaching
1319 the ground (W/m²/sr/nm)
1320 - "_adjacency_radiance": radiance reflected by the surrounding ground (albedo from
1321 setGroundAlbedo()) and scattered by the atmosphere into the sensor (W/m²/sr/nm)
1322 - "_upward_direct_transmittance": transmittance of the surface-leaving radiance straight to
1323 the sensor, including gas absorption
1324 - "_upward_diffuse_transmittance": transmittance of surface-leaving radiance scattered into
1325 the sensor direction, including gas absorption
1326 - "_spherical_albedo": spherical albedo of the atmosphere
1327 - "_direct_irradiance": direct solar irradiance at the ground, normal to the sun direction
1329 - "_diffuse_irradiance": diffuse solar irradiance at the ground on a horizontal surface
1331 - "_global_irradiance": global solar irradiance at the ground on a horizontal surface
1334 The unit vector toward the sensor is also stored, as vec3 global data with the suffix
1335 "_direction_to_sensor".
1338 label: Prefix of the labels under which the spectra are stored in Context global data
1339 direction_to_sensor: Vector pointing from the scene toward the sensor (need not be
1340 normalized). Its zenith angle must not exceed 60 degrees.
1341 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0). Default is 1.0 nm.
1344 ValueError: If label is empty, direction_to_sensor is not a non-zero vec3, or
1345 resolution is out of valid range
1346 SolarPositionError: If calculation fails, including for conditions outside the range of
1350 - Atmospheric conditions are taken from setAtmosphericConditions(), the ozone column
1351 from getOzoneColumn() and the ground albedo from setGroundAlbedo(). The turbidity is
1352 converted to aerosol optical depth at 550 nm with an Ångström exponent of 1.3.
1353 - The look-up table covers solar zenith angles up to 70 degrees, view zenith angles up
1354 to 60 degrees, aerosol optical depths at 550 nm up to 1.2 (turbidity up to about 0.55)
1355 and surface pressures from 701.2 to 1013 hPa (ground elevations up to about 3 km);
1356 pressures up to 1050 hPa are extrapolated linearly. Its gas tables cover water vapor
1357 paths up to 320 g/cm². Conditions outside these ranges, and a sun below the horizon,
1359 - The sensor is taken to be above the atmosphere. The model assumes clear sky and
1360 cannot be used with cloud calibration enabled.
1361 - RadiationModel.enableCameraAtmosphere() applies these spectra to a camera's images.
1364 >>> solar.setAtmosphericConditions(101325, 298, 0.5, 0.1)
1365 >>> solar.setGroundAlbedo(0.15)
1366 >>> solar.calculateSensorAtmosphereSpectra("satellite", vec3(0, 0, 1)) # sensor at nadir
1367 >>> radiation.setSourceSpectrum(sun_source, "satellite_direct_irradiance")
1368 >>> radiation.setDiffuseSpectrum("red", "satellite_diffuse_irradiance")
1371 if isinstance(resolution_nm, bool)
or not isinstance(resolution_nm, (int, float)):
1372 raise ValueError(f
"Wavelength resolution must be a number, got {type(resolution_nm).__name__}")
1373 if resolution_nm < 1.0
or resolution_nm > 2300.0:
1374 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
1379 solar_wrapper.calculateSensorAtmosphereSpectra(self.
_solar_pos, label,
1380 direction_to_sensor.to_list(), resolution_nm)
1381 except Exception
as e:
1386 Calculate the thermal infrared atmospheric quantities needed to simulate thermal imagery
1387 from a sensor above the atmosphere (e.g., a satellite).
1389 Radiance reaching the sensor at a thermal wavelength is modeled as
1390 L_sensor = tau * L_surface + L_up, where L_surface is the radiance leaving the scene
1391 (emitted plus reflected sky radiance), tau the atmospheric transmittance and L_up the
1392 radiance emitted upward by the atmosphere. These are interpolated from a look-up table
1393 computed with the libRadtran radiative transfer code (clear sky without aerosol).
1395 The following spectra are stored in Context global data as vectors of vec2
1396 (wavelength of the bin center in nm, value), with 234 points from 5502 to 15326 nm, each
1397 labeled with the given label followed by a suffix:
1399 - "_thermal_transmittance": atmospheric transmittance from the ground to the sensor
1400 - "_thermal_upwelling_radiance": radiance emitted by the atmosphere toward the sensor
1402 - "_thermal_downwelling_radiance": sky irradiance on a horizontal surface at the ground
1403 divided by pi (W/m²/sr/nm), independent of the view direction
1405 The column water vapor (float, g/cm²) is stored with the suffix "_thermal_water_vapor_cm",
1406 and the unit vector toward the sensor, as vec3, with the suffix "_direction_to_sensor".
1409 label: Prefix of the labels under which the results are stored in Context global data
1410 direction_to_sensor: Vector pointing from the scene toward the sensor (need not be
1411 normalized). Its zenith angle must not exceed 60 degrees.
1414 ValueError: If label is empty or direction_to_sensor is not a non-zero vec3
1415 SolarPositionError: If calculation fails, including for conditions outside the range of
1419 - The atmospheric profile is built from the surface air temperature, humidity and
1420 pressure set with setAtmosphericConditions() and the ozone column of
1421 getOzoneColumn(). The column water vapor, derived from the temperature and humidity,
1422 must lie within 0.1-7.5 g/cm², the pressure within 701.2-1050 hPa (pressures above
1423 1013 hPa are extrapolated) and the ozone column within 150-450 DU. The supported air
1424 temperatures depend on the pressure: 246.2-321.7 K at 1013 hPa.
1425 - The model assumes clear sky and does not depend on the position of the sun, so
1426 night-time imagery can be simulated. Reflected sunlight, which is significant below
1427 about 5 µm, is not included.
1430 >>> solar.setAtmosphericConditions(101325, 298, 0.5, 0.1)
1431 >>> solar.calculateSensorThermalAtmosphere("satellite", vec3(0, 0, 1)) # sensor at nadir
1432 >>> context.getGlobalDataSize("satellite_thermal_transmittance")
1440 solar_wrapper.calculateSensorThermalAtmosphere(self.
_solar_pos, label,
1441 direction_to_sensor.to_list())
1442 except Exception
as e:
1445 def getThermalSkyFlux(self, wavelength_min_nm: float, wavelength_max_nm: float) -> float:
1447 Calculate the sky longwave irradiance on a horizontal surface within a thermal band.
1449 This is the band-specific counterpart of getAmbientLongwaveFlux(): the downward irradiance
1450 emitted by a clear-sky atmosphere between two wavelengths, integrated from the downwelling
1451 radiance of the thermal atmosphere look-up table (see calculateSensorThermalAtmosphere()).
1452 It is the diffuse flux to give an emission band with the same wavelength bounds
1453 (RadiationModel.setDiffuseRadiationFlux()), so that the sky radiance the scene reflects is
1454 consistent with the thermal sensor atmosphere.
1457 wavelength_min_nm: Lower wavelength bound of the band in nm
1458 wavelength_max_nm: Upper wavelength bound of the band in nm
1461 Sky longwave irradiance within the band on a horizontal surface, in W/m²
1464 ValueError: If the bounds are not numbers satisfying 0 < wavelength_min_nm < wavelength_max_nm
1465 SolarPositionError: If calculation fails, including for bounds outside 5502-15326 nm or
1466 atmospheric conditions outside the ranges given for calculateSensorThermalAtmosphere()
1469 >>> radiation.addRadiationBand("TIR", 10600, 11190) # e.g., Landsat 8 TIRS band 10
1470 >>> radiation.setDiffuseRadiationFlux("TIR", solar.getThermalSkyFlux(10600, 11190))
1472 for name, value
in ((
"wavelength_min_nm", wavelength_min_nm), (
"wavelength_max_nm", wavelength_max_nm)):
1473 if isinstance(value, bool)
or not isinstance(value, (int, float)):
1474 raise ValueError(f
"{name} must be a number, got {type(value).__name__}")
1475 if wavelength_min_nm <= 0.0
or wavelength_max_nm <= wavelength_min_nm:
1477 f
"Wavelength bounds must satisfy 0 < wavelength_min_nm < wavelength_max_nm, "
1478 f
"got: ({wavelength_min_nm}, {wavelength_max_nm})")
1483 return solar_wrapper.getThermalSkyFlux(self.
_solar_pos, wavelength_min_nm, wavelength_max_nm)
1484 except Exception
as e:
1489 Check if SolarPosition is available in current build.
1492 True if plugin is available, False otherwise
1494 registry = get_plugin_registry()
1495 return registry.is_plugin_available(
'solarposition')
1500 latitude: Optional[float] =
None, longitude: Optional[float] =
None) -> SolarPosition:
1502 Create SolarPosition instance with context and optional coordinates.
1505 context: Helios Context
1506 utc_offset: UTC time offset in hours (optional)
1507 latitude: Latitude in degrees (optional)
1508 longitude: Longitude in degrees (optional)
1511 SolarPosition instance
1514 >>> solar = create_solar_position(context, utc_offset=-8, latitude=38.5, longitude=-121.7)
1516 return SolarPosition(context, utc_offset, latitude, longitude)
Exception raised for SolarPosition-specific errors.
High-level interface for solar position calculations and radiation modeling.
float getOzoneColumn(self)
Get the total column ozone used by the solar radiation models.
_check_context_alive(self)
Raise if the owning Context has been destroyed (see Context.check_context_alive).
bool pragueSkyModelNeedsUpdate(self, float ground_albedo=0.33, float sun_tolerance=0.01, float turbidity_tolerance=0.02, float albedo_tolerance=0.05)
Check if Prague Sky Model needs updating based on changed conditions.
float getThermalSkyFlux(self, float wavelength_min_nm, float wavelength_max_nm)
Calculate the sky longwave irradiance on a horizontal surface within a thermal band.
float calibrateTurbidityFromTimeseries(self, str timeseries_label)
Calibrate atmospheric turbidity using timeseries data.
enableCloudCalibration(self, str timeseries_label)
Enable cloud calibration using timeseries data.
None setGroundAlbedo(self, float albedo)
Set the albedo of the ground surrounding the scene.
Time getSunriseTime(self)
Calculate sunrise time for the current date and location.
None setOzoneColumn(self, float ozone_DU)
Set the total column ozone, overriding the climatological value.
float getSunZenith(self)
Get the sun zenith angle in radians.
float getSunAzimuth(self)
Get the sun azimuth angle in radians.
None calculateSensorAtmosphereSpectra(self, str label, vec3 direction_to_sensor, float resolution_nm=1.0)
Calculate the spectral atmospheric quantities needed to simulate imagery from a sensor above the atmo...
float getAmbientLongwaveFlux(self, Optional[float] temperature_K=None, Optional[float] humidity_rel=None)
Calculate the ambient (sky) longwave radiation flux.
enablePragueSkyModel(self)
Enable Prague Sky Model for physically-based sky radiance calculations.
float getSolarFluxPAR(self, Optional[float] pressure_Pa=None, Optional[float] temperature_K=None, Optional[float] humidity_rel=None, Optional[float] turbidity=None)
Calculate PAR (Photosynthetically Active Radiation) solar flux.
None setAtmosphericConditions(self, float pressure_Pa, float temperature_K, float humidity_rel, float turbidity)
Set atmospheric conditions for subsequent flux calculations (modern API).
float getSolarFlux(self, Optional[float] pressure_Pa=None, Optional[float] temperature_K=None, Optional[float] humidity_rel=None, Optional[float] turbidity=None)
Calculate total solar flux (supports legacy and modern APIs).
bool isPragueSkyModelEnabled(self)
Check if Prague Sky Model is currently enabled.
float getGroundAlbedo(self)
Get the albedo of the ground surrounding the scene.
None calculateSensorThermalAtmosphere(self, str label, vec3 direction_to_sensor)
Calculate the thermal infrared atmospheric quantities needed to simulate thermal imagery from a senso...
float getSunElevation(self)
Get the sun elevation angle in radians.
bool is_available(self)
Check if SolarPosition is available in current build.
calculateDirectSolarSpectrum(self, str label, float resolution_nm=1.0)
Calculate direct beam solar spectrum.
calculateGlobalSolarSpectrum(self, str label, float resolution_nm=1.0)
Calculate global (total) solar spectrum.
updatePragueSkyModel(self, float ground_albedo=0.33)
Update Prague Sky Model and store spectral-angular parameters in Context.
SphericalCoord getSunDirectionSpherical(self)
Get the sun direction as spherical coordinates.
setSunDirection(self, SphericalCoord sundirection)
Override the computed solar position with a prescribed sun direction.
Time getSunsetTime(self)
Calculate sunset time for the current date and location.
__enter__(self)
Context manager entry.
__exit__(self, exc_type, exc_val, exc_tb)
Context manager exit - cleanup resources.
Tuple[float, float, float, float] getAtmosphericConditions(self)
Get currently set atmospheric conditions from Context.
__del__(self)
Destructor to ensure C++ resources freed even without 'with' statement.
float getSolarFluxNIR(self, Optional[float] pressure_Pa=None, Optional[float] temperature_K=None, Optional[float] humidity_rel=None, Optional[float] turbidity=None)
Calculate NIR (Near-Infrared) solar flux.
calculateDiffuseSolarSpectrum(self, str label, float resolution_nm=1.0)
Calculate diffuse solar spectrum.
disableCloudCalibration(self)
Disable cloud calibration.
float getDiffuseFraction(self, Optional[float] pressure_Pa=None, Optional[float] temperature_K=None, Optional[float] humidity_rel=None, Optional[float] turbidity=None)
Calculate the diffuse fraction of solar radiation.
vec3 getSunDirectionVector(self)
Get the sun direction as a 3D unit vector.
Exception classes for PyHelios library.
Helios Time structure for representing time values.
_solarposition_working_directory(Tuple[str,...] required_assets=(_PRAGUE_DATASET,))
Context manager that temporarily changes working directory to where SolarPosition assets are located.
None _validate_sensor_arguments(label, direction_to_sensor)
Validate the label and sensor direction shared by the sensor atmosphere methods.
SolarPosition create_solar_position(Context context, Optional[float] utc_offset=None, Optional[float] latitude=None, Optional[float] longitude=None)
Create SolarPosition instance with context and optional coordinates.