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__)
27 Context manager that temporarily changes working directory to where SolarPosition assets are located.
29 SolarPosition C++ code opens the Prague sky model dataset through the hardcoded relative path
30 "plugins/solarposition/lib/prague_sky_model/PragueSkyModelReduced.dat", so it is only resolvable
31 when the process is running from the build directory. This manager temporarily changes to the
32 build directory where the dataset actually lives.
35 RuntimeError: If the build directory or the Prague dataset are not found, indicating a
38 asset_manager = get_asset_manager()
39 working_dir = asset_manager._get_helios_build_path()
41 if working_dir
and working_dir.exists():
42 solarposition_assets = working_dir /
'plugins' /
'solarposition'
45 current_dir = Path(__file__).parent
46 packaged_build = current_dir /
'assets' /
'build'
48 if packaged_build.exists():
49 working_dir = packaged_build
50 solarposition_assets = working_dir /
'plugins' /
'solarposition'
53 repo_root = current_dir.parent
54 build_lib_dir = repo_root /
'pyhelios_build' /
'build' /
'lib'
55 working_dir = build_lib_dir.parent
56 solarposition_assets = working_dir /
'plugins' /
'solarposition'
58 if not build_lib_dir.exists():
60 f
"PyHelios build directory not found at {build_lib_dir}. "
61 f
"Run: build_scripts/build_helios --clean"
64 prague_dataset = (solarposition_assets /
'lib' /
'prague_sky_model' /
65 'PragueSkyModelReduced.dat')
66 if not prague_dataset.exists():
68 f
"Prague sky model dataset not found at {prague_dataset}. "
69 f
"This indicates a build system error. The build script should copy the "
70 f
"~26 MB dataset to this location. "
71 f
"Rebuild with: build_scripts/build_helios --clean"
74 original_dir = os.getcwd()
77 logger.debug(f
"Changed working directory to {working_dir} for SolarPosition asset access")
80 os.chdir(original_dir)
81 logger.debug(f
"Restored working directory to {original_dir}")
85 """Exception raised for SolarPosition-specific errors"""
91 High-level interface for solar position calculations and radiation modeling.
93 SolarPosition provides comprehensive solar angle calculations, radiation flux
94 modeling, sunrise/sunset time calculations, and atmospheric turbidity calibration.
95 The plugin automatically uses Context time/date for calculations or can be
96 initialized with explicit coordinates.
98 This class requires the native Helios library built with SolarPosition support.
99 Use context managers for proper resource cleanup.
102 Basic usage with Context coordinates:
103 >>> with Context() as context:
104 ... context.setDate(2023, 6, 21) # Summer solstice
105 ... context.setTime(12, 0) # Solar noon
106 ... with SolarPosition(context) as solar:
107 ... elevation = solar.getSunElevation()
108 ... print(f"Sun elevation: {elevation:.1f}°")
110 Usage with explicit coordinates:
111 >>> with Context() as context:
112 ... # Davis, California coordinates
113 ... with SolarPosition(context, utc_offset=-8, latitude=38.5, longitude=-121.7) as solar:
114 ... azimuth = solar.getSunAzimuth()
115 ... flux = solar.getSolarFlux(101325, 288.15, 0.6, 0.1)
116 ... print(f"Solar flux: {flux:.1f} W/m²")
119 def __init__(self, context: Context, utc_offset: Optional[float] =
None,
120 latitude: Optional[float] =
None, longitude: Optional[float] =
None):
122 Initialize SolarPosition with a Helios context.
125 context: Active Helios Context instance
126 utc_offset: UTC time offset in hours (-14 to +12). Helios counts the offset
127 positive moving West, which inverts the real-world UTC-12..UTC+14
128 span, so the range is asymmetric. If provided with
129 latitude/longitude, creates plugin with explicit coordinates.
130 latitude: Latitude in degrees (-90 to +90). Required if utc_offset provided.
131 longitude: Longitude in degrees (-180 to +180). Required if utc_offset provided.
134 SolarPositionError: If plugin not available in current build
135 ValueError: If coordinate parameters are invalid or incomplete
136 RuntimeError: If plugin initialization fails
139 If coordinates are not provided, the plugin uses Context location settings.
140 Solar calculations depend on Context time/date - use context.setTime() and
141 context.setDate() to set the simulation time before calculations.
144 registry = get_plugin_registry()
145 if not registry.is_plugin_available(
'solarposition'):
147 "SolarPosition not available in current Helios library. "
148 "SolarPosition plugin availability depends on build configuration.\n"
150 "System requirements:\n"
151 " - Platforms: Windows, Linux, macOS\n"
152 " - Dependencies: None\n"
153 " - GPU: Not required\n"
155 "If you're seeing this error, the SolarPosition plugin may not be "
156 "properly compiled into your Helios library. Please rebuild PyHelios:\n"
157 " build_scripts/build_helios --clean"
161 if utc_offset
is not None or latitude
is not None or longitude
is not None:
163 if utc_offset
is None or latitude
is None or longitude
is None:
165 "If specifying coordinates, all three parameters must be provided: "
166 "utc_offset, latitude, longitude"
173 if utc_offset < -14.0
or utc_offset > 12.0:
174 raise ValueError(f
"UTC offset must be between -14 and +12 hours, got: {utc_offset}")
175 if latitude < -90.0
or latitude > 90.0:
176 raise ValueError(f
"Latitude must be between -90 and +90 degrees, got: {latitude}")
177 if longitude < -180.0
or longitude > 180.0:
178 raise ValueError(f
"Longitude must be between -180 and +180 degrees, got: {longitude}")
182 self.
_solar_pos = solar_wrapper.createSolarPositionWithCoordinates(
183 context.getNativePtr(), utc_offset, latitude, longitude
188 self.
_solar_pos = solar_wrapper.createSolarPosition(context.getNativePtr())
194 """Raise if the owning Context has been destroyed (see Context.check_context_alive)."""
195 check_context_alive(getattr(self,
"context",
None),
"SolarPosition")
198 """Context manager entry"""
201 def __exit__(self, exc_type, exc_val, exc_tb):
202 """Context manager exit - cleanup resources"""
203 if hasattr(self,
'_solar_pos')
and self.
_solar_pos:
204 solar_wrapper.destroySolarPosition(self.
_solar_pos)
208 """Destructor to ensure C++ resources freed even without 'with' statement."""
209 if hasattr(self,
'_solar_pos')
and self.
_solar_pos is not None:
211 solar_wrapper.destroySolarPosition(self.
_solar_pos)
213 except Exception
as e:
215 warnings.warn(f
"Error in SolarPosition.__del__: {e}")
219 humidity_rel: float, turbidity: float) ->
None:
221 Set atmospheric conditions for subsequent flux calculations (modern API).
223 This method sets global atmospheric conditions in the Context that are used
224 by parameter-free flux methods (modern API). Once set, you can call getSolarFlux(),
225 getSolarFluxPAR(), etc. without passing atmospheric parameters.
228 pressure_Pa: Atmospheric pressure in Pascals (e.g., 101325 for sea level)
229 temperature_K: Temperature in Kelvin (e.g., 288.15 for 15°C)
230 humidity_rel: Relative humidity as fraction (0.0-1.0)
231 turbidity: Atmospheric turbidity coefficient (typically 0.02-0.5)
234 ValueError: If atmospheric parameters are out of valid ranges
235 SolarPositionError: If operation fails
238 This is the modern API pattern. Atmospheric conditions are stored in Context
239 global data and reused by all parameter-free flux methods until changed.
242 >>> # Modern API (set once, use many times)
243 >>> with Context() as context:
244 ... with SolarPosition(context) as solar:
245 ... solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
246 ... flux = solar.getSolarFlux() # No parameters needed
247 ... par = solar.getSolarFluxPAR() # Uses same conditions
248 ... diffuse = solar.getDiffuseFraction() # Uses same conditions
251 if pressure_Pa < 0.0:
252 raise ValueError(f
"Atmospheric pressure must be non-negative, got: {pressure_Pa}")
253 if temperature_K < 0.0:
254 raise ValueError(f
"Temperature must be non-negative, got: {temperature_K}")
255 if humidity_rel < 0.0
or humidity_rel > 1.0:
256 raise ValueError(f
"Relative humidity must be between 0 and 1, got: {humidity_rel}")
258 raise ValueError(f
"Turbidity must be non-negative, got: {turbidity}")
262 solar_wrapper.setAtmosphericConditions(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
263 except Exception
as e:
268 Get currently set atmospheric conditions from Context.
271 Tuple of (pressure_Pa, temperature_K, humidity_rel, turbidity)
274 SolarPositionError: If operation fails
277 If atmospheric conditions have not been set via setAtmosphericConditions(),
278 returns default values: (101325 Pa, 300 K, 0.5, 0.02)
281 >>> pressure, temp, humidity, turbidity = solar.getAtmosphericConditions()
282 >>> print(f"Pressure: {pressure} Pa, Temp: {temp} K")
286 return solar_wrapper.getAtmosphericConditions(self.
_solar_pos)
287 except Exception
as e:
293 Get the sun elevation angle in radians.
296 Sun elevation angle in radians (0 = horizon, pi/2 = zenith)
299 SolarPositionError: If calculation fails
303 >>> elevation = solar.getSunElevation()
304 >>> print(f"Sun is {math.degrees(elevation):.1f}° above horizon")
308 return solar_wrapper.getSunElevation(self.
_solar_pos)
309 except Exception
as e:
314 Get the sun zenith angle in radians.
317 Sun zenith angle in radians (0 = zenith, pi/2 = horizon)
320 SolarPositionError: If calculation fails
324 >>> zenith = solar.getSunZenith()
325 >>> print(f"Sun zenith angle: {math.degrees(zenith):.1f}°")
329 return solar_wrapper.getSunZenith(self.
_solar_pos)
330 except Exception
as e:
335 Get the sun azimuth angle in radians.
338 Sun azimuth angle in radians (0 = North, pi/2 = East, pi = South,
342 SolarPositionError: If calculation fails
346 >>> azimuth = solar.getSunAzimuth()
347 >>> print(f"Sun azimuth: {math.degrees(azimuth):.1f}° (compass bearing)")
351 return solar_wrapper.getSunAzimuth(self.
_solar_pos)
352 except Exception
as e:
358 Get the sun direction as a 3D unit vector.
361 vec3 representing the sun direction vector (x, y, z)
364 SolarPositionError: If calculation fails
367 >>> direction = solar.getSunDirectionVector()
368 >>> print(f"Sun direction vector: ({direction.x:.3f}, {direction.y:.3f}, {direction.z:.3f})")
372 direction_list = solar_wrapper.getSunDirectionVector(self.
_solar_pos)
373 return vec3(direction_list[0], direction_list[1], direction_list[2])
374 except Exception
as e:
379 Get the sun direction as spherical coordinates.
382 SphericalCoord with radius=1, elevation and azimuth in radians
385 SolarPositionError: If calculation fails
388 >>> spherical = solar.getSunDirectionSpherical()
389 >>> print(f"Spherical: r={spherical.radius}, elev={spherical.elevation:.3f}, az={spherical.azimuth:.3f}")
393 spherical_list = solar_wrapper.getSunDirectionSpherical(self.
_solar_pos)
395 radius=spherical_list[0],
396 elevation=spherical_list[1],
397 azimuth=spherical_list[2]
399 except Exception
as e:
404 Override the computed solar position with a prescribed sun direction.
406 By default the sun position is computed from the date, time and location
407 set in the Context. Calling this method overrides that calculation, so
408 all subsequent sun queries (elevation, zenith, azimuth, direction
409 vectors) and flux calculations use the prescribed direction instead.
412 sundirection: SphericalCoord giving the direction of the sun.
413 Elevation and azimuth are in radians.
416 ValueError: If sundirection is not a SphericalCoord
417 SolarPositionError: If the override fails
420 >>> from pyhelios.types import SphericalCoord
422 >>> solar.setSunDirection(SphericalCoord(1.0, math.radians(45), math.radians(180)))
423 >>> math.degrees(solar.getSunElevation())
426 if not isinstance(sundirection, SphericalCoord):
428 f
"Sun direction must be a SphericalCoord, got {type(sundirection).__name__}"
433 solar_wrapper.setSunDirection(
436 sundirection.elevation,
437 sundirection.azimuth,
439 except Exception
as e:
443 def getSolarFlux(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
444 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
446 Calculate total solar flux (supports legacy and modern APIs).
448 This method supports both legacy and modern APIs:
449 - **Legacy API**: Pass all 4 atmospheric parameters explicitly
450 - **Modern API**: Pass no parameters, uses atmospheric conditions from setAtmosphericConditions()
453 pressure_Pa: Atmospheric pressure in Pascals (e.g., 101325 for sea level) [optional]
454 temperature_K: Temperature in Kelvin (e.g., 288.15 for 15°C) [optional]
455 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
456 turbidity: Atmospheric turbidity coefficient (typically 0.02-0.5) [optional]
459 Total solar flux in W/m²
462 ValueError: If some parameters provided but not all, or if values are invalid
463 SolarPositionError: If calculation fails or atmospheric conditions not set (modern API)
466 Legacy API (backward compatible):
467 >>> flux = solar.getSolarFlux(101325, 288.15, 0.6, 0.1)
469 Modern API (cleaner, reuses atmospheric state):
470 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
471 >>> flux = solar.getSolarFlux() # No parameters needed
474 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
475 humidity_rel
is not None, turbidity
is not None]
477 if all(params_provided):
481 return solar_wrapper.getSolarFlux(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
482 except Exception
as e:
485 elif not any(params_provided):
489 return solar_wrapper.getSolarFluxFromState(self.
_solar_pos)
490 except Exception
as e:
492 f
"Failed to calculate solar flux from atmospheric state: {e}\n"
493 "Hint: Call setAtmosphericConditions() first to use parameter-free API, "
494 "or provide all 4 atmospheric parameters for legacy API."
500 "Either provide all atmospheric parameters (pressure_Pa, temperature_K, humidity_rel, turbidity) "
501 "or provide none to use atmospheric conditions from setAtmosphericConditions(). "
502 "Partial parameter sets are not supported."
505 def getSolarFluxPAR(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
506 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
508 Calculate PAR (Photosynthetically Active Radiation) solar flux.
510 Supports both legacy (parameter-based) and modern (state-based) APIs.
513 pressure_Pa: Atmospheric pressure in Pascals [optional]
514 temperature_K: Temperature in Kelvin [optional]
515 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
516 turbidity: Atmospheric turbidity coefficient [optional]
519 PAR solar flux in W/m² (wavelength range ~400-700 nm)
522 ValueError: If some parameters provided but not all
523 SolarPositionError: If calculation fails
526 Legacy: par_flux = solar.getSolarFluxPAR(101325, 288.15, 0.6, 0.1)
527 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
528 par_flux = solar.getSolarFluxPAR()
530 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
531 humidity_rel
is not None, turbidity
is not None]
533 if all(params_provided):
536 return solar_wrapper.getSolarFluxPAR(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
537 except Exception
as e:
539 elif not any(params_provided):
542 return solar_wrapper.getSolarFluxPARFromState(self.
_solar_pos)
543 except Exception
as e:
545 f
"Failed to calculate PAR flux from atmospheric state: {e}\n"
546 "Hint: Call setAtmosphericConditions() first."
549 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
551 def getSolarFluxNIR(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
552 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
554 Calculate NIR (Near-Infrared) solar flux.
556 Supports both legacy (parameter-based) and modern (state-based) APIs.
559 pressure_Pa: Atmospheric pressure in Pascals [optional]
560 temperature_K: Temperature in Kelvin [optional]
561 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
562 turbidity: Atmospheric turbidity coefficient [optional]
565 NIR solar flux in W/m² (wavelength range >700 nm)
568 ValueError: If some parameters provided but not all
569 SolarPositionError: If calculation fails
572 Legacy: nir_flux = solar.getSolarFluxNIR(101325, 288.15, 0.6, 0.1)
573 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
574 nir_flux = solar.getSolarFluxNIR()
576 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
577 humidity_rel
is not None, turbidity
is not None]
579 if all(params_provided):
582 return solar_wrapper.getSolarFluxNIR(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
583 except Exception
as e:
585 elif not any(params_provided):
588 return solar_wrapper.getSolarFluxNIRFromState(self.
_solar_pos)
589 except Exception
as e:
591 f
"Failed to calculate NIR flux from atmospheric state: {e}\n"
592 "Hint: Call setAtmosphericConditions() first."
595 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
597 def getDiffuseFraction(self, pressure_Pa: Optional[float] =
None, temperature_K: Optional[float] =
None,
598 humidity_rel: Optional[float] =
None, turbidity: Optional[float] =
None) -> float:
600 Calculate the diffuse fraction of solar radiation.
602 Supports both legacy (parameter-based) and modern (state-based) APIs.
605 pressure_Pa: Atmospheric pressure in Pascals [optional]
606 temperature_K: Temperature in Kelvin [optional]
607 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
608 turbidity: Atmospheric turbidity coefficient [optional]
611 Diffuse fraction as ratio (0.0-1.0) where:
612 - 0.0 = all direct radiation
613 - 1.0 = all diffuse radiation
616 ValueError: If some parameters provided but not all
617 SolarPositionError: If calculation fails
620 Legacy: diffuse = solar.getDiffuseFraction(101325, 288.15, 0.6, 0.1)
621 Modern: solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
622 diffuse = solar.getDiffuseFraction()
624 params_provided = [pressure_Pa
is not None, temperature_K
is not None,
625 humidity_rel
is not None, turbidity
is not None]
627 if all(params_provided):
630 return solar_wrapper.getDiffuseFraction(self.
_solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
631 except Exception
as e:
633 elif not any(params_provided):
636 return solar_wrapper.getDiffuseFractionFromState(self.
_solar_pos)
637 except Exception
as e:
639 f
"Failed to calculate diffuse fraction from atmospheric state: {e}\n"
640 "Hint: Call setAtmosphericConditions() first."
643 raise ValueError(
"Provide all atmospheric parameters or none (use setAtmosphericConditions()).")
646 humidity_rel: Optional[float] =
None) -> float:
648 Calculate the ambient (sky) longwave radiation flux.
650 This method supports both legacy and modern APIs:
651 - **Legacy API**: Pass temperature and humidity explicitly
652 - **Modern API**: Pass no parameters, uses atmospheric conditions from setAtmosphericConditions()
655 temperature_K: Temperature in Kelvin [optional]
656 humidity_rel: Relative humidity as fraction (0.0-1.0) [optional]
659 Ambient longwave flux in W/m²
662 ValueError: If one parameter provided but not the other
663 SolarPositionError: If calculation fails
666 The longwave flux model is based on Prata (1996).
667 Returns downwelling longwave radiation flux on a horizontal surface.
671 >>> lw_flux = solar.getAmbientLongwaveFlux(288.15, 0.6)
673 Modern API (uses temperature and humidity from setAtmosphericConditions):
674 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
675 >>> lw_flux = solar.getAmbientLongwaveFlux()
677 params_provided = [temperature_K
is not None, humidity_rel
is not None]
679 if all(params_provided):
686 saved_conditions = solar_wrapper.getAtmosphericConditions(self.
_solar_pos)
690 solar_wrapper.setAtmosphericConditions(self.
_solar_pos,
697 result = solar_wrapper.getAmbientLongwaveFluxFromState(self.
_solar_pos)
700 solar_wrapper.setAtmosphericConditions(self.
_solar_pos, *saved_conditions)
704 except Exception
as e:
707 elif not any(params_provided):
711 return solar_wrapper.getAmbientLongwaveFluxFromState(self.
_solar_pos)
712 except Exception
as e:
714 f
"Failed to calculate ambient longwave flux from atmospheric state: {e}\n"
715 "Hint: Call setAtmosphericConditions() first to use parameter-free API, "
716 "or provide temperature_K and humidity_rel for legacy API."
722 "Either provide both temperature_K and humidity_rel, "
723 "or provide neither to use atmospheric conditions from setAtmosphericConditions()."
729 Calculate sunrise time for the current date and location.
732 Time object with sunrise time (hour, minute, second)
735 SolarPositionError: If calculation fails
738 >>> sunrise = solar.getSunriseTime()
739 >>> print(f"Sunrise: {sunrise}") # Prints as HH:MM:SS
743 hour, minute, second = solar_wrapper.getSunriseTime(self.
_solar_pos)
744 return Time(hour, minute, second)
745 except Exception
as e:
750 Calculate sunset time for the current date and location.
753 Time object with sunset time (hour, minute, second)
756 SolarPositionError: If calculation fails
759 >>> sunset = solar.getSunsetTime()
760 >>> print(f"Sunset: {sunset}") # Prints as HH:MM:SS
764 hour, minute, second = solar_wrapper.getSunsetTime(self.
_solar_pos)
765 return Time(hour, minute, second)
766 except Exception
as e:
772 Calibrate atmospheric turbidity using timeseries data.
775 timeseries_label: Label of timeseries data in Context. The data should
776 be global shortwave radiation flux on a horizontal plane in W/m^2,
777 and should contain at least one day of clear-sky conditions.
780 The calibrated turbidity value
783 ValueError: If timeseries label is invalid
784 SolarPositionError: If calibration fails
787 This method does not itself apply the calibrated value. Pass the
788 returned turbidity to setAtmosphericConditions() to use it.
791 >>> turbidity = solar.calibrateTurbidityFromTimeseries("solar_irradiance")
792 >>> solar.setAtmosphericConditions(101325, 293.15, 0.5, turbidity)
794 if not timeseries_label:
795 raise ValueError(
"Timeseries label cannot be empty")
799 return solar_wrapper.calibrateTurbidityFromTimeseries(self.
_solar_pos, timeseries_label)
800 except Exception
as e:
805 Enable cloud calibration using timeseries data.
808 timeseries_label: Label of cloud timeseries data in Context
811 ValueError: If timeseries label is invalid
812 SolarPositionError: If calibration setup fails
815 >>> solar.enableCloudCalibration("cloud_cover")
817 if not timeseries_label:
818 raise ValueError(
"Timeseries label cannot be empty")
822 solar_wrapper.enableCloudCalibration(self.
_solar_pos, timeseries_label)
823 except Exception
as e:
828 Disable cloud calibration.
831 SolarPositionError: If operation fails
834 >>> solar.disableCloudCalibration()
838 solar_wrapper.disableCloudCalibration(self.
_solar_pos)
839 except Exception
as e:
845 Enable Prague Sky Model for physically-based sky radiance calculations.
847 The Prague Sky Model provides high-quality spectral and angular sky radiance
848 distribution for accurate diffuse radiation modeling. It accounts for Rayleigh
849 and Mie scattering to produce realistic sky radiance patterns across the
850 360-1480 nm spectral range.
853 SolarPositionError: If operation fails
856 After enabling, call updatePragueSkyModel() to compute and store spectral-angular
857 parameters in Context global data. Requires ~27 MB data file:
858 plugins/solarposition/lib/prague_sky_model/PragueSkyModelReduced.dat
861 >>> with Context() as context:
862 ... with SolarPosition(context) as solar:
863 ... solar.enablePragueSkyModel()
864 ... solar.updatePragueSkyModel()
870 except Exception
as e:
875 Check if Prague Sky Model is currently enabled.
878 True if Prague Sky Model has been enabled via enablePragueSkyModel(), False otherwise
881 SolarPositionError: If operation fails
884 >>> if solar.isPragueSkyModelEnabled():
885 ... print("Prague Sky Model is active")
889 return solar_wrapper.isPragueSkyModelEnabled(self.
_solar_pos)
890 except Exception
as e:
895 Update Prague Sky Model and store spectral-angular parameters in Context.
897 This is a computationally intensive operation (~1100 model queries with OpenMP
898 parallelization) that computes sky radiance distribution for current atmospheric
899 and solar conditions. Use pragueSkyModelNeedsUpdate() for lazy evaluation to
900 avoid unnecessary updates.
903 ground_albedo: Ground surface albedo (default: 0.33 for typical soil/vegetation)
906 SolarPositionError: If update fails
909 Reads turbidity from Context atmospheric conditions. Stores results in Context
910 global data as "prague_sky_spectral_params" (1350 floats: 225 wavelengths × 6 params),
911 "prague_sky_sun_direction", "prague_sky_visibility_km", "prague_sky_ground_albedo",
912 and "prague_sky_valid" flag.
915 >>> solar.setAtmosphericConditions(101325, 288.15, 0.6, 0.1)
916 >>> solar.updatePragueSkyModel(ground_albedo=0.25)
921 solar_wrapper.updatePragueSkyModel(self.
_solar_pos, ground_albedo)
922 except Exception
as e:
926 sun_tolerance: float = 0.01,
927 turbidity_tolerance: float = 0.02,
928 albedo_tolerance: float = 0.05) -> bool:
930 Check if Prague Sky Model needs updating based on changed conditions.
932 Enables lazy evaluation to avoid expensive Prague updates when conditions haven't
933 changed significantly. Compares current state against cached values.
936 ground_albedo: Current ground albedo (default: 0.33)
937 sun_tolerance: Threshold for sun direction changes (default: 0.01 ≈ 0.57°)
938 turbidity_tolerance: Relative threshold for turbidity (default: 0.02 = 2%)
939 albedo_tolerance: Threshold for albedo changes (default: 0.05 = 5%)
942 True if updatePragueSkyModel() should be called, False if cached data is valid
945 SolarPositionError: If check fails
948 Reads turbidity from Context atmospheric conditions for comparison.
951 >>> if solar.pragueSkyModelNeedsUpdate():
952 ... solar.updatePragueSkyModel()
956 return solar_wrapper.pragueSkyModelNeedsUpdate(self.
_solar_pos, ground_albedo,
957 sun_tolerance, turbidity_tolerance,
959 except Exception
as e:
965 Calculate direct beam solar spectrum using SSolar-GOA model.
967 Computes the spectral irradiance of direct beam solar radiation across
968 300-2600 nm wavelength range using the SSolar-GOA (Global Ozone and
969 Atmospheric) spectral model. Results are stored in Context global data
970 as a vector of (wavelength, irradiance) pairs.
973 label: Label to store the spectrum data in Context global data
974 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
975 Lower values give finer spectral resolution but require
976 more computation. Default is 1.0 nm.
979 ValueError: If label is empty or resolution is out of valid range
980 SolarPositionError: If calculation fails
983 - Requires Context time/date to be set for accurate solar position
984 - Atmospheric parameters from Context location are used
985 - Results accessible via context.getGlobalData(label)
986 - SSolar-GOA model accounts for atmospheric absorption and scattering
989 >>> with Context() as context:
990 ... context.setDate(2023, 6, 21)
991 ... context.setTime(12, 0)
992 ... with SolarPosition(context) as solar:
993 ... solar.calculateDirectSolarSpectrum("direct_spectrum", resolution_nm=5.0)
994 ... spectrum = context.getGlobalData("direct_spectrum")
995 ... # spectrum is list of vec2(wavelength_nm, irradiance_W_m2_nm)
998 raise ValueError(
"Label cannot be empty")
999 if resolution_nm < 1.0
or resolution_nm > 2300.0:
1000 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
1004 solar_wrapper.calculateDirectSolarSpectrum(self.
_solar_pos, label, resolution_nm)
1005 except Exception
as e:
1010 Calculate diffuse solar spectrum using SSolar-GOA model.
1012 Computes the spectral irradiance of diffuse (scattered) solar radiation
1013 across 300-2600 nm wavelength range using the SSolar-GOA model. Results
1014 are stored in Context global data as a vector of (wavelength, irradiance) pairs.
1017 label: Label to store the spectrum data in Context global data
1018 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
1019 Lower values give finer spectral resolution but require
1020 more computation. Default is 1.0 nm.
1023 ValueError: If label is empty or resolution is out of valid range
1024 SolarPositionError: If calculation fails
1027 - Requires Context time/date to be set for accurate solar position
1028 - Atmospheric parameters from Context location are used
1029 - Results accessible via context.getGlobalData(label)
1030 - Diffuse radiation results from atmospheric scattering (Rayleigh, aerosol)
1033 >>> with Context() as context:
1034 ... context.setDate(2023, 6, 21)
1035 ... context.setTime(12, 0)
1036 ... with SolarPosition(context) as solar:
1037 ... solar.calculateDiffuseSolarSpectrum("diffuse_spectrum", resolution_nm=5.0)
1038 ... spectrum = context.getGlobalData("diffuse_spectrum")
1039 ... # spectrum is list of vec2(wavelength_nm, irradiance_W_m2_nm)
1042 raise ValueError(
"Label cannot be empty")
1043 if resolution_nm < 1.0
or resolution_nm > 2300.0:
1044 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
1048 solar_wrapper.calculateDiffuseSolarSpectrum(self.
_solar_pos, label, resolution_nm)
1049 except Exception
as e:
1054 Calculate global (total) solar spectrum using SSolar-GOA model.
1056 Computes the spectral irradiance of total solar radiation (direct + diffuse)
1057 across 300-2600 nm wavelength range using the SSolar-GOA model. Results
1058 are stored in Context global data as a vector of (wavelength, irradiance) pairs.
1061 label: Label to store the spectrum data in Context global data
1062 resolution_nm: Wavelength resolution in nanometers (1.0-2300.0).
1063 Lower values give finer spectral resolution but require
1064 more computation. Default is 1.0 nm.
1067 ValueError: If label is empty or resolution is out of valid range
1068 SolarPositionError: If calculation fails
1071 - Requires Context time/date to be set for accurate solar position
1072 - Atmospheric parameters from Context location are used
1073 - Results accessible via context.getGlobalData(label)
1074 - Global spectrum = direct beam + diffuse (sky) radiation
1075 - Most useful for plant canopy modeling and photosynthesis calculations
1078 >>> with Context() as context:
1079 ... context.setDate(2023, 6, 21)
1080 ... context.setTime(12, 0)
1081 ... with SolarPosition(context) as solar:
1082 ... solar.calculateGlobalSolarSpectrum("global_spectrum", resolution_nm=10.0)
1083 ... spectrum = context.getGlobalData("global_spectrum")
1084 ... # spectrum is list of vec2(wavelength_nm, irradiance_W_m2_nm)
1085 ... total_irradiance = sum([s.y for s in spectrum]) * 10.0 # Integrate
1088 raise ValueError(
"Label cannot be empty")
1089 if resolution_nm < 1.0
or resolution_nm > 2300.0:
1090 raise ValueError(f
"Wavelength resolution must be between 1 and 2300 nm, got: {resolution_nm}")
1094 solar_wrapper.calculateGlobalSolarSpectrum(self.
_solar_pos, label, resolution_nm)
1095 except Exception
as e:
1100 Check if SolarPosition is available in current build.
1103 True if plugin is available, False otherwise
1105 registry = get_plugin_registry()
1106 return registry.is_plugin_available(
'solarposition')
1111 latitude: Optional[float] =
None, longitude: Optional[float] =
None) -> SolarPosition:
1113 Create SolarPosition instance with context and optional coordinates.
1116 context: Helios Context
1117 utc_offset: UTC time offset in hours (optional)
1118 latitude: Latitude in degrees (optional)
1119 longitude: Longitude in degrees (optional)
1122 SolarPosition instance
1125 >>> solar = create_solar_position(context, utc_offset=-8, latitude=38.5, longitude=-121.7)
1127 return SolarPosition(context, utc_offset, latitude, longitude)
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.
float calibrateTurbidityFromTimeseries(self, str timeseries_label)
Calibrate atmospheric turbidity using timeseries data.
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 radians.
float getSunAzimuth(self)
Get the sun azimuth angle in radians.
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 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 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.
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 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_working_directory()
Context manager that temporarily changes working directory to where SolarPosition assets are located.
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.