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, check_context_alive
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 """Raise if the owning Context has been destroyed (see Context.check_context_alive)."""
123 check_context_alive(getattr(self,
"context",
None),
"SolarPosition")
126 """Context manager entry"""
129 def __exit__(self, exc_type, exc_val, exc_tb):
130 """Context manager exit - cleanup resources"""
131 if hasattr(self,
'_solar_pos')
and self.
_solar_pos:
136 """Destructor to ensure C++ resources freed even without 'with' statement."""
137 if hasattr(self,
'_solar_pos')
and self.
_solar_pos is not None:
139 solar_wrapper.destroySolarPosition(self.
_solar_pos)
141 except Exception
as e:
143 warnings.warn(f
"Error in SolarPosition.__del__: {e}")
147 humidity_rel: float, turbidity: float) ->
None:
149 Set atmospheric conditions for subsequent flux calculations (modern API).
151 This method sets global atmospheric conditions in the Context that are used
152 by parameter-free flux methods (modern API). Once set, you can call getSolarFlux(),
153 getSolarFluxPAR(), etc. without passing atmospheric parameters.
156 pressure_Pa: Atmospheric pressure in Pascals (e.g., 101325 for sea level)
157 temperature_K: Temperature in Kelvin (e.g., 288.15 for 15°C)
158 humidity_rel: Relative humidity as fraction (0.0-1.0)
159 turbidity: Atmospheric turbidity coefficient (typically 0.02-0.5)
162 ValueError: If atmospheric parameters are out of valid ranges
163 SolarPositionError: If operation fails
166 This is the modern API pattern. Atmospheric conditions are stored in Context
167 global data and reused by all parameter-free flux methods until changed.
170 >>> # Modern API (set once, use many times)
171 >>> with Context() as context:
172 ... with SolarPosition(context) as solar:
173 ... solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
174 ... flux = solar.getSolarFlux() # No parameters needed
175 ... par = solar.getSolarFluxPAR() # Uses same conditions
176 ... diffuse = solar.getDiffuseFraction() # Uses same conditions
179 if pressure_Pa < 0.0:
180 raise ValueError(f
"Atmospheric pressure must be non-negative, got: {pressure_Pa}")
181 if temperature_K < 0.0:
182 raise ValueError(f
"Temperature must be non-negative, got: {temperature_K}")
183 if humidity_rel < 0.0
or humidity_rel > 1.0:
184 raise ValueError(f
"Relative humidity must be between 0 and 1, got: {humidity_rel}")
186 raise ValueError(f
"Turbidity must be non-negative, got: {turbidity}")
190 solar_wrapper.setAtmosphericConditions(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
191 except Exception
as e:
196 Get currently set atmospheric conditions from Context.
199 Tuple of (pressure_Pa, temperature_K, humidity_rel, turbidity)
202 SolarPositionError: If operation fails
205 If atmospheric conditions have not been set via setAtmosphericConditions(),
206 returns default values: (101325 Pa, 300 K, 0.5, 0.02)
209 >>> pressure, temp, humidity, turbidity = solar.getAtmosphericConditions()
210 >>> print(f"Pressure: {pressure} Pa, Temp: {temp} K")
214 return solar_wrapper.getAtmosphericConditions(self.
_solar_pos)
215 except Exception
as e:
221 Get the sun elevation angle in degrees.
224 Sun elevation angle in degrees (0° = horizon, 90° = zenith)
227 SolarPositionError: If calculation fails
230 >>> elevation = solar.getSunElevation()
231 >>> print(f"Sun is {elevation:.1f}° above horizon")
235 return solar_wrapper.getSunElevation(self.
_solar_pos)
236 except Exception
as e:
241 Get the sun zenith angle in degrees.
244 Sun zenith angle in degrees (0° = zenith, 90° = horizon)
247 SolarPositionError: If calculation fails
250 >>> zenith = solar.getSunZenith()
251 >>> print(f"Sun zenith angle: {zenith:.1f}°")
255 return solar_wrapper.getSunZenith(self.
_solar_pos)
256 except Exception
as e:
261 Get the sun azimuth angle in degrees.
264 Sun azimuth angle in degrees (0° = North, 90° = East, 180° = South, 270° = West)
267 SolarPositionError: If calculation fails
270 >>> azimuth = solar.getSunAzimuth()
271 >>> print(f"Sun azimuth: {azimuth:.1f}° (compass bearing)")
275 return solar_wrapper.getSunAzimuth(self.
_solar_pos)
276 except Exception
as e:
282 Get the sun direction as a 3D unit vector.
285 vec3 representing the sun direction vector (x, y, z)
288 SolarPositionError: If calculation fails
291 >>> direction = solar.getSunDirectionVector()
292 >>> print(f"Sun direction vector: ({direction.x:.3f}, {direction.y:.3f}, {direction.z:.3f})")
296 direction_list = solar_wrapper.getSunDirectionVector(self.
_solar_pos)
297 return vec3(direction_list[0], direction_list[1], direction_list[2])
298 except Exception
as e:
303 Get the sun direction as spherical coordinates.
306 SphericalCoord with radius=1, elevation and azimuth in radians
309 SolarPositionError: If calculation fails
312 >>> spherical = solar.getSunDirectionSpherical()
313 >>> print(f"Spherical: r={spherical.radius}, elev={spherical.elevation:.3f}, az={spherical.azimuth:.3f}")
317 spherical_list = solar_wrapper.getSunDirectionSpherical(self.
_solar_pos)
319 radius=spherical_list[0],
320 elevation=spherical_list[1],
321 azimuth=spherical_list[2]
323 except Exception
as e:
327 def getSolarFlux(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
328 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
330 Calculate total solar flux (supports legacy and modern APIs).
332 This method supports both legacy and modern APIs:
333 - **Legacy API**: Pass all 4 atmospheric parameters explicitly
334 - **Modern API**: Pass no parameters, uses atmospheric conditions from setAtmosphericConditions()
337 pressure_Pa: Atmospheric pressure in Pascals (e.g., 101325 for sea level) [optional]
338 temperature_K: Temperature in Kelvin (e.g., 288.15 for 15°C) [optional]
339 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
340 turbidity: Atmospheric turbidity coefficient (typically 0.02-0.5) [optional]
343 Total solar flux in W/m²
346 ValueError: If some parameters provided but not all, or if values are invalid
347 SolarPositionError: If calculation fails or atmospheric conditions not set (modern API)
350 Legacy API (backward compatible):
351 >>> flux = solar.getSolarFlux(101325, 288.15, 0.6, 0.1)
353 Modern API (cleaner, reuses atmospheric state):
354 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
355 >>> flux = solar.getSolarFlux() # No parameters needed
358 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
359 humidity_rel
is not None, turbidity
is not None]
361 if all(params_provided):
365 return solar_wrapper.getSolarFlux(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
366 except Exception
as e:
369 elif not any(params_provided):
373 return solar_wrapper.getSolarFluxFromState(self.
_solar_pos)
374 except Exception
as e:
376 f
"Failed to calculate solar flux from atmospheric state: {e}\n"
377 "Hint: Call setAtmosphericConditions() first to use parameter-free API, "
378 "or provide all 4 atmospheric parameters for legacy API."
384 "Either provide all atmospheric parameters (pressure_Pa, temperature_K, humidity_rel, turbidity) "
385 "or provide none to use atmospheric conditions from setAtmosphericConditions(). "
386 "Partial parameter sets are not supported."
389 def getSolarFluxPAR(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
390 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
392 Calculate PAR (Photosynthetically Active Radiation) solar flux.
394 Supports both legacy (parameter-based) and modern (state-based) APIs.
397 pressure_Pa: Atmospheric pressure in Pascals [optional]
398 temperature_K: Temperature in Kelvin [optional]
399 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
400 turbidity: Atmospheric turbidity coefficient [optional]
403 PAR solar flux in W/m² (wavelength range ~400-700 nm)
406 ValueError: If some parameters provided but not all
407 SolarPositionError: If calculation fails
410 Legacy: par_flux = solar.getSolarFluxPAR(101325, 288.15, 0.6, 0.1)
411 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
412 par_flux = solar.getSolarFluxPAR()
414 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
415 humidity_rel
is not None, turbidity
is not None]
417 if all(params_provided):
420 return solar_wrapper.getSolarFluxPAR(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
421 except Exception
as e:
423 elif not any(params_provided):
426 return solar_wrapper.getSolarFluxPARFromState(self.
_solar_pos)
427 except Exception
as e:
429 f
"Failed to calculate PAR flux from atmospheric state: {e}\n"
430 "Hint: Call setAtmosphericConditions() first."
433 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
435 def getSolarFluxNIR(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
436 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
438 Calculate NIR (Near-Infrared) solar flux.
440 Supports both legacy (parameter-based) and modern (state-based) APIs.
443 pressure_Pa: Atmospheric pressure in Pascals [optional]
444 temperature_K: Temperature in Kelvin [optional]
445 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
446 turbidity: Atmospheric turbidity coefficient [optional]
449 NIR solar flux in W/m² (wavelength range >700 nm)
452 ValueError: If some parameters provided but not all
453 SolarPositionError: If calculation fails
456 Legacy: nir_flux = solar.getSolarFluxNIR(101325, 288.15, 0.6, 0.1)
457 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
458 nir_flux = solar.getSolarFluxNIR()
460 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
461 humidity_rel
is not None, turbidity
is not None]
463 if all(params_provided):
466 return solar_wrapper.getSolarFluxNIR(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
467 except Exception
as e:
469 elif not any(params_provided):
472 return solar_wrapper.getSolarFluxNIRFromState(self.
_solar_pos)
473 except Exception
as e:
475 f
"Failed to calculate NIR flux from atmospheric state: {e}\n"
476 "Hint: Call setAtmosphericConditions() first."
479 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
481 def getDiffuseFraction(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
482 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
484 Calculate the diffuse fraction of solar radiation.
486 Supports both legacy (parameter-based) and modern (state-based) APIs.
489 pressure_Pa: Atmospheric pressure in Pascals [optional]
490 temperature_K: Temperature in Kelvin [optional]
491 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
492 turbidity: Atmospheric turbidity coefficient [optional]
495 Diffuse fraction as ratio (0.0-1.0) where:
496 - 0.0 = all direct radiation
497 - 1.0 = all diffuse radiation
500 ValueError: If some parameters provided but not all
501 SolarPositionError: If calculation fails
504 Legacy: diffuse = solar.getDiffuseFraction(101325, 288.15, 0.6, 0.1)
505 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
506 diffuse = solar.getDiffuseFraction()
508 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
509 humidity_rel
is not None, turbidity
is not None]
511 if all(params_provided):
514 return solar_wrapper.getDiffuseFraction(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
515 except Exception
as e:
517 elif not any(params_provided):
520 return solar_wrapper.getDiffuseFractionFromState(self.
_solar_pos)
521 except Exception
as e:
523 f
"Failed to calculate diffuse fraction from atmospheric state: {e}\n"
524 "Hint: Call setAtmosphericConditions() first."
527 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
530 humidity_rel: Optional[float] =
None) -> float:
532 Calculate the ambient (sky) longwave radiation flux.
534 This method supports both legacy and modern APIs:
535 - **Legacy API**: Pass temperature and humidity explicitly
536 - **Modern API**: Pass no parameters, uses atmospheric conditions from setAtmosphericConditions()
539 temperature_K: Temperature in Kelvin [optional]
540 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
543 Ambient longwave flux in W/m²
546 ValueError: If one parameter provided but not the other
547 SolarPositionError: If calculation fails
550 The longwave flux model is based on Prata (1996).
551 Returns downwelling longwave radiation flux on a horizontal surface.
555 >>> lw_flux = solar.getAmbientLongwaveFlux(288.15, 0.6)
557 Modern API (uses temperature and humidity from setAtmosphericConditions):
558 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
559 >>> lw_flux = solar.getAmbientLongwaveFlux()
561 params_provided = [temperature_K
is not None, humidity_rel
is not None]
563 if all(params_provided):
570 saved_conditions = solar_wrapper.getAtmosphericConditions(self.
_solar_pos)
574 solar_wrapper.setAtmosphericConditions(self.
_solar_pos,
581 result = solar_wrapper.getAmbientLongwaveFluxFromState(self.
_solar_pos)
584 solar_wrapper.setAtmosphericConditions(self.
_solar_pos, *saved_conditions)
588 except Exception
as e:
591 elif not any(params_provided):
595 return solar_wrapper.getAmbientLongwaveFluxFromState(self.
_solar_pos)
596 except Exception
as e:
598 f
"Failed to calculate ambient longwave flux from atmospheric state: {e}\n"
599 "Hint: Call setAtmosphericConditions() first to use parameter-free API, "
600 "or provide temperature_K and humidity_rel for legacy API."
606 "Either provide both temperature_K and humidity_rel, "
607 "or provide neither to use atmospheric conditions from setAtmosphericConditions()."
613 Calculate sunrise time for the current date and location.
616 Time object with sunrise time (hour, minute, second)
619 SolarPositionError: If calculation fails
622 >>> sunrise = solar.getSunriseTime()
623 >>> print(f"Sunrise: {sunrise}") # Prints as HH:MM:SS
627 hour, minute, second = solar_wrapper.getSunriseTime(self.
_solar_pos)
628 return Time(hour, minute, second)
629 except Exception
as e:
634 Calculate sunset time for the current date and location.
637 Time object with sunset time (hour, minute, second)
640 SolarPositionError: If calculation fails
643 >>> sunset = solar.getSunsetTime()
644 >>> print(f"Sunset: {sunset}") # Prints as HH:MM:SS
648 hour, minute, second = solar_wrapper.getSunsetTime(self.
_solar_pos)
649 return Time(hour, minute, second)
650 except Exception
as e:
656 Calibrate atmospheric turbidity using timeseries data.
659 timeseries_label: Label of timeseries data in Context
662 ValueError: If timeseries label is invalid
663 SolarPositionError: If calibration fails
666 >>> solar.calibrateTurbidityFromTimeseries("solar_irradiance")
668 if not timeseries_label:
669 raise ValueError(
"Timeseries label cannot be empty")
673 solar_wrapper.calibrateTurbidityFromTimeseries(self.
_solar_pos, timeseries_label)
674 except Exception
as e:
679 Enable cloud calibration using timeseries data.
682 timeseries_label: Label of cloud timeseries data in Context
685 ValueError: If timeseries label is invalid
686 SolarPositionError: If calibration setup fails
689 >>> solar.enableCloudCalibration("cloud_cover")
691 if not timeseries_label:
692 raise ValueError(
"Timeseries label cannot be empty")
696 solar_wrapper.enableCloudCalibration(self.
_solar_pos, timeseries_label)
697 except Exception
as e:
702 Disable cloud calibration.
705 SolarPositionError: If operation fails
708 >>> solar.disableCloudCalibration()
712 solar_wrapper.disableCloudCalibration(self.
_solar_pos)
713 except Exception
as e:
719 Enable Prague Sky Model for physically-based sky radiance calculations.
721 The Prague Sky Model provides high-quality spectral and angular sky radiance
722 distribution for accurate diffuse radiation modeling. It accounts for Rayleigh
723 and Mie scattering to produce realistic sky radiance patterns across the
724 360-1480 nm spectral range.
727 SolarPositionError: If operation fails
730 After enabling, call updatePragueSkyModel() to compute and store spectral-angular
731 parameters in Context global data. Requires ~27 MB data file:
732 plugins/solarposition/lib/prague_sky_model/PragueSkyModelReduced.dat
735 >>> with Context() as context:
736 ... with SolarPosition(context) as solar:
737 ... solar.enablePragueSkyModel()
738 ... solar.updatePragueSkyModel()
742 solar_wrapper.enablePragueSkyModel(self.
_solar_pos)
743 except Exception
as e:
748 Check if Prague Sky Model is currently enabled.
751 True if Prague Sky Model has been enabled via enablePragueSkyModel(), False otherwise
754 SolarPositionError: If operation fails
757 >>> if solar.isPragueSkyModelEnabled():
758 ... print("Prague Sky Model is active")
762 return solar_wrapper.isPragueSkyModelEnabled(self.
_solar_pos)
763 except Exception
as e:
768 Update Prague Sky Model and store spectral-angular parameters in Context.
770 This is a computationally intensive operation (~1100 model queries with OpenMP
771 parallelization) that computes sky radiance distribution for current atmospheric
772 and solar conditions. Use pragueSkyModelNeedsUpdate() for lazy evaluation to
773 avoid unnecessary updates.
776 ground_albedo: Ground surface albedo (default: 0.33 for typical soil/vegetation)
779 SolarPositionError: If update fails
782 Reads turbidity from Context atmospheric conditions. Stores results in Context
783 global data as "prague_sky_spectral_params" (1350 floats: 225 wavelengths × 6 params),
784 "prague_sky_sun_direction", "prague_sky_visibility_km", "prague_sky_ground_albedo",
785 and "prague_sky_valid" flag.
788 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
789 >>> solar.updatePragueSkyModel(ground_albedo=0.25)
793 solar_wrapper.updatePragueSkyModel(self.
_solar_pos, ground_albedo)
794 except Exception
as e:
798 sun_tolerance: float = 0.01,
799 turbidity_tolerance: float = 0.02,
800 albedo_tolerance: float = 0.05) -> bool:
802 Check if Prague Sky Model needs updating based on changed conditions.
804 Enables lazy evaluation to avoid expensive Prague updates when conditions haven't
805 changed significantly. Compares current state against cached values.
808 ground_albedo: Current ground albedo (default: 0.33)
809 sun_tolerance: Threshold for sun direction changes (default: 0.01 ≈ 0.57°)
810 turbidity_tolerance: Relative threshold for turbidity (default: 0.02 = 2%)
811 albedo_tolerance: Threshold for albedo changes (default: 0.05 = 5%)
814 True if updatePragueSkyModel() should be called, False if cached data is valid
817 SolarPositionError: If check fails
820 Reads turbidity from Context atmospheric conditions for comparison.
823 >>> if solar.pragueSkyModelNeedsUpdate():
824 ... solar.updatePragueSkyModel()
828 return solar_wrapper.pragueSkyModelNeedsUpdate(self.
_solar_pos, ground_albedo,
829 sun_tolerance, turbidity_tolerance,
831 except Exception
as e:
837 Calculate direct beam solar spectrum using SSolar-GOA model.
839 Computes the spectral irradiance of direct beam solar radiation across
840 300-2600 nm wavelength range using the SSolar-GOA (Global Ozone and
841 Atmospheric) spectral model. Results are stored in Context global data
842 as a vector of (wavelength, irradiance) pairs.
845 label: Label to store the spectrum data in Context global data
846 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
847 Lower values give finer spectral resolution but require
848 more computation. Default is 1.0 nm.
851 ValueError: If label is empty or resolution is out of valid range
852 SolarPositionError: If calculation fails
855 - Requires Context time/date to be set for accurate solar position
856 - Atmospheric parameters from Context location are used
857 - Results accessible via context.getGlobalData(label)
858 - SSolar-GOA model accounts for atmospheric absorption and scattering
861 >>> with Context() as context:
862 ... context.setDate(2023, 6, 21)
863 ... context.setTime(12, 0)
864 ... with SolarPosition(context) as solar:
865 ... solar.calculateDirectSolarSpectrum("direct_spectrum", resolution_nm=5.0)
866 ... spectrum = context.getGlobalData("direct_spectrum")
867 ... # spectrum is list of vec2(wavelength_nm, irradiance_W_m2_nm)
870 raise ValueError(
"Label cannot be empty")
871 if resolution_nm < 1.0
or resolution_nm > 2300.0:
872 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
876 solar_wrapper.calculateDirectSolarSpectrum(self.
_solar_pos, label, resolution_nm)
877 except Exception
as e:
882 Calculate diffuse solar spectrum using SSolar-GOA model.
884 Computes the spectral irradiance of diffuse (scattered) solar radiation
885 across 300-2600 nm wavelength range using the SSolar-GOA model. Results
886 are stored in Context global data as a vector of (wavelength, irradiance) pairs.
889 label: Label to store the spectrum data in Context global data
890 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
891 Lower values give finer spectral resolution but require
892 more computation. Default is 1.0 nm.
895 ValueError: If label is empty or resolution is out of valid range
896 SolarPositionError: If calculation fails
899 - Requires Context time/date to be set for accurate solar position
900 - Atmospheric parameters from Context location are used
901 - Results accessible via context.getGlobalData(label)
902 - Diffuse radiation results from atmospheric scattering (Rayleigh, aerosol)
905 >>> with Context() as context:
906 ... context.setDate(2023, 6, 21)
907 ... context.setTime(12, 0)
908 ... with SolarPosition(context) as solar:
909 ... solar.calculateDiffuseSolarSpectrum("diffuse_spectrum", resolution_nm=5.0)
910 ... spectrum = context.getGlobalData("diffuse_spectrum")
911 ... # spectrum is list of vec2(wavelength_nm, irradiance_W_m2_nm)
914 raise ValueError(
"Label cannot be empty")
915 if resolution_nm < 1.0
or resolution_nm > 2300.0:
916 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
920 solar_wrapper.calculateDiffuseSolarSpectrum(self.
_solar_pos, label, resolution_nm)
921 except Exception
as e:
926 Calculate global (total) solar spectrum using SSolar-GOA model.
928 Computes the spectral irradiance of total solar radiation (direct + diffuse)
929 across 300-2600 nm wavelength range using the SSolar-GOA model. Results
930 are stored in Context global data as a vector of (wavelength, irradiance) pairs.
933 label: Label to store the spectrum data in Context global data
934 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
935 Lower values give finer spectral resolution but require
936 more computation. Default is 1.0 nm.
939 ValueError: If label is empty or resolution is out of valid range
940 SolarPositionError: If calculation fails
943 - Requires Context time/date to be set for accurate solar position
944 - Atmospheric parameters from Context location are used
945 - Results accessible via context.getGlobalData(label)
946 - Global spectrum = direct beam + diffuse (sky) radiation
947 - Most useful for plant canopy modeling and photosynthesis calculations
950 >>> with Context() as context:
951 ... context.setDate(2023, 6, 21)
952 ... context.setTime(12, 0)
953 ... with SolarPosition(context) as solar:
954 ... solar.calculateGlobalSolarSpectrum("global_spectrum", resolution_nm=10.0)
955 ... spectrum = context.getGlobalData("global_spectrum")
956 ... # spectrum is list of vec2(wavelength_nm, irradiance_W_m2_nm)
957 ... total_irradiance = sum([s.y for s in spectrum]) * 10.0 # Integrate
960 raise ValueError(
"Label cannot be empty")
961 if resolution_nm < 1.0
or resolution_nm > 2300.0:
962 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
966 solar_wrapper.calculateGlobalSolarSpectrum(self.
_solar_pos, label, resolution_nm)
967 except Exception
as e:
972 Check if SolarPosition is available in current build.
975 True if plugin is available, False otherwise
977 registry = get_plugin_registry()
978 return registry.is_plugin_available(
'solarposition')
983 latitude: Optional[float] =
None, longitude: Optional[float] =
None) -> SolarPosition:
985 Create SolarPosition instance with context and optional coordinates.
988 context: Helios Context
989 utc_offset: UTC time offset in hours (optional)
990 latitude: Latitude in degrees (optional)
991 longitude: Longitude in degrees (optional)
994 SolarPosition instance
997 >>> solar = create_solar_position(context, utc_offset=-8, latitude=38.5, longitude=-121.7)
Exception raised for SolarPosition-specific errors.
High-level interface for solar position calculations and radiation modeling.
_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.
__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.