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
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 """Get the native pointer for advanced operations."""
145 Enable console output messages from the energy balance model.
148 EnergyBalanceModelError: If operation fails
152 except Exception
as e:
157 Disable console output messages from the energy balance model.
160 EnergyBalanceModelError: If operation fails
164 except Exception
as e:
167 @validate_energy_run_params
168 def run(self, uuids: Optional[List[int]] =
None, dt: Optional[float] =
None) ->
None:
170 Run the energy balance model.
172 This method supports multiple execution modes:
173 - Steady state for all primitives: run()
174 - Dynamic with timestep for all primitives: run(dt=60.0)
175 - Steady state for specific primitives: run(uuids=[1, 2, 3])
176 - Dynamic with timestep for specific primitives: run(uuids=[1, 2, 3], dt=60.0)
179 uuids: Optional list of primitive UUIDs to process. If None, processes all primitives.
180 dt: Optional timestep in seconds for dynamic simulation. If None, runs steady-state.
183 ValueError: If parameters are invalid
184 EnergyBalanceModelError: If energy balance calculation fails
187 >>> # Steady state for all primitives
188 >>> energy_balance.run()
190 >>> # Dynamic simulation with 60-second timestep
191 >>> energy_balance.run(dt=60.0)
193 >>> # Steady state for specific patches
194 >>> energy_balance.run(uuids=[patch1_uuid, patch2_uuid])
196 >>> # Dynamic simulation for specific patches
197 >>> energy_balance.run(uuids=[patch1_uuid, patch2_uuid], dt=30.0)
200 if uuids
is None and dt
is None:
203 elif uuids
is None and dt
is not None:
206 elif uuids
is not None and dt
is None:
211 energy_wrapper.runForUUIDsDynamic(self.
energy_model, uuids, dt)
213 except Exception
as e:
216 @validate_energy_band_params
219 Add a radiation band or bands for absorbed flux calculations.
221 The energy balance model uses radiation bands from the RadiationModel
222 plugin to calculate absorbed radiation flux for each primitive.
225 band: Name of radiation band (e.g., "SW", "PAR", "NIR", "LW")
226 or list of band names
229 ValueError: If band name is invalid
230 EnergyBalanceModelError: If operation fails
233 >>> energy_balance.addRadiationBand("SW") # Single band
234 >>> energy_balance.addRadiationBand(["SW", "LW", "PAR"]) # Multiple bands
236 if isinstance(band, str):
238 raise ValueError(
"Band name must be a non-empty string")
240 energy_wrapper.addRadiationBand(self.
energy_model, band)
241 except Exception
as e:
243 elif isinstance(band, list):
245 raise ValueError(
"Bands list cannot be empty")
247 if not isinstance(b, str)
or not b:
248 raise ValueError(
"All band names must be non-empty strings")
250 energy_wrapper.addRadiationBands(self.
energy_model, band)
251 except Exception
as e:
254 raise ValueError(
"Band must be a string or list of strings")
256 @validate_air_energy_params
258 reference_height_m: Optional[float] =
None) ->
None:
260 Enable air energy balance model for canopy-scale thermal calculations.
262 The air energy balance computes average air temperature and water vapor
263 mole fraction based on the energy balance of the air layer in the canopy.
266 canopy_height_m: Optional canopy height in meters. If not provided,
267 computed automatically from primitive bounding box.
268 reference_height_m: Optional reference height in meters where ambient
269 conditions are measured. If not provided, assumes
270 reference height is at canopy top.
273 ValueError: If parameters are invalid
274 EnergyBalanceModelError: If operation fails
277 >>> # Automatic canopy height detection
278 >>> energy_balance.enable_air_energy_balance()
280 >>> # Manual canopy and reference heights
281 >>> energy_balance.enable_air_energy_balance(canopy_height_m=5.0, reference_height_m=10.0)
283 if canopy_height_m
is not None and canopy_height_m <= 0:
284 raise ValueError(
"Canopy height must be positive")
285 if reference_height_m
is not None and reference_height_m <= 0:
286 raise ValueError(
"Reference height must be positive")
289 if canopy_height_m
is None and reference_height_m
is None:
290 energy_wrapper.enableAirEnergyBalance(self.
energy_model)
291 elif canopy_height_m
is not None and reference_height_m
is not None:
292 energy_wrapper.enableAirEnergyBalanceWithParameters(
295 raise ValueError(
"Both canopy_height_m and reference_height_m must be provided together, or both None")
297 except Exception
as e:
300 @validate_evaluate_air_energy_params
302 UUIDs: Optional[List[int]] =
None) ->
None:
304 Advance the air energy balance over time.
306 This method advances the air energy balance model by integrating over
307 multiple timesteps to reach the target time advancement.
310 dt_sec: Timestep in seconds for integration
311 time_advance_sec: Total time to advance in seconds (must be >= dt_sec)
312 uuids: Optional list of primitive UUIDs. If None, processes all primitives.
315 ValueError: If parameters are invalid
316 EnergyBalanceModelError: If operation fails
319 >>> # Advance air energy balance by 1 hour using 60-second timesteps
320 >>> energy_balance.evaluate_air_energy_balance(dt_sec=60.0, time_advance_sec=3600.0)
322 >>> # Advance for specific primitives
323 >>> energy_balance.evaluate_air_energy_balance(
324 ... dt_sec=30.0, time_advance_sec=1800.0, uuids=[patch1_uuid, patch2_uuid])
327 raise ValueError(
"Time step must be positive")
328 if time_advance_sec < dt_sec:
329 raise ValueError(
"Total time advance must be greater than or equal to time step")
333 energy_wrapper.evaluateAirEnergyBalance(self.
energy_model, dt_sec, time_advance_sec)
335 energy_wrapper.evaluateAirEnergyBalanceForUUIDs(
338 except Exception
as e:
341 @validate_output_data_params
344 Add optional output primitive data to the Context.
346 This method adds additional data fields to primitives that will be
347 calculated and stored during energy balance execution.
350 label: Name of the data field to add (e.g., "vapor_pressure_deficit",
351 "boundary_layer_conductance", "net_radiation")
354 ValueError: If label is invalid
355 EnergyBalanceModelError: If operation fails
358 >>> energy_balance.add_optional_output_data("vapor_pressure_deficit")
359 >>> energy_balance.add_optional_output_data("net_radiation")
361 if not label
or not isinstance(label, str):
362 raise ValueError(
"Label must be a non-empty string")
365 energy_wrapper.optionalOutputPrimitiveData(self.
energy_model, label)
366 except Exception
as e:
369 @validate_print_report_params
372 Print a report detailing usage of default input values.
374 This diagnostic method prints information about which primitives are
375 using default values for energy balance parameters, helping identify
376 where additional parameter specification might be needed.
379 uuids: Optional list of primitive UUIDs to report on. If None,
380 reports on all primitives.
383 EnergyBalanceModelError: If operation fails
386 >>> # Report on all primitives
387 >>> energy_balance.print_default_value_report()
389 >>> # Report on specific primitives
390 >>> energy_balance.print_default_value_report(uuids=[patch1_uuid, patch2_uuid])
394 energy_wrapper.printDefaultValueReport(self.
energy_model)
396 energy_wrapper.printDefaultValueReportForUUIDs(self.
energy_model, UUIDs)
398 except Exception
as e:
403 Check if EnergyBalanceModel is available in current build.
406 True if plugin is available, False otherwise
408 registry = get_plugin_registry()
409 return registry.is_plugin_available(
'energybalance')
415 Create EnergyBalanceModel instance with context.
418 context: Helios Context
421 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.
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.