2High-level EnergyBalance interface for PyHelios.
4This module provides a user-friendly interface to the energy balance modeling
5capabilities with graceful plugin handling and informative error messages.
9from typing
import List, Optional, Union
10from contextlib
import contextmanager
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
21logger = logging.getLogger(__name__)
25 """Exception raised for EnergyBalance-specific errors."""
31 High-level interface for energy balance modeling and thermal calculations.
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.
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.
42 - NVIDIA GPU with CUDA support
43 - CUDA Toolkit installed
44 - Energy balance plugin compiled into PyHelios
47 >>> with Context() as context:
48 ... # Add some geometry
49 ... patch_uuid = context.addPatch(center=[0, 0, 1], size=[1, 1])
51 ... with EnergyBalanceModel(context) as energy_balance:
52 ... # Add radiation band for flux calculations
53 ... energy_balance.addRadiationBand("SW")
55 ... # Run steady-state energy balance
56 ... energy_balance.run()
58 ... # Or run dynamic simulation with timestep
59 ... energy_balance.run(dt=60.0) # 60 second timestep
62 def __init__(self, context: Context):
64 Initialize EnergyBalanceModel with graceful plugin handling.
67 context: Helios Context instance
70 TypeError: If context is not a Context instance
71 EnergyBalanceModelError: If energy balance plugin is not available
74 if not isinstance(context, Context):
75 raise TypeError(f
"EnergyBalanceModel requires a Context instance, got {type(context).__name__}")
81 registry = get_plugin_registry()
83 if not registry.is_plugin_available(
'energybalance'):
85 plugin_info = registry.get_plugin_capabilities()
86 available_plugins = registry.get_available_plugins()
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}"
104 alternatives = registry.suggest_alternatives(
'energybalance')
106 error_msg += f
"\n\nAlternative plugins available: {alternatives}"
107 error_msg +=
"\nConsider using radiation or photosynthesis for related thermal modeling."
113 self.
energy_model = energy_wrapper.createEnergyBalanceModel(context.getNativePtr())
116 "Failed to create EnergyBalanceModel instance. "
117 "This may indicate a problem with the native library or CUDA initialization."
119 logger.info(
"EnergyBalanceModel created successfully")
121 except Exception
as e:
125 """Raise if the owning Context has been destroyed (see Context.check_context_alive)."""
126 check_context_alive(self.
context,
"EnergyBalanceModel")
129 """Context manager entry."""
132 def __exit__(self, exc_type, exc_value, traceback):
133 """Context manager exit with proper cleanup."""
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}")
144 """Destructor to ensure C++ resources freed even without 'with' statement."""
145 if hasattr(self,
'energy_model')
and self.
energy_model is not None:
147 energy_wrapper.destroyEnergyBalanceModel(self.
energy_model)
149 except Exception
as e:
151 warnings.warn(f
"Error in EnergyBalanceModel.__del__: {e}")
154 """Get the native pointer for advanced operations."""
159 Enable console output messages from the energy balance model.
162 EnergyBalanceModelError: If operation fails
167 except Exception
as e:
172 Disable console output messages from the energy balance model.
175 EnergyBalanceModelError: If operation fails
180 except Exception
as e:
183 @validate_energy_run_params
184 def run(self, uuids: Optional[List[int]] =
None, dt: Optional[float] =
None) ->
None:
186 Run the energy balance model.
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)
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.
199 ValueError: If parameters are invalid
200 EnergyBalanceModelError: If energy balance calculation fails
203 >>> # Steady state for all primitives
204 >>> energy_balance.run()
206 >>> # Dynamic simulation with 60-second timestep
207 >>> energy_balance.run(dt=60.0)
209 >>> # Steady state for specific patches
210 >>> energy_balance.run(uuids=[patch1_uuid, patch2_uuid])
212 >>> # Dynamic simulation for specific patches
213 >>> energy_balance.run(uuids=[patch1_uuid, patch2_uuid], dt=30.0)
217 if uuids
is None and dt
is None:
220 elif uuids
is None and dt
is not None:
223 elif uuids
is not None and dt
is None:
228 energy_wrapper.runForUUIDsDynamic(self.
energy_model, uuids, dt)
230 except Exception
as e:
233 @validate_energy_band_params
236 Add a radiation band or bands for absorbed flux calculations.
238 The energy balance model uses radiation bands from the RadiationModel
239 plugin to calculate absorbed radiation flux for each primitive.
242 band: Name of radiation band (e.g., "SW", "PAR", "NIR", "LW")
243 or list of band names
246 ValueError: If band name is invalid
247 EnergyBalanceModelError: If operation fails
250 >>> energy_balance.addRadiationBand("SW") # Single band
251 >>> energy_balance.addRadiationBand(["SW", "LW", "PAR"]) # Multiple bands
253 if isinstance(band, str):
255 raise ValueError(
"Band name must be a non-empty string")
258 energy_wrapper.addRadiationBand(self.
energy_model, band)
259 except Exception
as e:
261 elif isinstance(band, list):
263 raise ValueError(
"Bands list cannot be empty")
265 if not isinstance(b, str)
or not b:
266 raise ValueError(
"All band names must be non-empty strings")
269 energy_wrapper.addRadiationBands(self.
energy_model, band)
270 except Exception
as e:
273 raise ValueError(
"Band must be a string or list of strings")
275 @validate_air_energy_params
277 reference_height_m: Optional[float] =
None) ->
None:
279 Enable air energy balance model for canopy-scale thermal calculations.
281 The air energy balance computes average air temperature and water vapor
282 mole fraction based on the energy balance of the air layer in the canopy.
285 canopy_height_m: Optional canopy height in meters. If not provided,
286 computed automatically from primitive bounding box.
287 reference_height_m: Optional reference height in meters where ambient
288 conditions are measured. If not provided, assumes
289 reference height is at canopy top.
292 ValueError: If parameters are invalid
293 EnergyBalanceModelError: If operation fails
296 >>> # Automatic canopy height detection
297 >>> energy_balance.enable_air_energy_balance()
299 >>> # Manual canopy and reference heights
300 >>> energy_balance.enable_air_energy_balance(canopy_height_m=5.0, reference_height_m=10.0)
302 if canopy_height_m
is not None and canopy_height_m <= 0:
303 raise ValueError(
"Canopy height must be positive")
304 if reference_height_m
is not None and reference_height_m <= 0:
305 raise ValueError(
"Reference height must be positive")
309 if canopy_height_m
is None and reference_height_m
is None:
310 energy_wrapper.enableAirEnergyBalance(self.
energy_model)
311 elif canopy_height_m
is not None and reference_height_m
is not None:
312 energy_wrapper.enableAirEnergyBalanceWithParameters(
315 raise ValueError(
"Both canopy_height_m and reference_height_m must be provided together, or both None")
317 except Exception
as e:
320 @validate_evaluate_air_energy_params
322 UUIDs: Optional[List[int]] =
None) ->
None:
324 Advance the air energy balance over time.
326 This method advances the air energy balance model by integrating over
327 multiple timesteps to reach the target time advancement.
330 dt_sec: Timestep in seconds for integration
331 time_advance_sec: Total time to advance in seconds (must be >= dt_sec)
332 UUIDs: Optional list of primitive UUIDs. If None, processes all primitives.
335 ValueError: If parameters are invalid
336 EnergyBalanceModelError: If operation fails
339 >>> # Advance air energy balance by 1 hour using 60-second timesteps
340 >>> energy_balance.evaluate_air_energy_balance(dt_sec=60.0, time_advance_sec=3600.0)
342 >>> # Advance for specific primitives
343 >>> energy_balance.evaluate_air_energy_balance(
344 ... dt_sec=30.0, time_advance_sec=1800.0, uuids=[patch1_uuid, patch2_uuid])
347 raise ValueError(
"Time step must be positive")
348 if time_advance_sec < dt_sec:
349 raise ValueError(
"Total time advance must be greater than or equal to time step")
353 energy_wrapper.evaluateAirEnergyBalance(self.
energy_model, dt_sec, time_advance_sec)
355 energy_wrapper.evaluateAirEnergyBalanceForUUIDs(
358 except Exception
as e:
361 @validate_output_data_params
364 Add optional output primitive data to the Context.
366 This method adds additional data fields to primitives that will be
367 calculated and stored during energy balance execution.
370 label: Name of the data field to add (e.g., "vapor_pressure_deficit",
371 "boundary_layer_conductance", "net_radiation")
374 ValueError: If label is invalid
375 EnergyBalanceModelError: If operation fails
378 >>> energy_balance.add_optional_output_data("vapor_pressure_deficit")
379 >>> energy_balance.add_optional_output_data("net_radiation")
381 if not label
or not isinstance(label, str):
382 raise ValueError(
"Label must be a non-empty string")
386 energy_wrapper.optionalOutputPrimitiveData(self.
energy_model, label)
387 except Exception
as e:
390 @validate_print_report_params
393 Print a report detailing usage of default input values.
395 This diagnostic method prints information about which primitives are
396 using default values for energy balance parameters, helping identify
397 where additional parameter specification might be needed.
400 UUIDs: Optional list of primitive UUIDs to report on. If None,
401 reports on all primitives.
404 EnergyBalanceModelError: If operation fails
407 >>> # Report on all primitives
408 >>> energy_balance.print_default_value_report()
410 >>> # Report on specific primitives
411 >>> energy_balance.print_default_value_report(uuids=[patch1_uuid, patch2_uuid])
415 energy_wrapper.printDefaultValueReport(self.
energy_model)
417 energy_wrapper.printDefaultValueReportForUUIDs(self.
energy_model, UUIDs)
419 except Exception
as e:
424 Check if EnergyBalanceModel is available in current build.
427 True if plugin is available, False otherwise
429 registry = get_plugin_registry()
430 return registry.is_plugin_available(
'energybalance')
434 Enable GPU acceleration for energy balance calculations.
436 Attempts to enable GPU acceleration using CUDA. If GPU is not available at runtime,
437 this will raise an error. The energy balance model will use three-tier execution:
438 GPU (CUDA), OpenMP (parallel CPU), or serial CPU fallback.
441 NotImplementedError: If library not compiled with CUDA support
442 EnergyBalanceModelError: If GPU acceleration cannot be enabled
445 >>> with EnergyBalanceModel(context) as energy_balance:
447 ... energy_balance.enableGPUAcceleration()
448 ... print("GPU acceleration enabled")
449 ... except NotImplementedError:
450 ... print("GPU not available - using CPU mode")
453 Only available when PyHelios is compiled with CUDA support.
454 OpenMP CPU mode is recommended for most workloads without GPU.
459 except NotImplementedError:
461 except Exception
as e:
466 Disable GPU acceleration and force CPU mode.
468 Forces the use of OpenMP CPU implementation even if GPU is available.
469 Useful for testing, benchmarking, or when CPU performance is preferred.
472 EnergyBalanceModelError: If operation fails
475 >>> energy_balance.disableGPUAcceleration()
478 Only available when PyHelios is compiled with CUDA support.
479 Has no effect if GPU support is not compiled in.
483 energy_wrapper.disableGPUAcceleration(self.
energy_model)
484 except Exception
as e:
489 Check if GPU acceleration is currently enabled.
492 True if GPU acceleration is enabled and available, False otherwise
495 >>> if energy_balance.isGPUAccelerationEnabled():
496 ... print("Using GPU acceleration")
498 ... print("Using CPU mode")
501 Returns False if library not compiled with CUDA support.
505 except NotImplementedError:
507 except Exception
as e:
513 Check if GPU acceleration functions are available in this build.
516 True if GPU acceleration support is compiled in, False otherwise
519 >>> if EnergyBalanceModel.isGPUAccelerationAvailable():
520 ... print("GPU acceleration supported")
522 ... print("GPU acceleration not compiled in - CPU mode only")
524 return energy_wrapper.isGPUAccelerationAvailable()
530 Create EnergyBalanceModel instance with context.
533 context: Helios Context
536 EnergyBalanceModel instance
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.
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.
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.
__enter__(self)
Context manager entry.
None addRadiationBand(self, Union[str, List[str]] band)
Add a radiation band or bands for absorbed flux calculations.
__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 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.
EnergyBalanceModel create_energy_balance_model(Context context)
Create EnergyBalanceModel instance with context.