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
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 """Context manager entry."""
128 def __exit__(self, exc_type, exc_value, traceback):
129 """Context manager exit with proper cleanup."""
132 energy_wrapper.destroyEnergyBalanceModel(self.
energy_model)
133 logger.debug(
"EnergyBalanceModel destroyed successfully")
134 except Exception
as e:
135 logger.warning(f
"Error destroying EnergyBalanceModel: {e}")
140 """Destructor to ensure C++ resources freed even without 'with' statement."""
141 if hasattr(self,
'energy_model')
and self.
energy_model is not None:
143 energy_wrapper.destroyEnergyBalanceModel(self.
energy_model)
145 except Exception
as e:
147 warnings.warn(f
"Error in EnergyBalanceModel.__del__: {e}")
150 """Get the native pointer for advanced operations."""
155 Enable console output messages from the energy balance model.
158 EnergyBalanceModelError: If operation fails
162 except Exception
as e:
167 Disable console output messages from the energy balance model.
170 EnergyBalanceModelError: If operation fails
174 except Exception
as e:
177 @validate_energy_run_params
178 def run(self, uuids: Optional[List[int]] =
None, dt: Optional[float] =
None) ->
None:
180 Run the energy balance model.
182 This method supports multiple execution modes:
183 - Steady state for all primitives: run()
184 - Dynamic with timestep for all primitives: run(dt=60.0)
185 - Steady state for specific primitives: run(uuids=[1, 2, 3])
186 - Dynamic with timestep for specific primitives: run(uuids=[1, 2, 3], dt=60.0)
189 uuids: Optional list of primitive UUIDs to process. If None, processes all primitives.
190 dt: Optional timestep in seconds for dynamic simulation. If None, runs steady-state.
193 ValueError: If parameters are invalid
194 EnergyBalanceModelError: If energy balance calculation fails
197 >>> # Steady state for all primitives
198 >>> energy_balance.run()
200 >>> # Dynamic simulation with 60-second timestep
201 >>> energy_balance.run(dt=60.0)
203 >>> # Steady state for specific patches
204 >>> energy_balance.run(uuids=[patch1_uuid, patch2_uuid])
206 >>> # Dynamic simulation for specific patches
207 >>> energy_balance.run(uuids=[patch1_uuid, patch2_uuid], dt=30.0)
210 if uuids
is None and dt
is None:
213 elif uuids
is None and dt
is not None:
216 elif uuids
is not None and dt
is None:
221 energy_wrapper.runForUUIDsDynamic(self.
energy_model, uuids, dt)
223 except Exception
as e:
226 @validate_energy_band_params
229 Add a radiation band or bands for absorbed flux calculations.
231 The energy balance model uses radiation bands from the RadiationModel
232 plugin to calculate absorbed radiation flux for each primitive.
235 band: Name of radiation band (e.g., "SW", "PAR", "NIR", "LW")
236 or list of band names
239 ValueError: If band name is invalid
240 EnergyBalanceModelError: If operation fails
243 >>> energy_balance.addRadiationBand("SW") # Single band
244 >>> energy_balance.addRadiationBand(["SW", "LW", "PAR"]) # Multiple bands
246 if isinstance(band, str):
248 raise ValueError(
"Band name must be a non-empty string")
250 energy_wrapper.addRadiationBand(self.
energy_model, band)
251 except Exception
as e:
253 elif isinstance(band, list):
255 raise ValueError(
"Bands list cannot be empty")
257 if not isinstance(b, str)
or not b:
258 raise ValueError(
"All band names must be non-empty strings")
260 energy_wrapper.addRadiationBands(self.
energy_model, band)
261 except Exception
as e:
264 raise ValueError(
"Band must be a string or list of strings")
266 @validate_air_energy_params
268 reference_height_m: Optional[float] =
None) ->
None:
270 Enable air energy balance model for canopy-scale thermal calculations.
272 The air energy balance computes average air temperature and water vapor
273 mole fraction based on the energy balance of the air layer in the canopy.
276 canopy_height_m: Optional canopy height in meters. If not provided,
277 computed automatically from primitive bounding box.
278 reference_height_m: Optional reference height in meters where ambient
279 conditions are measured. If not provided, assumes
280 reference height is at canopy top.
283 ValueError: If parameters are invalid
284 EnergyBalanceModelError: If operation fails
287 >>> # Automatic canopy height detection
288 >>> energy_balance.enable_air_energy_balance()
290 >>> # Manual canopy and reference heights
291 >>> energy_balance.enable_air_energy_balance(canopy_height_m=5.0, reference_height_m=10.0)
293 if canopy_height_m
is not None and canopy_height_m <= 0:
294 raise ValueError(
"Canopy height must be positive")
295 if reference_height_m
is not None and reference_height_m <= 0:
296 raise ValueError(
"Reference height must be positive")
299 if canopy_height_m
is None and reference_height_m
is None:
300 energy_wrapper.enableAirEnergyBalance(self.
energy_model)
301 elif canopy_height_m
is not None and reference_height_m
is not None:
302 energy_wrapper.enableAirEnergyBalanceWithParameters(
305 raise ValueError(
"Both canopy_height_m and reference_height_m must be provided together, or both None")
307 except Exception
as e:
310 @validate_evaluate_air_energy_params
312 UUIDs: Optional[List[int]] =
None) ->
None:
314 Advance the air energy balance over time.
316 This method advances the air energy balance model by integrating over
317 multiple timesteps to reach the target time advancement.
320 dt_sec: Timestep in seconds for integration
321 time_advance_sec: Total time to advance in seconds (must be >= dt_sec)
322 UUIDs: Optional list of primitive UUIDs. If None, processes all primitives.
325 ValueError: If parameters are invalid
326 EnergyBalanceModelError: If operation fails
329 >>> # Advance air energy balance by 1 hour using 60-second timesteps
330 >>> energy_balance.evaluate_air_energy_balance(dt_sec=60.0, time_advance_sec=3600.0)
332 >>> # Advance for specific primitives
333 >>> energy_balance.evaluate_air_energy_balance(
334 ... dt_sec=30.0, time_advance_sec=1800.0, uuids=[patch1_uuid, patch2_uuid])
337 raise ValueError(
"Time step must be positive")
338 if time_advance_sec < dt_sec:
339 raise ValueError(
"Total time advance must be greater than or equal to time step")
343 energy_wrapper.evaluateAirEnergyBalance(self.
energy_model, dt_sec, time_advance_sec)
345 energy_wrapper.evaluateAirEnergyBalanceForUUIDs(
348 except Exception
as e:
351 @validate_output_data_params
354 Add optional output primitive data to the Context.
356 This method adds additional data fields to primitives that will be
357 calculated and stored during energy balance execution.
360 label: Name of the data field to add (e.g., "vapor_pressure_deficit",
361 "boundary_layer_conductance", "net_radiation")
364 ValueError: If label is invalid
365 EnergyBalanceModelError: If operation fails
368 >>> energy_balance.add_optional_output_data("vapor_pressure_deficit")
369 >>> energy_balance.add_optional_output_data("net_radiation")
371 if not label
or not isinstance(label, str):
372 raise ValueError(
"Label must be a non-empty string")
375 energy_wrapper.optionalOutputPrimitiveData(self.
energy_model, label)
376 except Exception
as e:
379 @validate_print_report_params
382 Print a report detailing usage of default input values.
384 This diagnostic method prints information about which primitives are
385 using default values for energy balance parameters, helping identify
386 where additional parameter specification might be needed.
389 UUIDs: Optional list of primitive UUIDs to report on. If None,
390 reports on all primitives.
393 EnergyBalanceModelError: If operation fails
396 >>> # Report on all primitives
397 >>> energy_balance.print_default_value_report()
399 >>> # Report on specific primitives
400 >>> energy_balance.print_default_value_report(uuids=[patch1_uuid, patch2_uuid])
404 energy_wrapper.printDefaultValueReport(self.
energy_model)
406 energy_wrapper.printDefaultValueReportForUUIDs(self.
energy_model, UUIDs)
408 except Exception
as e:
413 Check if EnergyBalanceModel is available in current build.
416 True if plugin is available, False otherwise
418 registry = get_plugin_registry()
419 return registry.is_plugin_available(
'energybalance')
425 Create EnergyBalanceModel instance with context.
428 context: Helios Context
431 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.
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.
getNativePtr(self)
Get the native pointer for advanced operations.
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.