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.
9from typing
import List, Tuple, Optional, Union
10from .wrappers
import USolarPositionWrapper
as solar_wrapper
11from .Context
import Context
12from .plugins.registry
import get_plugin_registry
13from .exceptions
import HeliosError
14from .wrappers.DataTypes
import Time, Date, vec3, SphericalCoord
18 """Exception raised for SolarPosition-specific errors"""
24 High-level interface for solar position calculations and radiation modeling.
26 SolarPosition provides comprehensive solar angle calculations, radiation flux
27 modeling, sunrise/sunset time calculations, and atmospheric turbidity calibration.
28 The plugin automatically uses Context time/date for calculations or can be
29 initialized with explicit coordinates.
31 This class requires the native Helios library built with SolarPosition support.
32 Use context managers for proper resource cleanup.
35 Basic usage with Context coordinates:
36 >>> with Context() as context:
37 ... context.setDate(2023, 6, 21) # Summer solstice
38 ... context.setTime(12, 0) # Solar noon
39 ... with SolarPosition(context) as solar:
40 ... elevation = solar.getSunElevation()
41 ... print(f"Sun elevation: {elevation:.1f}°")
43 Usage with explicit coordinates:
44 >>> with Context() as context:
45 ... # Davis, California coordinates
46 ... with SolarPosition(context, utc_offset=-8, latitude=38.5, longitude=-121.7) as solar:
47 ... azimuth = solar.getSunAzimuth()
48 ... flux = solar.getSolarFlux(101325, 288.15, 0.6, 0.1)
49 ... print(f"Solar flux: {flux:.1f} W/m²")
52 def __init__(self, context: Context, utc_offset: Optional[float] =
None,
53 latitude: Optional[float] =
None, longitude: Optional[float] =
None):
55 Initialize SolarPosition with a Helios context.
58 context: Active Helios Context instance
59 utc_offset: UTC time offset in hours (-12 to +12). If provided with
60 latitude/longitude, creates plugin with explicit coordinates.
61 latitude: Latitude in degrees (-90 to +90). Required if utc_offset provided.
62 longitude: Longitude in degrees (-180 to +180). Required if utc_offset provided.
65 SolarPositionError: If plugin not available in current build
66 ValueError: If coordinate parameters are invalid or incomplete
67 RuntimeError: If plugin initialization fails
70 If coordinates are not provided, the plugin uses Context location settings.
71 Solar calculations depend on Context time/date - use context.setTime() and
72 context.setDate() to set the simulation time before calculations.
75 registry = get_plugin_registry()
76 if not registry.is_plugin_available(
'solarposition'):
78 "SolarPosition not available in current Helios library. "
79 "SolarPosition plugin availability depends on build configuration.\n"
81 "System requirements:\n"
82 " - Platforms: Windows, Linux, macOS\n"
83 " - Dependencies: None\n"
84 " - GPU: Not required\n"
86 "If you're seeing this error, the SolarPosition plugin may not be "
87 "properly compiled into your Helios library. Please rebuild PyHelios:\n"
88 " build_scripts/build_helios --clean"
92 if utc_offset
is not None or latitude
is not None or longitude
is not None:
94 if utc_offset
is None or latitude
is None or longitude
is None:
96 "If specifying coordinates, all three parameters must be provided: "
97 "utc_offset, latitude, longitude"
101 if utc_offset < -12.0
or utc_offset > 12.0:
102 raise ValueError(f
"UTC offset must be between -12 and +12 hours, got: {utc_offset}")
103 if latitude < -90.0
or latitude > 90.0:
104 raise ValueError(f
"Latitude must be between -90 and +90 degrees, got: {latitude}")
105 if longitude < -180.0
or longitude > 180.0:
106 raise ValueError(f
"Longitude must be between -180 and +180 degrees, got: {longitude}")
110 self.
_solar_pos = solar_wrapper.createSolarPositionWithCoordinates(
111 context.getNativePtr(), utc_offset, latitude, longitude
116 self.
_solar_pos = solar_wrapper.createSolarPosition(context.getNativePtr())
122 """Context manager entry"""
125 def __exit__(self, exc_type, exc_val, exc_tb):
126 """Context manager exit - cleanup resources"""
127 if hasattr(self,
'_solar_pos')
and self.
_solar_pos:
128 solar_wrapper.destroySolarPosition(self.
_solar_pos)
132 """Destructor to ensure C++ resources freed even without 'with' statement."""
133 if hasattr(self,
'_solar_pos')
and self.
_solar_pos is not None:
135 solar_wrapper.destroySolarPosition(self.
_solar_pos)
137 except Exception
as e:
139 warnings.warn(f
"Error in SolarPosition.__del__: {e}")
143 humidity_rel: float, turbidity: float) ->
None:
145 Set atmospheric conditions for subsequent flux calculations (modern API).
147 This method sets global atmospheric conditions in the Context that are used
148 by parameter-free flux methods (modern API). Once set, you can call getSolarFlux(),
149 getSolarFluxPAR(), etc. without passing atmospheric parameters.
152 pressure_Pa: Atmospheric pressure in Pascals (e.g., 101325 for sea level)
153 temperature_K: Temperature in Kelvin (e.g., 288.15 for 15°C)
154 humidity_rel: Relative humidity as fraction (0.0-1.0)
155 turbidity: Atmospheric turbidity coefficient (typically 0.02-0.5)
158 ValueError: If atmospheric parameters are out of valid ranges
159 SolarPositionError: If operation fails
162 This is the modern API pattern. Atmospheric conditions are stored in Context
163 global data and reused by all parameter-free flux methods until changed.
166 >>> # Modern API (set once, use many times)
167 >>> with Context() as context:
168 ... with SolarPosition(context) as solar:
169 ... solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
170 ... flux = solar.getSolarFlux() # No parameters needed
171 ... par = solar.getSolarFluxPAR() # Uses same conditions
172 ... diffuse = solar.getDiffuseFraction() # Uses same conditions
175 if pressure_Pa < 0.0:
176 raise ValueError(f
"Atmospheric pressure must be non-negative, got: {pressure_Pa}")
177 if temperature_K < 0.0:
178 raise ValueError(f
"Temperature must be non-negative, got: {temperature_K}")
179 if humidity_rel < 0.0
or humidity_rel > 1.0:
180 raise ValueError(f
"Relative humidity must be between 0 and 1, got: {humidity_rel}")
182 raise ValueError(f
"Turbidity must be non-negative, got: {turbidity}")
185 solar_wrapper.setAtmosphericConditions(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
186 except Exception
as e:
191 Get currently set atmospheric conditions from Context.
194 Tuple of (pressure_Pa, temperature_K, humidity_rel, turbidity)
197 SolarPositionError: If operation fails
200 If atmospheric conditions have not been set via setAtmosphericConditions(),
201 returns default values: (101325 Pa, 300 K, 0.5, 0.02)
204 >>> pressure, temp, humidity, turbidity = solar.getAtmosphericConditions()
205 >>> print(f"Pressure: {pressure} Pa, Temp: {temp} K")
208 return solar_wrapper.getAtmosphericConditions(self.
_solar_pos)
209 except Exception
as e:
215 Get the sun elevation angle in degrees.
218 Sun elevation angle in degrees (0° = horizon, 90° = zenith)
221 SolarPositionError: If calculation fails
224 >>> elevation = solar.getSunElevation()
225 >>> print(f"Sun is {elevation:.1f}° above horizon")
228 return solar_wrapper.getSunElevation(self.
_solar_pos)
229 except Exception
as e:
234 Get the sun zenith angle in degrees.
237 Sun zenith angle in degrees (0° = zenith, 90° = horizon)
240 SolarPositionError: If calculation fails
243 >>> zenith = solar.getSunZenith()
244 >>> print(f"Sun zenith angle: {zenith:.1f}°")
247 return solar_wrapper.getSunZenith(self.
_solar_pos)
248 except Exception
as e:
253 Get the sun azimuth angle in degrees.
256 Sun azimuth angle in degrees (0° = North, 90° = East, 180° = South, 270° = West)
259 SolarPositionError: If calculation fails
262 >>> azimuth = solar.getSunAzimuth()
263 >>> print(f"Sun azimuth: {azimuth:.1f}° (compass bearing)")
266 return solar_wrapper.getSunAzimuth(self.
_solar_pos)
267 except Exception
as e:
273 Get the sun direction as a 3D unit vector.
276 vec3 representing the sun direction vector (x, y, z)
279 SolarPositionError: If calculation fails
282 >>> direction = solar.getSunDirectionVector()
283 >>> print(f"Sun direction vector: ({direction.x:.3f}, {direction.y:.3f}, {direction.z:.3f})")
286 direction_list = solar_wrapper.getSunDirectionVector(self.
_solar_pos)
287 return vec3(direction_list[0], direction_list[1], direction_list[2])
288 except Exception
as e:
293 Get the sun direction as spherical coordinates.
296 SphericalCoord with radius=1, elevation and azimuth in radians
299 SolarPositionError: If calculation fails
302 >>> spherical = solar.getSunDirectionSpherical()
303 >>> print(f"Spherical: r={spherical.radius}, elev={spherical.elevation:.3f}, az={spherical.azimuth:.3f}")
306 spherical_list = solar_wrapper.getSunDirectionSpherical(self.
_solar_pos)
308 radius=spherical_list[0],
309 elevation=spherical_list[1],
310 azimuth=spherical_list[2]
312 except Exception
as e:
316 def getSolarFlux(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
317 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
319 Calculate total solar flux (supports legacy and modern APIs).
321 This method supports both legacy and modern APIs:
322 - **Legacy API**: Pass all 4 atmospheric parameters explicitly
323 - **Modern API**: Pass no parameters, uses atmospheric conditions from setAtmosphericConditions()
326 pressure_Pa: Atmospheric pressure in Pascals (e.g., 101325 for sea level) [optional]
327 temperature_K: Temperature in Kelvin (e.g., 288.15 for 15°C) [optional]
328 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
329 turbidity: Atmospheric turbidity coefficient (typically 0.02-0.5) [optional]
332 Total solar flux in W/m²
335 ValueError: If some parameters provided but not all, or if values are invalid
336 SolarPositionError: If calculation fails or atmospheric conditions not set (modern API)
339 Legacy API (backward compatible):
340 >>> flux = solar.getSolarFlux(101325, 288.15, 0.6, 0.1)
342 Modern API (cleaner, reuses atmospheric state):
343 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
344 >>> flux = solar.getSolarFlux() # No parameters needed
347 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
348 humidity_rel
is not None, turbidity
is not None]
350 if all(params_provided):
353 return solar_wrapper.getSolarFlux(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
354 except Exception
as e:
357 elif not any(params_provided):
360 return solar_wrapper.getSolarFluxFromState(self.
_solar_pos)
361 except Exception
as e:
363 f
"Failed to calculate solar flux from atmospheric state: {e}\n"
364 "Hint: Call setAtmosphericConditions() first to use parameter-free API, "
365 "or provide all 4 atmospheric parameters for legacy API."
371 "Either provide all atmospheric parameters (pressure_Pa, temperature_K, humidity_rel, turbidity) "
372 "or provide none to use atmospheric conditions from setAtmosphericConditions(). "
373 "Partial parameter sets are not supported."
376 def getSolarFluxPAR(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
377 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
379 Calculate PAR (Photosynthetically Active Radiation) solar flux.
381 Supports both legacy (parameter-based) and modern (state-based) APIs.
384 pressure_Pa: Atmospheric pressure in Pascals [optional]
385 temperature_K: Temperature in Kelvin [optional]
386 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
387 turbidity: Atmospheric turbidity coefficient [optional]
390 PAR solar flux in W/m² (wavelength range ~400-700 nm)
393 ValueError: If some parameters provided but not all
394 SolarPositionError: If calculation fails
397 Legacy: par_flux = solar.getSolarFluxPAR(101325, 288.15, 0.6, 0.1)
398 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
399 par_flux = solar.getSolarFluxPAR()
401 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
402 humidity_rel
is not None, turbidity
is not None]
404 if all(params_provided):
406 return solar_wrapper.getSolarFluxPAR(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
407 except Exception
as e:
409 elif not any(params_provided):
411 return solar_wrapper.getSolarFluxPARFromState(self.
_solar_pos)
412 except Exception
as e:
414 f
"Failed to calculate PAR flux from atmospheric state: {e}\n"
415 "Hint: Call setAtmosphericConditions() first."
418 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
420 def getSolarFluxNIR(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
421 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
423 Calculate NIR (Near-Infrared) solar flux.
425 Supports both legacy (parameter-based) and modern (state-based) APIs.
428 pressure_Pa: Atmospheric pressure in Pascals [optional]
429 temperature_K: Temperature in Kelvin [optional]
430 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
431 turbidity: Atmospheric turbidity coefficient [optional]
434 NIR solar flux in W/m² (wavelength range >700 nm)
437 ValueError: If some parameters provided but not all
438 SolarPositionError: If calculation fails
441 Legacy: nir_flux = solar.getSolarFluxNIR(101325, 288.15, 0.6, 0.1)
442 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
443 nir_flux = solar.getSolarFluxNIR()
445 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
446 humidity_rel
is not None, turbidity
is not None]
448 if all(params_provided):
450 return solar_wrapper.getSolarFluxNIR(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
451 except Exception
as e:
453 elif not any(params_provided):
455 return solar_wrapper.getSolarFluxNIRFromState(self.
_solar_pos)
456 except Exception
as e:
458 f
"Failed to calculate NIR flux from atmospheric state: {e}\n"
459 "Hint: Call setAtmosphericConditions() first."
462 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
464 def getDiffuseFraction(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
465 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
467 Calculate the diffuse fraction of solar radiation.
469 Supports both legacy (parameter-based) and modern (state-based) APIs.
472 pressure_Pa: Atmospheric pressure in Pascals [optional]
473 temperature_K: Temperature in Kelvin [optional]
474 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
475 turbidity: Atmospheric turbidity coefficient [optional]
478 Diffuse fraction as ratio (0.0-1.0) where:
479 - 0.0 = all direct radiation
480 - 1.0 = all diffuse radiation
483 ValueError: If some parameters provided but not all
484 SolarPositionError: If calculation fails
487 Legacy: diffuse = solar.getDiffuseFraction(101325, 288.15, 0.6, 0.1)
488 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
489 diffuse = solar.getDiffuseFraction()
491 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
492 humidity_rel
is not None, turbidity
is not None]
494 if all(params_provided):
496 return solar_wrapper.getDiffuseFraction(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
497 except Exception
as e:
499 elif not any(params_provided):
501 return solar_wrapper.getDiffuseFractionFromState(self.
_solar_pos)
502 except Exception
as e:
504 f
"Failed to calculate diffuse fraction from atmospheric state: {e}\n"
505 "Hint: Call setAtmosphericConditions() first."
508 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
511 humidity_rel: Optional[float] =
None) -> float:
513 Calculate the ambient (sky) longwave radiation flux.
515 This method supports both legacy and modern APIs:
516 - **Legacy API**: Pass temperature and humidity explicitly
517 - **Modern API**: Pass no parameters, uses atmospheric conditions from setAtmosphericConditions()
520 temperature_K: Temperature in Kelvin [optional]
521 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
524 Ambient longwave flux in W/m²
527 ValueError: If one parameter provided but not the other
528 SolarPositionError: If calculation fails
531 The longwave flux model is based on Prata (1996).
532 Returns downwelling longwave radiation flux on a horizontal surface.
536 >>> lw_flux = solar.getAmbientLongwaveFlux(288.15, 0.6)
538 Modern API (uses temperature and humidity from setAtmosphericConditions):
539 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
540 >>> lw_flux = solar.getAmbientLongwaveFlux()
542 params_provided = [temperature_K
is not None, humidity_rel
is not None]
544 if all(params_provided):
550 saved_conditions = solar_wrapper.getAtmosphericConditions(self.
_solar_pos)
554 solar_wrapper.setAtmosphericConditions(self.
_solar_pos,
561 result = solar_wrapper.getAmbientLongwaveFluxFromState(self.
_solar_pos)
564 solar_wrapper.setAtmosphericConditions(self.
_solar_pos, *saved_conditions)
568 except Exception
as e:
571 elif not any(params_provided):
574 return solar_wrapper.getAmbientLongwaveFluxFromState(self.
_solar_pos)
575 except Exception
as e:
577 f
"Failed to calculate ambient longwave flux from atmospheric state: {e}\n"
578 "Hint: Call setAtmosphericConditions() first to use parameter-free API, "
579 "or provide temperature_K and humidity_rel for legacy API."
585 "Either provide both temperature_K and humidity_rel, "
586 "or provide neither to use atmospheric conditions from setAtmosphericConditions()."
592 Calculate sunrise time for the current date and location.
595 Time object with sunrise time (hour, minute, second)
598 SolarPositionError: If calculation fails
601 >>> sunrise = solar.getSunriseTime()
602 >>> print(f"Sunrise: {sunrise}") # Prints as HH:MM:SS
605 hour, minute, second = solar_wrapper.getSunriseTime(self.
_solar_pos)
606 return Time(hour, minute, second)
607 except Exception
as e:
612 Calculate sunset time for the current date and location.
615 Time object with sunset time (hour, minute, second)
618 SolarPositionError: If calculation fails
621 >>> sunset = solar.getSunsetTime()
622 >>> print(f"Sunset: {sunset}") # Prints as HH:MM:SS
625 hour, minute, second = solar_wrapper.getSunsetTime(self.
_solar_pos)
626 return Time(hour, minute, second)
627 except Exception
as e:
633 Calibrate atmospheric turbidity using timeseries data.
636 timeseries_label: Label of timeseries data in Context
639 ValueError: If timeseries label is invalid
640 SolarPositionError: If calibration fails
643 >>> solar.calibrateTurbidityFromTimeseries("solar_irradiance")
645 if not timeseries_label:
646 raise ValueError(
"Timeseries label cannot be empty")
649 solar_wrapper.calibrateTurbidityFromTimeseries(self.
_solar_pos, timeseries_label)
650 except Exception
as e:
655 Enable cloud calibration using timeseries data.
658 timeseries_label: Label of cloud timeseries data in Context
661 ValueError: If timeseries label is invalid
662 SolarPositionError: If calibration setup fails
665 >>> solar.enableCloudCalibration("cloud_cover")
667 if not timeseries_label:
668 raise ValueError(
"Timeseries label cannot be empty")
671 solar_wrapper.enableCloudCalibration(self.
_solar_pos, timeseries_label)
672 except Exception
as e:
677 Disable cloud calibration.
680 SolarPositionError: If operation fails
683 >>> solar.disableCloudCalibration()
686 solar_wrapper.disableCloudCalibration(self.
_solar_pos)
687 except Exception
as e:
693 Enable Prague Sky Model for physically-based sky radiance calculations.
695 The Prague Sky Model provides high-quality spectral and angular sky radiance
696 distribution for accurate diffuse radiation modeling. It accounts for Rayleigh
697 and Mie scattering to produce realistic sky radiance patterns across the
698 360-1480 nm spectral range.
701 SolarPositionError: If operation fails
704 After enabling, call updatePragueSkyModel() to compute and store spectral-angular
705 parameters in Context global data. Requires ~27 MB data file:
706 plugins/solarposition/lib/prague_sky_model/PragueSkyModelReduced.dat
709 >>> with Context() as context:
710 ... with SolarPosition(context) as solar:
711 ... solar.enablePragueSkyModel()
712 ... solar.updatePragueSkyModel()
715 solar_wrapper.enablePragueSkyModel(self.
_solar_pos)
716 except Exception
as e:
721 Check if Prague Sky Model is currently enabled.
724 True if Prague Sky Model has been enabled via enablePragueSkyModel(), False otherwise
727 SolarPositionError: If operation fails
730 >>> if solar.isPragueSkyModelEnabled():
731 ... print("Prague Sky Model is active")
734 return solar_wrapper.isPragueSkyModelEnabled(self.
_solar_pos)
735 except Exception
as e:
740 Update Prague Sky Model and store spectral-angular parameters in Context.
742 This is a computationally intensive operation (~1100 model queries with OpenMP
743 parallelization) that computes sky radiance distribution for current atmospheric
744 and solar conditions. Use pragueSkyModelNeedsUpdate() for lazy evaluation to
745 avoid unnecessary updates.
748 ground_albedo: Ground surface albedo (default: 0.33 for typical soil/vegetation)
751 SolarPositionError: If update fails
754 Reads turbidity from Context atmospheric conditions. Stores results in Context
755 global data as "prague_sky_spectral_params" (1350 floats: 225 wavelengths × 6 params),
756 "prague_sky_sun_direction", "prague_sky_visibility_km", "prague_sky_ground_albedo",
757 and "prague_sky_valid" flag.
760 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
761 >>> solar.updatePragueSkyModel(ground_albedo=0.25)
764 solar_wrapper.updatePragueSkyModel(self.
_solar_pos, ground_albedo)
765 except Exception
as e:
769 sun_tolerance: float = 0.01,
770 turbidity_tolerance: float = 0.02,
771 albedo_tolerance: float = 0.05) -> bool:
773 Check if Prague Sky Model needs updating based on changed conditions.
775 Enables lazy evaluation to avoid expensive Prague updates when conditions haven't
776 changed significantly. Compares current state against cached values.
779 ground_albedo: Current ground albedo (default: 0.33)
780 sun_tolerance: Threshold for sun direction changes (default: 0.01 ≈ 0.57°)
781 turbidity_tolerance: Relative threshold for turbidity (default: 0.02 = 2%)
782 albedo_tolerance: Threshold for albedo changes (default: 0.05 = 5%)
785 True if updatePragueSkyModel() should be called, False if cached data is valid
788 SolarPositionError: If check fails
791 Reads turbidity from Context atmospheric conditions for comparison.
794 >>> if solar.pragueSkyModelNeedsUpdate():
795 ... solar.updatePragueSkyModel()
798 return solar_wrapper.pragueSkyModelNeedsUpdate(self.
_solar_pos, ground_albedo,
799 sun_tolerance, turbidity_tolerance,
801 except Exception
as e:
807 Calculate direct beam solar spectrum using SSolar-GOA model.
809 Computes the spectral irradiance of direct beam solar radiation across
810 300-2600 nm wavelength range using the SSolar-GOA (Global Ozone and
811 Atmospheric) spectral model. Results are stored in Context global data
812 as a vector of (wavelength, irradiance) pairs.
815 label: Label to store the spectrum data in Context global data
816 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
817 Lower values give finer spectral resolution but require
818 more computation. Default is 1.0 nm.
821 ValueError: If label is empty or resolution is out of valid range
822 SolarPositionError: If calculation fails
825 - Requires Context time/date to be set for accurate solar position
826 - Atmospheric parameters from Context location are used
827 - Results accessible via context.getGlobalData(label)
828 - SSolar-GOA model accounts for atmospheric absorption and scattering
831 >>> with Context() as context:
832 ... context.setDate(2023, 6, 21)
833 ... context.setTime(12, 0)
834 ... with SolarPosition(context) as solar:
835 ... solar.calculateDirectSolarSpectrum("direct_spectrum", resolution_nm=5.0)
836 ... spectrum = context.getGlobalData("direct_spectrum")
837 ... # spectrum is list of vec2(wavelength_nm, irradiance_W_m2_nm)
840 raise ValueError(
"Label cannot be empty")
841 if resolution_nm < 1.0
or resolution_nm > 2300.0:
842 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
845 solar_wrapper.calculateDirectSolarSpectrum(self.
_solar_pos, label, resolution_nm)
846 except Exception
as e:
851 Calculate diffuse solar spectrum using SSolar-GOA model.
853 Computes the spectral irradiance of diffuse (scattered) solar radiation
854 across 300-2600 nm wavelength range using the SSolar-GOA model. Results
855 are stored in Context global data as a vector of (wavelength, irradiance) pairs.
858 label: Label to store the spectrum data in Context global data
859 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
860 Lower values give finer spectral resolution but require
861 more computation. Default is 1.0 nm.
864 ValueError: If label is empty or resolution is out of valid range
865 SolarPositionError: If calculation fails
868 - Requires Context time/date to be set for accurate solar position
869 - Atmospheric parameters from Context location are used
870 - Results accessible via context.getGlobalData(label)
871 - Diffuse radiation results from atmospheric scattering (Rayleigh, aerosol)
874 >>> with Context() as context:
875 ... context.setDate(2023, 6, 21)
876 ... context.setTime(12, 0)
877 ... with SolarPosition(context) as solar:
878 ... solar.calculateDiffuseSolarSpectrum("diffuse_spectrum", resolution_nm=5.0)
879 ... spectrum = context.getGlobalData("diffuse_spectrum")
880 ... # spectrum is list of vec2(wavelength_nm, irradiance_W_m2_nm)
883 raise ValueError(
"Label cannot be empty")
884 if resolution_nm < 1.0
or resolution_nm > 2300.0:
885 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
888 solar_wrapper.calculateDiffuseSolarSpectrum(self.
_solar_pos, label, resolution_nm)
889 except Exception
as e:
894 Calculate global (total) solar spectrum using SSolar-GOA model.
896 Computes the spectral irradiance of total solar radiation (direct + diffuse)
897 across 300-2600 nm wavelength range using the SSolar-GOA model. Results
898 are stored in Context global data as a vector of (wavelength, irradiance) pairs.
901 label: Label to store the spectrum data in Context global data
902 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
903 Lower values give finer spectral resolution but require
904 more computation. Default is 1.0 nm.
907 ValueError: If label is empty or resolution is out of valid range
908 SolarPositionError: If calculation fails
911 - Requires Context time/date to be set for accurate solar position
912 - Atmospheric parameters from Context location are used
913 - Results accessible via context.getGlobalData(label)
914 - Global spectrum = direct beam + diffuse (sky) radiation
915 - Most useful for plant canopy modeling and photosynthesis calculations
918 >>> with Context() as context:
919 ... context.setDate(2023, 6, 21)
920 ... context.setTime(12, 0)
921 ... with SolarPosition(context) as solar:
922 ... solar.calculateGlobalSolarSpectrum("global_spectrum", resolution_nm=10.0)
923 ... spectrum = context.getGlobalData("global_spectrum")
924 ... # spectrum is list of vec2(wavelength_nm, irradiance_W_m2_nm)
925 ... total_irradiance = sum([s.y for s in spectrum]) * 10.0 # Integrate
928 raise ValueError(
"Label cannot be empty")
929 if resolution_nm < 1.0
or resolution_nm > 2300.0:
930 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
933 solar_wrapper.calculateGlobalSolarSpectrum(self.
_solar_pos, label, resolution_nm)
934 except Exception
as e:
939 Check if SolarPosition is available in current build.
942 True if plugin is available, False otherwise
944 registry = get_plugin_registry()
945 return registry.is_plugin_available(
'solarposition')
950 latitude: Optional[float] =
None, longitude: Optional[float] =
None) -> SolarPosition:
952 Create SolarPosition instance with context and optional coordinates.
955 context: Helios Context
956 utc_offset: UTC time offset in hours (optional)
957 latitude: Latitude in degrees (optional)
958 longitude: Longitude in degrees (optional)
961 SolarPosition instance
964 >>> solar = create_solar_position(context, utc_offset=-8, latitude=38.5, longitude=-121.7)
966 return SolarPosition(context, utc_offset, latitude, longitude)
Exception raised for SolarPosition-specific errors.
High-level interface for solar position calculations and radiation modeling.
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.
__init__(self, Context context, Optional[float] utc_offset=None, Optional[float] latitude=None, Optional[float] longitude=None)
Initialize SolarPosition with a Helios context.
enableCloudCalibration(self, str timeseries_label)
Enable cloud calibration using timeseries data.
Time getSunriseTime(self)
Calculate sunrise time for the current date and location.
float getSunZenith(self)
Get the sun zenith angle in degrees.
float getSunAzimuth(self)
Get the sun azimuth angle in degrees.
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 getSunElevation(self)
Get the sun elevation angle in degrees.
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 using SSolar-GOA model.
calculateGlobalSolarSpectrum(self, str label, float resolution_nm=1.0)
Calculate global (total) solar spectrum using SSolar-GOA model.
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.
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.
calibrateTurbidityFromTimeseries(self, str timeseries_label)
Calibrate atmospheric turbidity using timeseries data.
__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 using SSolar-GOA model.
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 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.