0.1.39
Loading...
Searching...
No Matches
EnergyBalance.py
Go to the documentation of this file.
1"""
2High-level EnergyBalance interface for PyHelios.
3
4This module provides a user-friendly interface to the energy balance modeling
5capabilities with graceful plugin handling and informative error messages.
6"""
7
8import logging
9from typing import List, Optional, Union
10from contextlib import contextmanager
11
12from .plugins.registry import get_plugin_registry
13from .wrappers import UEnergyBalanceWrapper as energy_wrapper
14from .Context import Context, check_context_alive
15from .exceptions import HeliosError
16from .validation.plugin_decorators import (
17 validate_energy_run_params, validate_energy_band_params, validate_air_energy_params,
18 validate_evaluate_air_energy_params, validate_output_data_params, validate_print_report_params
19)
20
21logger = logging.getLogger(__name__)
22
23
25 """Exception raised for EnergyBalance-specific errors."""
26 pass
27
28
30 """
31 High-level interface for energy balance modeling and thermal calculations.
32
33 This class provides a user-friendly wrapper around the native Helios
34 energy balance plugin with automatic plugin availability checking and
35 graceful error handling.
36
37 The energy balance model computes surface temperatures based on local energy
38 balance equations, including radiation absorption, convection, and transpiration.
39 It supports both steady-state and dynamic (time-stepping) calculations.
40
41 System requirements:
42 - NVIDIA GPU with CUDA support
43 - CUDA Toolkit installed
44 - Energy balance plugin compiled into PyHelios
45
46 Example:
47 >>> with Context() as context:
48 ... # Add some geometry
49 ... patch_uuid = context.addPatch(center=[0, 0, 1], size=[1, 1])
50 ...
51 ... with EnergyBalanceModel(context) as energy_balance:
52 ... # Add radiation band for flux calculations
53 ... energy_balance.addRadiationBand("SW")
54 ...
55 ... # Run steady-state energy balance
56 ... energy_balance.run()
57 ...
58 ... # Or run dynamic simulation with timestep
59 ... energy_balance.run(dt=60.0) # 60 second timestep
60 """
61
62 def __init__(self, context: Context):
63 """
64 Initialize EnergyBalanceModel with graceful plugin handling.
65
66 Args:
67 context: Helios Context instance
68
69 Raises:
70 TypeError: If context is not a Context instance
71 EnergyBalanceModelError: If energy balance plugin is not available
72 """
73 # Validate context type
74 if not isinstance(context, Context):
75 raise TypeError(f"EnergyBalanceModel requires a Context instance, got {type(context).__name__}")
76
77 self.context = context
78 self.energy_model = None
79
80 # Check plugin availability using registry
81 registry = get_plugin_registry()
82
83 if not registry.is_plugin_available('energybalance'):
84 # Get helpful information about the missing plugin
85 plugin_info = registry.get_plugin_capabilities()
86 available_plugins = registry.get_available_plugins()
87
88 error_msg = (
89 "EnergyBalanceModel requires the 'energybalance' plugin which is not available.\n\n"
90 "The energy balance plugin provides GPU-accelerated thermal modeling and surface temperature calculations.\n"
91 "System requirements:\n"
92 "- NVIDIA GPU with CUDA support\n"
93 "- CUDA Toolkit installed\n"
94 "- Energy balance plugin compiled into PyHelios\n\n"
95 "To enable energy balance modeling:\n"
96 "1. Build PyHelios with energy balance plugin:\n"
97 " build_scripts/build_helios --plugins energybalance\n"
98 "2. Or build with multiple plugins:\n"
99 " build_scripts/build_helios --plugins energybalance,visualizer,weberpenntree\n"
100 f"\nCurrently available plugins: {available_plugins}"
101 )
102
103 # Suggest alternatives if available
104 alternatives = registry.suggest_alternatives('energybalance')
105 if alternatives:
106 error_msg += f"\n\nAlternative plugins available: {alternatives}"
107 error_msg += "\nConsider using radiation or photosynthesis for related thermal modeling."
108
109 raise EnergyBalanceModelError(error_msg)
110
111 # Plugin is available - create energy balance model
112 try:
113 self.energy_model = energy_wrapper.createEnergyBalanceModel(context.getNativePtr())
114 if self.energy_model is None:
116 "Failed to create EnergyBalanceModel instance. "
117 "This may indicate a problem with the native library or CUDA initialization."
118 )
119 logger.info("EnergyBalanceModel created successfully")
120
121 except Exception as e:
122 raise EnergyBalanceModelError(f"Failed to initialize EnergyBalanceModel: {e}")
123
124 def _check_context_alive(self):
125 """Raise if the owning Context has been destroyed (see Context.check_context_alive)."""
126 check_context_alive(self.context, "EnergyBalanceModel")
128 def __enter__(self):
129 """Context manager entry."""
130 return self
132 def __exit__(self, exc_type, exc_value, traceback):
133 """Context manager exit with proper cleanup."""
134 if self.energy_model is not None:
135 try:
136 energy_wrapper.destroyEnergyBalanceModel(self.energy_model)
137 logger.debug("EnergyBalanceModel destroyed successfully")
138 except Exception as e:
139 logger.warning(f"Error destroying EnergyBalanceModel: {e}")
140 finally:
141 self.energy_model = None
142
143 def __del__(self):
144 """Destructor to ensure C++ resources freed even without 'with' statement."""
145 if hasattr(self, 'energy_model') and self.energy_model is not None:
146 try:
147 energy_wrapper.destroyEnergyBalanceModel(self.energy_model)
148 self.energy_model = None
149 except Exception as e:
150 import warnings
151 warnings.warn(f"Error in EnergyBalanceModel.__del__: {e}")
152
153 def getNativePtr(self):
154 """Get the native pointer for advanced operations."""
155 return self.energy_model
157 def enableMessages(self) -> None:
158 """
159 Enable console output messages from the energy balance model.
160
161 Raises:
162 EnergyBalanceModelError: If operation fails
163 """
165 try:
166 energy_wrapper.enableMessages(self.energy_model)
167 except Exception as e:
168 raise EnergyBalanceModelError(f"Failed to enable messages: {e}")
169
170 def disableMessages(self) -> None:
171 """
172 Disable console output messages from the energy balance model.
173
174 Raises:
175 EnergyBalanceModelError: If operation fails
176 """
178 try:
179 energy_wrapper.disableMessages(self.energy_model)
180 except Exception as e:
181 raise EnergyBalanceModelError(f"Failed to disable messages: {e}")
182
183 @validate_energy_run_params
184 def run(self, uuids: Optional[List[int]] = None, dt: Optional[float] = None) -> None:
185 """
186 Run the energy balance model.
187
188 This method supports multiple execution modes:
189 - Steady state for all primitives: run()
190 - Dynamic with timestep for all primitives: run(dt=60.0)
191 - Steady state for specific primitives: run(uuids=[1, 2, 3])
192 - Dynamic with timestep for specific primitives: run(uuids=[1, 2, 3], dt=60.0)
193
194 Args:
195 uuids: Optional list of primitive UUIDs to process. If None, processes all primitives.
196 dt: Optional timestep in seconds for dynamic simulation. If None, runs steady-state.
197
198 Raises:
199 ValueError: If parameters are invalid
200 EnergyBalanceModelError: If energy balance calculation fails, if a UUID does not
201 exist, or if the same UUID appears more than once in ``uuids``
202 (helios-core 1.3.92+), or if the emitting radiation bands do not account for
203 the blackbody spectrum exactly once -- overlapping bounded bands, bands holding
204 less than 95% of the emission, or more than one emitting band without
205 wavelength bounds (helios-core 1.3.93+)
206
207 Example:
208 >>> # Steady state for all primitives
209 >>> energy_balance.run()
210
211 >>> # Dynamic simulation with 60-second timestep
212 >>> energy_balance.run(dt=60.0)
213
214 >>> # Steady state for specific patches
215 >>> energy_balance.run(uuids=[patch1_uuid, patch2_uuid])
216
217 >>> # Dynamic simulation for specific patches
218 >>> energy_balance.run(uuids=[patch1_uuid, patch2_uuid], dt=30.0)
219 """
221 try:
222 if uuids is None and dt is None:
223 # Steady state for all primitives
224 energy_wrapper.run(self.energy_model)
225 elif uuids is None and dt is not None:
226 # Dynamic with timestep for all primitives
227 energy_wrapper.runDynamic(self.energy_model, dt)
228 elif uuids is not None and dt is None:
229 # Steady state for specific primitives
230 energy_wrapper.runForUUIDs(self.energy_model, uuids)
231 else:
232 # Dynamic with timestep for specific primitives
233 energy_wrapper.runForUUIDsDynamic(self.energy_model, uuids, dt)
234
235 except Exception as e:
236 raise EnergyBalanceModelError(f"Energy balance calculation failed: {e}")
237
238 @validate_energy_band_params
239 def addRadiationBand(self, band: Union[str, List[str]]) -> None:
240 """
241 Add a radiation band or bands for absorbed flux calculations.
242
243 The energy balance model uses radiation bands from the RadiationModel
244 plugin to calculate absorbed radiation flux for each primitive.
245
246 Args:
247 band: Name of radiation band (e.g., "SW", "PAR", "NIR", "LW")
248 or list of band names
249
250 Raises:
251 ValueError: If band name is invalid
252 EnergyBalanceModelError: If operation fails
253
254 Example:
255 >>> energy_balance.addRadiationBand("SW") # Single band
256 >>> energy_balance.addRadiationBand(["SW", "LW", "PAR"]) # Multiple bands
257 """
258 if isinstance(band, str):
259 if not band:
260 raise ValueError("Band name must be a non-empty string")
262 try:
263 energy_wrapper.addRadiationBand(self.energy_model, band)
264 except Exception as e:
265 raise EnergyBalanceModelError(f"Failed to add radiation band '{band}': {e}")
266 elif isinstance(band, list):
267 if not band:
268 raise ValueError("Bands list cannot be empty")
269 for b in band:
270 if not isinstance(b, str) or not b:
271 raise ValueError("All band names must be non-empty strings")
273 try:
274 energy_wrapper.addRadiationBands(self.energy_model, band)
275 except Exception as e:
276 raise EnergyBalanceModelError(f"Failed to add radiation bands {band}: {e}")
277 else:
278 raise ValueError("Band must be a string or list of strings")
279
280 @validate_air_energy_params
281 def enableAirEnergyBalance(self, canopy_height_m: Optional[float] = None,
282 reference_height_m: Optional[float] = None) -> None:
283 """
284 Enable air energy balance model for canopy-scale thermal calculations.
285
286 The air energy balance computes average air temperature and water vapor
287 mole fraction based on the energy balance of the air layer in the canopy.
288
289 Args:
290 canopy_height_m: Optional canopy height in meters. If not provided,
291 computed automatically from primitive bounding box.
292 reference_height_m: Optional reference height in meters where ambient
293 conditions are measured. If not provided, assumes
294 reference height is at canopy top.
295
296 Raises:
297 ValueError: If parameters are invalid
298 EnergyBalanceModelError: If operation fails
299
300 Example:
301 >>> # Automatic canopy height detection
302 >>> energy_balance.enable_air_energy_balance()
303
304 >>> # Manual canopy and reference heights
305 >>> energy_balance.enable_air_energy_balance(canopy_height_m=5.0, reference_height_m=10.0)
306 """
307 if canopy_height_m is not None and canopy_height_m <= 0:
308 raise ValueError("Canopy height must be positive")
309 if reference_height_m is not None and reference_height_m <= 0:
310 raise ValueError("Reference height must be positive")
311
313 try:
314 if canopy_height_m is None and reference_height_m is None:
315 energy_wrapper.enableAirEnergyBalance(self.energy_model)
316 elif canopy_height_m is not None and reference_height_m is not None:
317 energy_wrapper.enableAirEnergyBalanceWithParameters(
318 self.energy_model, canopy_height_m, reference_height_m)
319 else:
320 raise ValueError("Both canopy_height_m and reference_height_m must be provided together, or both None")
321
322 except Exception as e:
323 raise EnergyBalanceModelError(f"Failed to enable air energy balance: {e}")
324
325 def enableCanopyAirspaceModel(self, canopy_UUIDs: List[int], canopy_height_m: float,
326 reference_height_m: float, leaf_area_index: float,
327 num_layers: int = 1,
328 ground_UUIDs: Optional[List[int]] = None) -> None:
329 """
330 Enable the canopy airspace model.
331
332 Resolves the within-canopy air temperature and humidity surrounding leaves from a
333 vertically layered resistance network, instead of holding them at a prescribed
334 value, so the canopy feeds back on the air driving its own transpiration. The
335 airspace is divided into vertical layers of equal leaf area index, each exchanging
336 sensible heat and water vapor with its leaves, its neighboring layers and -- for
337 the outermost layers -- the soil surface and the above-canopy reference air.
338
339 Unlike :meth:`enableAirEnergyBalance`, which evolves a prognostic boundary layer
340 and thereby assumes a horizontally infinite canopy, this model suits canopies of
341 limited extent subject to advection, such as an orchard block. The two determine
342 the same air state and **may not both be enabled**.
343
344 Because it solves for a steady state, it cannot be combined with the form of
345 :meth:`run` that takes a timestep.
346
347 The above-canopy boundary condition is read from global data
348 ``air_temperature_reference``, ``air_humidity_reference`` and
349 ``wind_speed_reference`` when present, otherwise from this model's defaults. Set
350 them with :meth:`Context.setGlobalData` before calling :meth:`run`.
351
352 After :meth:`run`, primitive data ``air_temperature``, ``air_humidity`` and
353 ``wind_speed`` are set on the canopy and ground primitives, and global data
354 ``canopy_air_temperature``, ``canopy_air_humidity``, their per-layer counterparts,
355 ``aerodynamic_resistance``, ``canopy_airspace_iterations``, ``friction_velocity`` and
356 ``canopy_stability_parameter`` are reported.
357
358 Turbulent transport above and at the top of the canopy is corrected for atmospheric
359 stability (Monin-Obukhov) by default: exchange is enhanced over a canopy warmer than
360 the reference air and suppressed over one cooler than it. Use
361 :meth:`disableCanopyAirspaceStabilityCorrection` to assume neutral stability.
362
363 The soil exchanges sensible heat and water vapor with the lowest layer through the
364 same boundary-layer and moisture conductances its own surface energy balance uses,
365 so the heat and water released by the soil are exactly what the airspace receives.
366 Ground primitives without ``boundarylayer_conductance`` primitive data use the
367 bare-soil conductance of Kustas and Norman (1999) rather than the flat-plate
368 relation used for other primitives; a value set by the user or by the
369 boundary-layer conductance plug-in takes precedence. Soil evaporation is governed
370 by the ``moisture_conductance`` (zero by default, i.e. a dry soil) and
371 ``surface_humidity`` primitive data of the ground primitives. Soil fluxes are
372 averaged over the area of the ground primitives, which are taken to represent the
373 soil surface beneath the whole canopy footprint.
374
375 Args:
376 canopy_UUIDs: Canopy (leaf) primitives exchanging heat and moisture with the airspace.
377 canopy_height_m: Height of the canopy in meters.
378 reference_height_m: Height at which above-canopy conditions are measured, in
379 meters. Must be greater than ``canopy_height_m``.
380 leaf_area_index: One-sided leaf area index on a ground-area basis.
381 num_layers: Number of vertical layers of equal leaf area index. 1 gives a
382 single within-canopy node.
383 ground_UUIDs: Ground primitives forming the soil node beneath the canopy. When
384 omitted there is no exchange with the soil surface.
385
386 Raises:
387 ValueError: If parameters are invalid.
388 EnergyBalanceModelError: If the operation fails.
389
390 Example:
391 >>> energy_balance.enableCanopyAirspaceModel(
392 ... canopy_UUIDs=leaf_uuids, canopy_height_m=3.0, reference_height_m=5.0,
393 ... leaf_area_index=2.5, num_layers=5, ground_UUIDs=ground_uuids)
394 >>> energy_balance.run()
395 """
396 if not canopy_UUIDs:
397 raise ValueError("canopy_UUIDs must contain at least one UUID")
398 if canopy_height_m <= 0:
399 raise ValueError(f"Canopy height must be positive, got {canopy_height_m}")
400 if reference_height_m <= canopy_height_m:
401 raise ValueError(
402 f"Reference height ({reference_height_m}) must be greater than "
403 f"canopy height ({canopy_height_m})")
404 if leaf_area_index <= 0:
405 raise ValueError(f"Leaf area index must be positive, got {leaf_area_index}")
406 if num_layers < 1:
407 raise ValueError(f"Number of layers must be at least 1, got {num_layers}")
408
410 try:
411 energy_wrapper.enableCanopyAirspaceModel(
412 self.energy_model, canopy_UUIDs, ground_UUIDs or [],
413 canopy_height_m, reference_height_m, leaf_area_index, num_layers)
414 except Exception as e:
415 raise EnergyBalanceModelError(f"Failed to enable canopy airspace model: {e}")
416
417 def disableCanopyAirspaceModel(self) -> None:
418 """
419 Disable the canopy airspace model.
420
421 Subsequent calls to :meth:`run` perform a single surface energy balance pass using
422 whatever ``air_temperature`` and ``air_humidity`` primitive data are currently set.
423
424 Raises:
425 EnergyBalanceModelError: If the operation fails.
426 """
428 try:
429 energy_wrapper.disableCanopyAirspaceModel(self.energy_model)
430 except Exception as e:
431 raise EnergyBalanceModelError(f"Failed to disable canopy airspace model: {e}")
432
434 """
435 Enable Monin-Obukhov stability corrections in the canopy airspace model (the default).
436
437 The aerodynamic resistance between the canopy top and the reference height, and the
438 eddy diffusivity at the canopy top, are corrected for atmospheric stability using the
439 Obukhov length implied by the sensible and latent heat fluxes leaving the canopy.
440 Unstable conditions (canopy warmer than the reference air, typically daytime) enhance
441 turbulent exchange, and stable conditions (night) suppress it. The model reports the
442 resulting global data ``friction_velocity`` (m/s) and ``canopy_stability_parameter``.
443
444 Raises:
445 NotImplementedError: If the native library predates helios-core v1.3.93.
446 EnergyBalanceModelError: If the operation fails.
447 """
449 try:
450 energy_wrapper.enableCanopyAirspaceStabilityCorrection(self.energy_model)
451 except NotImplementedError:
452 raise
453 except Exception as e:
454 raise EnergyBalanceModelError(f"Failed to enable canopy airspace stability correction: {e}")
455
457 """
458 Disable Monin-Obukhov stability corrections in the canopy airspace model.
459
460 Turbulent transport then assumes neutral stability, as before helios-core 1.3.93,
461 which overestimates the above-canopy resistance over a canopy warmer than the air and
462 underestimates it over one cooler than the air.
463
464 Raises:
465 NotImplementedError: If the native library predates helios-core v1.3.93.
466 EnergyBalanceModelError: If the operation fails.
467 """
469 try:
470 energy_wrapper.disableCanopyAirspaceStabilityCorrection(self.energy_model)
471 except NotImplementedError:
472 raise
473 except Exception as e:
474 raise EnergyBalanceModelError(f"Failed to disable canopy airspace stability correction: {e}")
475
476 def setCanopyAirspaceConvergence(self, tolerance_K: float = 0.01,
477 max_iterations: int = 50) -> None:
478 """
479 Set convergence criteria for the canopy airspace iteration.
480
481 Args:
482 tolerance_K: Temperature convergence tolerance in Kelvin. Iteration stops when
483 the maximum change in any layer's air temperature falls below this value.
484 max_iterations: Maximum number of coupled surface-energy-balance and airspace
485 iterations.
486
487 Raises:
488 ValueError: If parameters are invalid.
489 EnergyBalanceModelError: If the operation fails.
490 """
491 if tolerance_K <= 0:
492 raise ValueError(f"Convergence tolerance must be positive, got {tolerance_K}")
493 if max_iterations < 1:
494 raise ValueError(f"Maximum iterations must be at least 1, got {max_iterations}")
495
497 try:
498 energy_wrapper.setCanopyAirspaceConvergence(
499 self.energy_model, tolerance_K, max_iterations)
500 except Exception as e:
501 raise EnergyBalanceModelError(f"Failed to set canopy airspace convergence: {e}")
502
503 @validate_evaluate_air_energy_params
504 def evaluateAirEnergyBalance(self, dt_sec: float, time_advance_sec: float,
505 UUIDs: Optional[List[int]] = None) -> None:
506 """
507 Advance the air energy balance over time.
508
509 This method advances the air energy balance model by integrating over
510 multiple timesteps to reach the target time advancement.
511
512 Args:
513 dt_sec: Timestep in seconds for integration
514 time_advance_sec: Total time to advance in seconds (must be >= dt_sec)
515 UUIDs: Optional list of primitive UUIDs. If None, processes all primitives.
516
517 Raises:
518 ValueError: If parameters are invalid
519 EnergyBalanceModelError: If operation fails
520
521 Example:
522 >>> # Advance air energy balance by 1 hour using 60-second timesteps
523 >>> energy_balance.evaluate_air_energy_balance(dt_sec=60.0, time_advance_sec=3600.0)
524
525 >>> # Advance for specific primitives
526 >>> energy_balance.evaluate_air_energy_balance(
527 ... dt_sec=30.0, time_advance_sec=1800.0, uuids=[patch1_uuid, patch2_uuid])
528 """
529 if dt_sec <= 0:
530 raise ValueError("Time step must be positive")
531 if time_advance_sec < dt_sec:
532 raise ValueError("Total time advance must be greater than or equal to time step")
533
534 try:
535 if UUIDs is None:
536 energy_wrapper.evaluateAirEnergyBalance(self.energy_model, dt_sec, time_advance_sec)
537 else:
538 energy_wrapper.evaluateAirEnergyBalanceForUUIDs(
539 self.energy_model, UUIDs, dt_sec, time_advance_sec)
540
541 except Exception as e:
542 raise EnergyBalanceModelError(f"Failed to evaluate air energy balance: {e}")
543
544 @validate_output_data_params
545 def optionalOutputPrimitiveData(self, label: str) -> None:
546 """
547 Add optional output primitive data to the Context.
548
549 This method adds additional data fields to primitives that will be
550 calculated and stored during energy balance execution.
551
552 Args:
553 label: Name of the data field to add (e.g., "vapor_pressure_deficit",
554 "boundary_layer_conductance", "net_radiation")
555
556 Raises:
557 ValueError: If label is invalid
558 EnergyBalanceModelError: If operation fails
559
560 Example:
561 >>> energy_balance.add_optional_output_data("vapor_pressure_deficit")
562 >>> energy_balance.add_optional_output_data("net_radiation")
563 """
564 if not label or not isinstance(label, str):
565 raise ValueError("Label must be a non-empty string")
566
568 try:
569 energy_wrapper.optionalOutputPrimitiveData(self.energy_model, label)
570 except Exception as e:
571 raise EnergyBalanceModelError(f"Failed to add optional output data '{label}': {e}")
572
573 @validate_print_report_params
574 def printDefaultValueReport(self, UUIDs: Optional[List[int]] = None) -> None:
575 """
576 Print a report detailing usage of default input values.
577
578 This diagnostic method prints information about which primitives are
579 using default values for energy balance parameters, helping identify
580 where additional parameter specification might be needed.
581
582 Args:
583 UUIDs: Optional list of primitive UUIDs to report on. If None,
584 reports on all primitives.
585
586 Raises:
587 EnergyBalanceModelError: If operation fails
588
589 Example:
590 >>> # Report on all primitives
591 >>> energy_balance.print_default_value_report()
592
593 >>> # Report on specific primitives
594 >>> energy_balance.print_default_value_report(uuids=[patch1_uuid, patch2_uuid])
595 """
596 try:
597 if UUIDs is None:
598 energy_wrapper.printDefaultValueReport(self.energy_model)
599 else:
600 energy_wrapper.printDefaultValueReportForUUIDs(self.energy_model, UUIDs)
601
602 except Exception as e:
603 raise EnergyBalanceModelError(f"Failed to print default value report: {e}")
604
605 def is_available(self) -> bool:
606 """
607 Check if EnergyBalanceModel is available in current build.
608
609 Returns:
610 True if plugin is available, False otherwise
611 """
612 registry = get_plugin_registry()
613 return registry.is_plugin_available('energybalance')
614
615 def enableGPUAcceleration(self) -> None:
616 """
617 Enable GPU acceleration for energy balance calculations.
618
619 Attempts to enable GPU acceleration using CUDA. If GPU is not available at runtime,
620 this will raise an error. The energy balance model will use three-tier execution:
621 GPU (CUDA), OpenMP (parallel CPU), or serial CPU fallback.
622
623 Raises:
624 NotImplementedError: If library not compiled with CUDA support
625 EnergyBalanceModelError: If GPU acceleration cannot be enabled
626
627 Example:
628 >>> with EnergyBalanceModel(context) as energy_balance:
629 ... try:
630 ... energy_balance.enableGPUAcceleration()
631 ... print("GPU acceleration enabled")
632 ... except NotImplementedError:
633 ... print("GPU not available - using CPU mode")
634
635 Note:
636 Only available when PyHelios is compiled with CUDA support.
637 OpenMP CPU mode is recommended for most workloads without GPU.
638 """
640 try:
641 energy_wrapper.enableGPUAcceleration(self.energy_model)
642 except NotImplementedError:
643 raise
644 except Exception as e:
645 raise EnergyBalanceModelError(f"Failed to enable GPU acceleration: {e}")
646
647 def disableGPUAcceleration(self) -> None:
648 """
649 Disable GPU acceleration and force CPU mode.
650
651 Forces the use of OpenMP CPU implementation even if GPU is available.
652 Useful for testing, benchmarking, or when CPU performance is preferred.
653
654 Raises:
655 EnergyBalanceModelError: If operation fails
656
657 Example:
658 >>> energy_balance.disableGPUAcceleration()
659
660 Note:
661 Only available when PyHelios is compiled with CUDA support.
662 Has no effect if GPU support is not compiled in.
663 """
665 try:
666 energy_wrapper.disableGPUAcceleration(self.energy_model)
667 except Exception as e:
668 raise EnergyBalanceModelError(f"Failed to disable GPU acceleration: {e}")
669
670 def isGPUAccelerationEnabled(self) -> bool:
671 """
672 Check if GPU acceleration is currently enabled.
673
674 Returns:
675 True if GPU acceleration is enabled and available, False otherwise
676
677 Example:
678 >>> if energy_balance.isGPUAccelerationEnabled():
679 ... print("Using GPU acceleration")
680 ... else:
681 ... print("Using CPU mode")
682
683 Note:
684 Returns False if library not compiled with CUDA support.
685 """
686 try:
687 return energy_wrapper.isGPUAccelerationEnabled(self.energy_model)
688 except NotImplementedError:
689 return False
690 except Exception as e:
691 raise EnergyBalanceModelError(f"Failed to check GPU acceleration status: {e}")
692
693 @staticmethod
694 def isGPUAccelerationAvailable() -> bool:
695 """
696 Check if GPU acceleration functions are available in this build.
697
698 Returns:
699 True if GPU acceleration support is compiled in, False otherwise
700
701 Example:
702 >>> if EnergyBalanceModel.isGPUAccelerationAvailable():
703 ... print("GPU acceleration supported")
704 ... else:
705 ... print("GPU acceleration not compiled in - CPU mode only")
706 """
707 return energy_wrapper.isGPUAccelerationAvailable()
709
710# Convenience function
711def create_energy_balance_model(context: Context) -> EnergyBalanceModel:
712 """
713 Create EnergyBalanceModel instance with context.
714
715 Args:
716 context: Helios Context
717
718 Returns:
719 EnergyBalanceModel instance
720 """
721 return EnergyBalanceModel(context)
Exception raised for EnergyBalance-specific errors.
High-level interface for energy balance modeling and thermal calculations.
__init__(self, Context context)
Initialize EnergyBalanceModel with graceful plugin handling.
None optionalOutputPrimitiveData(self, str label)
Add optional output primitive data to the Context.
__del__(self)
Destructor to ensure C++ resources freed even without 'with' statement.
None enableCanopyAirspaceStabilityCorrection(self)
Enable Monin-Obukhov stability corrections in the canopy airspace model (the default).
bool is_available(self)
Check if EnergyBalanceModel is available in current build.
None disableMessages(self)
Disable console output messages from the energy balance model.
None enableMessages(self)
Enable console output messages from the energy balance model.
None disableCanopyAirspaceStabilityCorrection(self)
Disable Monin-Obukhov stability corrections in the canopy airspace model.
bool isGPUAccelerationEnabled(self)
Check if GPU acceleration is currently enabled.
bool isGPUAccelerationAvailable()
Check if GPU acceleration functions are available in this build.
None run(self, Optional[List[int]] uuids=None, Optional[float] dt=None)
Run the energy balance model.
None printDefaultValueReport(self, Optional[List[int]] UUIDs=None)
Print a report detailing usage of default input values.
None setCanopyAirspaceConvergence(self, float tolerance_K=0.01, int max_iterations=50)
Set convergence criteria for the canopy airspace iteration.
__enter__(self)
Context manager entry.
None addRadiationBand(self, Union[str, List[str]] band)
Add a radiation band or bands for absorbed flux calculations.
None enableCanopyAirspaceModel(self, List[int] canopy_UUIDs, float canopy_height_m, float reference_height_m, float leaf_area_index, int num_layers=1, Optional[List[int]] ground_UUIDs=None)
Enable the canopy airspace model.
__exit__(self, exc_type, exc_value, traceback)
Context manager exit with proper cleanup.
_check_context_alive(self)
Raise if the owning Context has been destroyed (see Context.check_context_alive).
getNativePtr(self)
Get the native pointer for advanced operations.
None enableGPUAcceleration(self)
Enable GPU acceleration for energy balance calculations.
None disableGPUAcceleration(self)
Disable GPU acceleration and force CPU mode.
None disableCanopyAirspaceModel(self)
Disable the canopy airspace model.
None evaluateAirEnergyBalance(self, float dt_sec, float time_advance_sec, Optional[List[int]] UUIDs=None)
Advance the air energy balance over time.
None enableAirEnergyBalance(self, Optional[float] canopy_height_m=None, Optional[float] reference_height_m=None)
Enable air energy balance model for canopy-scale thermal calculations.
Exception classes for PyHelios library.
Definition exceptions.py:10
EnergyBalanceModel create_energy_balance_model(Context context)
Create EnergyBalanceModel instance with context.