0.1.8
Loading...
Searching...
No Matches
SolarPosition.py
Go to the documentation of this file.
1"""
2SolarPosition - High-level interface for solar position and radiation calculations
3
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.
7"""
8
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
15
16
18 """Exception raised for SolarPosition-specific errors"""
19 pass
20
21
22class SolarPosition:
23 """
24 High-level interface for solar position calculations and radiation modeling.
25
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.
30
31 This class requires the native Helios library built with SolarPosition support.
32 Use context managers for proper resource cleanup.
33
34 Examples:
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}°")
42
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²")
50 """
51
52 def __init__(self, context: Context, utc_offset: Optional[float] = None,
53 latitude: Optional[float] = None, longitude: Optional[float] = None):
54 """
55 Initialize SolarPosition with a Helios context.
57 Args:
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.
63
64 Raises:
65 SolarPositionError: If plugin not available in current build
66 ValueError: If coordinate parameters are invalid or incomplete
67 RuntimeError: If plugin initialization fails
68
69 Note:
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.
73 """
74 # Check plugin availability
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"
80 "\n"
81 "System requirements:\n"
82 " - Platforms: Windows, Linux, macOS\n"
83 " - Dependencies: None\n"
84 " - GPU: Not required\n"
85 "\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"
89 )
90
91 # Validate coordinate parameters
92 if utc_offset is not None or latitude is not None or longitude is not None:
93 # If any coordinate parameter is provided, all must be provided
94 if utc_offset is None or latitude is None or longitude is None:
95 raise ValueError(
96 "If specifying coordinates, all three parameters must be provided: "
97 "utc_offset, latitude, longitude"
98 )
99
100 # Validate coordinate ranges
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}")
107
108 # Create with explicit coordinates
109 self.context = context
110 self._solar_pos = solar_wrapper.createSolarPositionWithCoordinates(
111 context.getNativePtr(), utc_offset, latitude, longitude
112 )
113 else:
114 # Create using Context location
115 self.context = context
116 self._solar_pos = solar_wrapper.createSolarPosition(context.getNativePtr())
117
118 if not self._solar_pos:
119 raise SolarPositionError("Failed to initialize SolarPosition")
120
121 def __enter__(self):
122 """Context manager entry"""
123 return self
124
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)
129 self._solar_pos = None
130
131 # Solar angle calculations
132 def getSunElevation(self) -> float:
133 """
134 Get the sun elevation angle in degrees.
135
136 Returns:
137 Sun elevation angle in degrees (0° = horizon, 90° = zenith)
138
139 Raises:
140 SolarPositionError: If calculation fails
141
142 Example:
143 >>> elevation = solar.getSunElevation()
144 >>> print(f"Sun is {elevation:.1f}° above horizon")
145 """
146 try:
147 return solar_wrapper.getSunElevation(self._solar_pos)
148 except Exception as e:
149 raise SolarPositionError(f"Failed to get sun elevation: {e}")
150
151 def getSunZenith(self) -> float:
152 """
153 Get the sun zenith angle in degrees.
154
155 Returns:
156 Sun zenith angle in degrees (0° = zenith, 90° = horizon)
157
158 Raises:
159 SolarPositionError: If calculation fails
160
161 Example:
162 >>> zenith = solar.getSunZenith()
163 >>> print(f"Sun zenith angle: {zenith:.1f}°")
164 """
165 try:
166 return solar_wrapper.getSunZenith(self._solar_pos)
167 except Exception as e:
168 raise SolarPositionError(f"Failed to get sun zenith: {e}")
169
170 def getSunAzimuth(self) -> float:
171 """
172 Get the sun azimuth angle in degrees.
173
174 Returns:
175 Sun azimuth angle in degrees (0° = North, 90° = East, 180° = South, 270° = West)
176
177 Raises:
178 SolarPositionError: If calculation fails
179
180 Example:
181 >>> azimuth = solar.getSunAzimuth()
182 >>> print(f"Sun azimuth: {azimuth:.1f}° (compass bearing)")
183 """
184 try:
185 return solar_wrapper.getSunAzimuth(self._solar_pos)
186 except Exception as e:
187 raise SolarPositionError(f"Failed to get sun azimuth: {e}")
188
189 # Solar direction vectors
190 def getSunDirectionVector(self) -> vec3:
191 """
192 Get the sun direction as a 3D unit vector.
193
194 Returns:
195 vec3 representing the sun direction vector (x, y, z)
196
197 Raises:
198 SolarPositionError: If calculation fails
199
200 Example:
201 >>> direction = solar.getSunDirectionVector()
202 >>> print(f"Sun direction vector: ({direction.x:.3f}, {direction.y:.3f}, {direction.z:.3f})")
203 """
204 try:
205 direction_list = solar_wrapper.getSunDirectionVector(self._solar_pos)
206 return vec3(direction_list[0], direction_list[1], direction_list[2])
207 except Exception as e:
208 raise SolarPositionError(f"Failed to get sun direction vector: {e}")
209
210 def getSunDirectionSpherical(self) -> SphericalCoord:
211 """
212 Get the sun direction as spherical coordinates.
213
214 Returns:
215 SphericalCoord with radius=1, elevation and azimuth in radians
216
217 Raises:
218 SolarPositionError: If calculation fails
219
220 Example:
221 >>> spherical = solar.getSunDirectionSpherical()
222 >>> print(f"Spherical: r={spherical.radius}, elev={spherical.elevation:.3f}, az={spherical.azimuth:.3f}")
223 """
224 try:
225 spherical_list = solar_wrapper.getSunDirectionSpherical(self._solar_pos)
226 return SphericalCoord(
227 radius=spherical_list[0],
228 elevation=spherical_list[1],
229 azimuth=spherical_list[2]
230 )
231 except Exception as e:
232 raise SolarPositionError(f"Failed to get sun direction spherical: {e}")
233
234 # Solar flux calculations
235 def getSolarFlux(self, pressure_Pa: float, temperature_K: float,
236 humidity_rel: float, turbidity: float) -> float:
237 """
238 Calculate total solar flux with atmospheric parameters.
240 Args:
241 pressure_Pa: Atmospheric pressure in Pascals (e.g., 101325 for sea level)
242 temperature_K: Temperature in Kelvin (e.g., 288.15 for 15°C)
243 humidity_rel: Relative humidity as fraction (0.0-1.0)
244 turbidity: Atmospheric turbidity coefficient (typically 0.05-0.5)
245
246 Returns:
247 Total solar flux in W/m²
248
249 Raises:
250 ValueError: If atmospheric parameters are out of valid ranges
251 SolarPositionError: If calculation fails
252
253 Example:
254 >>> # Standard atmospheric conditions
255 >>> flux = solar.getSolarFlux(101325, 288.15, 0.6, 0.1)
256 >>> print(f"Total solar flux: {flux:.1f} W/m²")
257 """
258 try:
259 return solar_wrapper.getSolarFlux(self._solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
260 except Exception as e:
261 raise SolarPositionError(f"Failed to calculate solar flux: {e}")
262
263 def getSolarFluxPAR(self, pressure_Pa: float, temperature_K: float,
264 humidity_rel: float, turbidity: float) -> float:
265 """
266 Calculate PAR (Photosynthetically Active Radiation) solar flux.
267
268 Args:
269 pressure_Pa: Atmospheric pressure in Pascals
270 temperature_K: Temperature in Kelvin
271 humidity_rel: Relative humidity as fraction (0.0-1.0)
272 turbidity: Atmospheric turbidity coefficient
273
274 Returns:
275 PAR solar flux in W/m² (wavelength range ~400-700 nm)
276
277 Raises:
278 ValueError: If atmospheric parameters are invalid
279 SolarPositionError: If calculation fails
280
281 Example:
282 >>> par_flux = solar.getSolarFluxPAR(101325, 288.15, 0.6, 0.1)
283 >>> print(f"PAR flux: {par_flux:.1f} W/m²")
284 """
285 try:
286 return solar_wrapper.getSolarFluxPAR(self._solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
287 except Exception as e:
288 raise SolarPositionError(f"Failed to calculate PAR flux: {e}")
289
290 def getSolarFluxNIR(self, pressure_Pa: float, temperature_K: float,
291 humidity_rel: float, turbidity: float) -> float:
292 """
293 Calculate NIR (Near-Infrared) solar flux.
294
295 Args:
296 pressure_Pa: Atmospheric pressure in Pascals
297 temperature_K: Temperature in Kelvin
298 humidity_rel: Relative humidity as fraction (0.0-1.0)
299 turbidity: Atmospheric turbidity coefficient
300
301 Returns:
302 NIR solar flux in W/m² (wavelength range >700 nm)
304 Raises:
305 ValueError: If atmospheric parameters are invalid
306 SolarPositionError: If calculation fails
307
308 Example:
309 >>> nir_flux = solar.getSolarFluxNIR(101325, 288.15, 0.6, 0.1)
310 >>> print(f"NIR flux: {nir_flux:.1f} W/m²")
311 """
312 try:
313 return solar_wrapper.getSolarFluxNIR(self._solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
314 except Exception as e:
315 raise SolarPositionError(f"Failed to calculate NIR flux: {e}")
316
317 def getDiffuseFraction(self, pressure_Pa: float, temperature_K: float,
318 humidity_rel: float, turbidity: float) -> float:
319 """
320 Calculate the diffuse fraction of solar radiation.
321
322 Args:
323 pressure_Pa: Atmospheric pressure in Pascals
324 temperature_K: Temperature in Kelvin
325 humidity_rel: Relative humidity as fraction (0.0-1.0)
326 turbidity: Atmospheric turbidity coefficient
327
328 Returns:
329 Diffuse fraction as ratio (0.0-1.0) where:
330 - 0.0 = all direct radiation
331 - 1.0 = all diffuse radiation
333 Raises:
334 ValueError: If atmospheric parameters are invalid
335 SolarPositionError: If calculation fails
336
337 Example:
338 >>> diffuse_fraction = solar.getDiffuseFraction(101325, 288.15, 0.6, 0.1)
339 >>> print(f"Diffuse fraction: {diffuse_fraction:.3f} ({diffuse_fraction*100:.1f}%)")
340 """
341 try:
342 return solar_wrapper.getDiffuseFraction(self._solar_pos, pressure_Pa, temperature_K, humidity_rel, turbidity)
343 except Exception as e:
344 raise SolarPositionError(f"Failed to calculate diffuse fraction: {e}")
345
346 # Time calculations
347 def getSunriseTime(self) -> Time:
348 """
349 Calculate sunrise time for the current date and location.
350
351 Returns:
352 Time object with sunrise time (hour, minute, second)
353
354 Raises:
355 SolarPositionError: If calculation fails
356
357 Example:
358 >>> sunrise = solar.getSunriseTime()
359 >>> print(f"Sunrise: {sunrise}") # Prints as HH:MM:SS
360 """
361 try:
362 hour, minute, second = solar_wrapper.getSunriseTime(self._solar_pos)
363 return Time(hour, minute, second)
364 except Exception as e:
365 raise SolarPositionError(f"Failed to calculate sunrise time: {e}")
366
367 def getSunsetTime(self) -> Time:
368 """
369 Calculate sunset time for the current date and location.
370
371 Returns:
372 Time object with sunset time (hour, minute, second)
373
374 Raises:
375 SolarPositionError: If calculation fails
376
377 Example:
378 >>> sunset = solar.getSunsetTime()
379 >>> print(f"Sunset: {sunset}") # Prints as HH:MM:SS
380 """
381 try:
382 hour, minute, second = solar_wrapper.getSunsetTime(self._solar_pos)
383 return Time(hour, minute, second)
384 except Exception as e:
385 raise SolarPositionError(f"Failed to calculate sunset time: {e}")
387 # Calibration functions
388 def calibrateTurbidityFromTimeseries(self, timeseries_label: str):
389 """
390 Calibrate atmospheric turbidity using timeseries data.
391
392 Args:
393 timeseries_label: Label of timeseries data in Context
394
395 Raises:
396 ValueError: If timeseries label is invalid
397 SolarPositionError: If calibration fails
398
399 Example:
400 >>> solar.calibrateTurbidityFromTimeseries("solar_irradiance")
401 """
402 if not timeseries_label:
403 raise ValueError("Timeseries label cannot be empty")
404
405 try:
406 solar_wrapper.calibrateTurbidityFromTimeseries(self._solar_pos, timeseries_label)
407 except Exception as e:
408 raise SolarPositionError(f"Failed to calibrate turbidity: {e}")
409
410 def enableCloudCalibration(self, timeseries_label: str):
411 """
412 Enable cloud calibration using timeseries data.
413
414 Args:
415 timeseries_label: Label of cloud timeseries data in Context
416
417 Raises:
418 ValueError: If timeseries label is invalid
419 SolarPositionError: If calibration setup fails
420
421 Example:
422 >>> solar.enableCloudCalibration("cloud_cover")
423 """
424 if not timeseries_label:
425 raise ValueError("Timeseries label cannot be empty")
426
427 try:
428 solar_wrapper.enableCloudCalibration(self._solar_pos, timeseries_label)
429 except Exception as e:
430 raise SolarPositionError(f"Failed to enable cloud calibration: {e}")
432 def disableCloudCalibration(self):
433 """
434 Disable cloud calibration.
435
436 Raises:
437 SolarPositionError: If operation fails
438
439 Example:
440 >>> solar.disableCloudCalibration()
441 """
442 try:
443 solar_wrapper.disableCloudCalibration(self._solar_pos)
444 except Exception as e:
445 raise SolarPositionError(f"Failed to disable cloud calibration: {e}")
446
447 # Note: Additional utility functions can be added here as needed
448
449 def is_available(self) -> bool:
450 """
451 Check if SolarPosition is available in current build.
452
453 Returns:
454 True if plugin is available, False otherwise
455 """
456 registry = get_plugin_registry()
457 return registry.is_plugin_available('solarposition')
458
459
460# Convenience function
461def create_solar_position(context: Context, utc_offset: Optional[float] = None,
462 latitude: Optional[float] = None, longitude: Optional[float] = None) -> SolarPosition:
463 """
464 Create SolarPosition instance with context and optional coordinates.
465
466 Args:
467 context: Helios Context
468 utc_offset: UTC time offset in hours (optional)
469 latitude: Latitude in degrees (optional)
470 longitude: Longitude in degrees (optional)
471
472 Returns:
473 SolarPosition instance
474
475 Example:
476 >>> solar = create_solar_position(context, utc_offset=-8, latitude=38.5, longitude=-121.7)
477 """
478 return SolarPosition(context, utc_offset, latitude, longitude)
Exception raised for SolarPosition-specific errors.
High-level interface for solar position calculations and radiation modeling.
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 getSunElevation(self)
Get the sun elevation angle in degrees.
bool is_available(self)
Check if SolarPosition is available in current build.
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.
calibrateTurbidityFromTimeseries(self, str timeseries_label)
Calibrate atmospheric turbidity using timeseries data.
float getSolarFlux(self, float pressure_Pa, float temperature_K, float humidity_rel, float turbidity)
Calculate total solar flux with atmospheric parameters.
float getDiffuseFraction(self, float pressure_Pa, float temperature_K, float humidity_rel, float turbidity)
Calculate the diffuse fraction of solar radiation.
float getSolarFluxPAR(self, float pressure_Pa, float temperature_K, float humidity_rel, float turbidity)
Calculate PAR (Photosynthetically Active Radiation) solar flux.
disableCloudCalibration(self)
Disable cloud calibration.
float getSolarFluxNIR(self, float pressure_Pa, float temperature_K, float humidity_rel, float turbidity)
Calculate NIR (Near-Infrared) solar flux.
vec3 getSunDirectionVector(self)
Get the sun direction as a 3D unit vector.
Exception classes for PyHelios library.
Definition exceptions.py:10
Helios Time structure for representing time values.
Definition DataTypes.py:583
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.