0.1.19
Loading...
Searching...
No Matches
plugins.py
Go to the documentation of this file.
1"""
2Validation for PyHelios plugin parameters.
3
4Provides comprehensive validation for plugin operations,
5ensuring parameters are valid before reaching C++ plugin code.
6"""
7
8from typing import Any, List, Union
9from .core import is_finite_numeric, validate_positive_value, validate_non_negative_value
10from .datatypes import validate_vec3
11from .exceptions import ValidationError, create_validation_error
12
13
14def validate_wavelength_range(wavelength_min: float, wavelength_max: float,
15 param_name_min: str = "wavelength_min",
16 param_name_max: str = "wavelength_max",
17 function_name: str = None):
18 """Validate wavelength range for radiation modeling."""
19 if not is_finite_numeric(wavelength_min) or not is_finite_numeric(wavelength_max):
20 raise create_validation_error(
21 f"Wavelength bounds must be finite numbers, got min={wavelength_min}, max={wavelength_max}",
22 param_name=f"{param_name_min}, {param_name_max}",
23 function_name=function_name,
24 expected_type="finite numbers",
25 actual_value=f"min={wavelength_min}, max={wavelength_max}",
26 suggestion="Use finite numeric values for wavelength bounds."
27 )
28
29 if wavelength_min <= 0 or wavelength_max <= 0:
30 raise create_validation_error(
31 f"Wavelength values must be positive, got min={wavelength_min}, max={wavelength_max}",
32 param_name=f"{param_name_min}, {param_name_max}",
33 function_name=function_name,
34 expected_type="positive numbers",
35 actual_value=f"min={wavelength_min}, max={wavelength_max}",
36 suggestion="Wavelengths must be greater than 0."
37 )
38
39 if wavelength_min >= wavelength_max:
40 raise create_validation_error(
41 f"Wavelength minimum ({wavelength_min}) must be less than maximum ({wavelength_max})",
42 param_name=f"{param_name_min}, {param_name_max}",
43 function_name=function_name,
44 expected_type="min < max",
45 actual_value=(wavelength_min, wavelength_max),
46 suggestion="Ensure wavelength_min < wavelength_max."
47 )
48
49 # Physical reasonableness check (UV to far-IR range in nanometers)
50 if wavelength_min < 100 or wavelength_max > 100000:
51 raise create_validation_error(
52 f"Wavelength range [{wavelength_min}, {wavelength_max}] nm seems unrealistic",
53 param_name=f"{param_name_min}, {param_name_max}",
54 function_name=function_name,
55 expected_type="wavelengths in range 100-100000 nm",
56 actual_value=f"min={wavelength_min}, max={wavelength_max}",
57 suggestion="Typical wavelength range is 100-100000 nm (UV to far-IR). "
58 "Provide wavelength values in nanometers (e.g., PAR: 400-700 nm)."
59 )
60
61
62def validate_flux_value(flux: float, param_name: str = "flux", function_name: str = None):
63 """Validate radiation flux value."""
64 if not is_finite_numeric(flux):
65 raise create_validation_error(
66 f"Parameter must be a finite number, got {flux} ({type(flux).__name__})",
67 param_name=param_name,
68 function_name=function_name,
69 expected_type="finite number",
70 actual_value=flux,
71 suggestion="Flux values must be finite numbers (not NaN or infinity)."
72 )
73
74 if flux < 0:
75 raise create_validation_error(
76 f"Parameter must be non-negative, got {flux}",
77 param_name=param_name,
78 function_name=function_name,
79 expected_type="non-negative number",
80 actual_value=flux,
81 suggestion="Flux values cannot be negative."
82 )
83
84
85def validate_ray_count(count: int, param_name: str = "ray_count", function_name: str = None):
86 """Validate ray count for radiation simulations."""
87 if not isinstance(count, int):
88 raise create_validation_error(
89 f"Parameter must be an integer, got {type(count).__name__}",
90 param_name=param_name,
91 function_name=function_name,
92 expected_type="integer",
93 actual_value=count,
94 suggestion="Ray count must be a positive integer."
95 )
96
97 if count <= 0:
98 raise create_validation_error(
99 f"Parameter must be positive, got {count}",
100 param_name=param_name,
101 function_name=function_name,
102 expected_type="positive integer",
103 actual_value=count,
104 suggestion="Ray count must be greater than 0."
105 )
106
107 if count > 10_000_000: # 10 million rays as reasonable upper limit
108 raise create_validation_error(
109 f"Parameter {count:,} is very large and may cause memory issues",
110 param_name=param_name,
111 function_name=function_name,
112 expected_type="integer <= 10,000,000",
113 actual_value=count,
114 suggestion="Consider using fewer rays (< 10,000,000) for practical simulations."
115 )
116
117
118def validate_direction_vector(direction: Any, param_name: str = "direction", function_name: str = None):
119 """Validate direction vector for radiation sources."""
120 validate_vec3(direction, param_name, function_name)
121
122 # Check for zero vector
123 if hasattr(direction, 'x') and hasattr(direction, 'y') and hasattr(direction, 'z'):
124 magnitude_squared = direction.x**2 + direction.y**2 + direction.z**2
125 if magnitude_squared == 0:
126 raise create_validation_error(
127 f"Direction vector cannot be zero vector (0,0,0)",
128 param_name=param_name,
129 function_name=function_name,
130 expected_type="non-zero vec3",
131 actual_value=direction,
132 suggestion="Provide a direction vector with non-zero magnitude."
133 )
134
135
136def validate_band_label(label: str, param_name: str = "band_label", function_name: str = None):
137 """Validate radiation band label."""
138 if not isinstance(label, str):
139 raise create_validation_error(
140 f"Parameter must be a string, got {type(label).__name__}",
141 param_name=param_name,
142 function_name=function_name,
143 expected_type="string",
144 actual_value=label,
145 suggestion="Band labels must be strings."
146 )
147
148 if not label.strip():
149 raise create_validation_error(
150 f"Parameter cannot be empty or whitespace-only",
151 param_name=param_name,
152 function_name=function_name,
153 expected_type="non-empty string",
154 actual_value=repr(label),
155 suggestion="Provide a non-empty band label."
156 )
157
158
159def validate_source_id(source_id: int, param_name: str = "source_id", function_name: str = None):
160 """Validate radiation source ID."""
161 if not isinstance(source_id, int):
162 raise create_validation_error(
163 f"Parameter must be an integer, got {type(source_id).__name__}",
164 param_name=param_name,
165 function_name=function_name,
166 expected_type="integer",
167 actual_value=source_id,
168 suggestion="Source IDs are integers returned by add*RadiationSource methods."
169 )
170
171 if source_id < 0:
172 raise create_validation_error(
173 f"Parameter must be non-negative, got {source_id}",
174 param_name=param_name,
175 function_name=function_name,
176 expected_type="non-negative integer",
177 actual_value=source_id,
178 suggestion="Source IDs are non-negative integers."
179 )
180
181
182def validate_source_id_list(source_ids: List[int], param_name: str = "source_ids", function_name: str = None):
183 """Validate list of radiation source IDs."""
184 if not isinstance(source_ids, list):
185 raise create_validation_error(
186 f"Parameter must be a list, got {type(source_ids).__name__}",
187 param_name=param_name,
188 function_name=function_name,
189 expected_type="list of integers",
190 actual_value=source_ids,
191 suggestion="Provide a list of source IDs."
192 )
193
194 if not source_ids:
195 raise create_validation_error(
196 f"Parameter cannot be empty",
197 param_name=param_name,
198 function_name=function_name,
199 expected_type="non-empty list",
200 actual_value=source_ids,
201 suggestion="Provide at least one source ID."
202 )
203
204 for i, source_id in enumerate(source_ids):
205 validate_source_id(source_id, f"{param_name}[{i}]", function_name)
206
207
208def validate_position_like(value: Any, param_name: str = "position", function_name: str = None):
209 """Validate a position-like parameter (vec3 or 3-element list/tuple).
210
211 Used by RadiationModel methods that accept flexible position types.
212 """
213 from ..wrappers.DataTypes import vec3
214 if isinstance(value, vec3):
215 return
216 if hasattr(value, 'x') and hasattr(value, 'y') and hasattr(value, 'z'):
217 return # vec3-like
218 if isinstance(value, (list, tuple)):
219 if len(value) != 3:
220 raise create_validation_error(
221 f"Parameter must have 3 elements, got {len(value)}",
222 param_name=param_name,
223 function_name=function_name,
224 expected_type="vec3 or 3-element list/tuple",
225 actual_value=value,
226 )
227 return
228 raise create_validation_error(
229 f"Parameter must be a vec3 or 3-element list/tuple, got {type(value).__name__}",
230 param_name=param_name,
231 function_name=function_name,
232 expected_type="vec3 or 3-element list/tuple",
233 actual_value=value,
234 suggestion="Use vec3(x, y, z) or [x, y, z]."
235 )
236
237
238def validate_direction_like(value: Any, param_name: str = "direction", function_name: str = None):
239 """Validate a direction-like parameter (vec3, SphericalCoord, or 3-element list/tuple).
240
241 Used by RadiationModel methods that accept flexible direction types.
242 """
243 from ..wrappers.DataTypes import vec3, SphericalCoord
244 if isinstance(value, (vec3, SphericalCoord)):
245 return
246 if hasattr(value, 'x') and hasattr(value, 'y') and hasattr(value, 'z'):
247 return # vec3-like
248 if hasattr(value, 'radius') and hasattr(value, 'elevation') and hasattr(value, 'azimuth'):
249 return # SphericalCoord-like
250 if isinstance(value, (list, tuple)):
251 if len(value) != 3:
252 raise create_validation_error(
253 f"Parameter must have 3 elements, got {len(value)}",
254 param_name=param_name,
255 function_name=function_name,
256 expected_type="vec3, SphericalCoord, or 3-element list/tuple",
257 actual_value=value,
258 )
259 return
260 raise create_validation_error(
261 f"Parameter must be a vec3, SphericalCoord, or 3-element list/tuple, got {type(value).__name__}",
262 param_name=param_name,
263 function_name=function_name,
264 expected_type="vec3, SphericalCoord, or 3-element list/tuple",
265 actual_value=value,
266 suggestion="Use vec3(x, y, z), SphericalCoord(r, e, a), or [x, y, z]."
267 )
268
269
270def validate_size_like(value: Any, param_name: str = "size", function_name: str = None):
271 """Validate a size-like parameter (vec2 or 2-element list/tuple).
272
273 Used by RadiationModel methods that accept flexible size types.
274 """
275 from ..wrappers.DataTypes import vec2
276 if isinstance(value, vec2):
277 return
278 if hasattr(value, 'x') and hasattr(value, 'y') and not hasattr(value, 'z'):
279 return # vec2-like
280 if isinstance(value, (list, tuple)):
281 if len(value) != 2:
282 raise create_validation_error(
283 f"Parameter must have 2 elements, got {len(value)}",
284 param_name=param_name,
285 function_name=function_name,
286 expected_type="vec2 or 2-element list/tuple",
287 actual_value=value,
288 )
289 return
290 raise create_validation_error(
291 f"Parameter must be a vec2 or 2-element list/tuple, got {type(value).__name__}",
292 param_name=param_name,
293 function_name=function_name,
294 expected_type="vec2 or 2-element list/tuple",
295 actual_value=value,
296 suggestion="Use vec2(x, y) or [x, y]."
297 )
298
299
300def validate_wpt_parameters(scale_factor: float = 1.0, recursion_level: int = 5,
301 segment_resolution: int = 10, param_prefix: str = "WPT"):
302 """Validate WeberPennTree generation parameters."""
303 if not is_finite_numeric(scale_factor):
304 raise create_validation_error(
305 f"Scale factor must be a finite number, got {scale_factor} ({type(scale_factor).__name__})",
306 param_name=f"{param_prefix}_scale_factor",
307 expected_type="finite number",
308 actual_value=scale_factor,
309 suggestion="Scale factor must be a finite number."
310 )
311
312 if scale_factor <= 0:
313 raise create_validation_error(
314 f"Scale factor must be positive, got {scale_factor}",
315 param_name=f"{param_prefix}_scale_factor",
316 expected_type="positive number",
317 actual_value=scale_factor,
318 suggestion="Scale factor must be greater than 0."
319 )
320
321 if scale_factor > 100:
322 raise create_validation_error(
323 f"Scale factor {scale_factor} is very large and may cause issues",
324 param_name=f"{param_prefix}_scale_factor",
325 expected_type="reasonable scale factor (0.1-10)",
326 actual_value=scale_factor,
327 suggestion="Typical scale factors are 0.1-10. Large values may cause performance issues."
328 )
329
330 if not isinstance(recursion_level, int):
331 raise create_validation_error(
332 f"Recursion level must be an integer, got {type(recursion_level).__name__}",
333 param_name=f"{param_prefix}_recursion_level",
334 expected_type="integer",
335 actual_value=recursion_level,
336 suggestion="Recursion level must be an integer."
337 )
338
339 if recursion_level < 1 or recursion_level > 10:
340 raise create_validation_error(
341 f"Recursion level must be between 1-10, got {recursion_level}",
342 param_name=f"{param_prefix}_recursion_level",
343 expected_type="integer in range 1-10",
344 actual_value=recursion_level,
345 suggestion="Use recursion level between 1-10 for realistic tree generation."
346 )
347
348 if not isinstance(segment_resolution, int):
349 raise create_validation_error(
350 f"Segment resolution must be an integer, got {type(segment_resolution).__name__}",
351 param_name=f"{param_prefix}_segment_resolution",
352 expected_type="integer",
353 actual_value=segment_resolution,
354 suggestion="Segment resolution must be an integer."
355 )
356
357 if segment_resolution < 3 or segment_resolution > 50:
358 raise create_validation_error(
359 f"Segment resolution must be between 3-50, got {segment_resolution}",
360 param_name=f"{param_prefix}_segment_resolution",
361 expected_type="integer in range 3-50",
362 actual_value=segment_resolution,
363 suggestion="Use segment resolution between 3-50 for good performance and quality."
364 )
365
366
367def validate_time_value(time_val: Any, param_name: str = "time", function_name: str = None):
368 """Validate time values for energy balance calculations."""
369 if not is_finite_numeric(time_val):
370 raise create_validation_error(
371 f"Parameter must be a finite number, got {time_val} ({type(time_val).__name__})",
372 param_name=param_name,
373 function_name=function_name,
374 expected_type="finite number",
375 actual_value=time_val,
376 suggestion="Time values must be finite numbers (not NaN or infinity)."
377 )
378
379 if time_val < 0:
380 raise create_validation_error(
381 f"Parameter cannot be negative, got {time_val}",
382 param_name=param_name,
383 function_name=function_name,
384 expected_type="non-negative number",
385 actual_value=time_val,
386 suggestion="Time values must be >= 0."
387 )
388
389
390def validate_physical_quantity(value: Any, quantity_name: str,
391 expected_units: str = None, min_val: float = None,
392 max_val: float = None, param_name: str = None,
393 function_name: str = None):
394 """Validate physical quantity values with optional range checking."""
395 param_name = param_name or quantity_name.lower().replace(' ', '_')
396
397 if not is_finite_numeric(value):
398 units_info = f" ({expected_units})" if expected_units else ""
399 raise create_validation_error(
400 f"{quantity_name}{units_info} must be a finite number, got {value} ({type(value).__name__})",
401 param_name=param_name,
402 function_name=function_name,
403 expected_type="finite number",
404 actual_value=value,
405 suggestion=f"Provide a finite numeric value for {quantity_name}."
406 )
407
408 if min_val is not None and value < min_val:
409 units_info = f" {expected_units}" if expected_units else ""
410 raise create_validation_error(
411 f"{quantity_name} must be >= {min_val}{units_info}, got {value}",
412 param_name=param_name,
413 function_name=function_name,
414 expected_type=f"number >= {min_val}",
415 actual_value=value,
416 suggestion=f"Use a value >= {min_val} for {quantity_name}."
417 )
418
419 if max_val is not None and value > max_val:
420 units_info = f" {expected_units}" if expected_units else ""
421 raise create_validation_error(
422 f"{quantity_name} must be <= {max_val}{units_info}, got {value}",
423 param_name=param_name,
424 function_name=function_name,
425 expected_type=f"number <= {max_val}",
426 actual_value=value,
427 suggestion=f"Use a value <= {max_val} for {quantity_name}."
428 )
429
430
431def validate_tree_id(tree_id: Any, param_name: str = "tree_id", function_name: str = None):
432 """Validate WeberPennTree tree ID."""
433 if not isinstance(tree_id, int):
434 raise create_validation_error(
435 f"Parameter must be an integer, got {type(tree_id).__name__}",
436 param_name=param_name,
437 function_name=function_name,
438 expected_type="integer",
439 actual_value=tree_id,
440 suggestion="Tree IDs are integers returned by buildTree()."
441 )
442
443 if tree_id < 0:
444 raise create_validation_error(
445 f"Parameter must be non-negative, got {tree_id}",
446 param_name=param_name,
447 function_name=function_name,
448 expected_type="non-negative integer",
449 actual_value=tree_id,
450 suggestion="Tree IDs are non-negative integers."
451 )
452
453
454def validate_segment_resolution(resolution: Any, min_val: int = 3, max_val: int = 100,
455 param_name: str = "resolution", function_name: str = None):
456 """Validate segment resolution parameters for tree generation."""
457 if not isinstance(resolution, int):
458 raise create_validation_error(
459 f"Parameter must be an integer, got {type(resolution).__name__}",
460 param_name=param_name,
461 function_name=function_name,
462 expected_type="integer",
463 actual_value=resolution,
464 suggestion="Segment resolution must be an integer."
465 )
466
467 if resolution < min_val or resolution > max_val:
468 raise create_validation_error(
469 f"Parameter must be between {min_val}-{max_val}, got {resolution}",
470 param_name=param_name,
471 function_name=function_name,
472 expected_type=f"integer in range {min_val}-{max_val}",
473 actual_value=resolution,
474 suggestion=f"Use resolution between {min_val}-{max_val} for good performance and quality."
475 )
476
477
478def validate_angle_degrees(angle: Any, param_name: str = "angle", function_name: str = None):
479 """Validate angle values in degrees."""
480 if not is_finite_numeric(angle):
481 raise create_validation_error(
482 f"Parameter must be a finite number, got {angle} ({type(angle).__name__})",
483 param_name=param_name,
484 function_name=function_name,
485 expected_type="finite number",
486 actual_value=angle,
487 suggestion="Angle values must be finite numbers (not NaN or infinity)."
488 )
489
490 # Note: We don't restrict angle range since angles can be outside [0, 360] for various reasons
491 # The underlying C++ code will handle angle normalization
492
493
494def validate_scaling_factor(scale: Any, min_val: float = 0.001, max_val: float = 1000.0,
495 param_name: str = "scaling_factor", function_name: str = None):
496 """Validate scaling factor parameters."""
497 if not is_finite_numeric(scale):
498 raise create_validation_error(
499 f"Parameter must be a finite number, got {scale} ({type(scale).__name__})",
500 param_name=param_name,
501 function_name=function_name,
502 expected_type="finite number",
503 actual_value=scale,
504 suggestion="Scaling factors must be finite numbers."
505 )
506
507 if scale <= 0:
508 raise create_validation_error(
509 f"Parameter must be positive, got {scale}",
510 param_name=param_name,
511 function_name=function_name,
512 expected_type="positive number",
513 actual_value=scale,
514 suggestion="Scaling factors must be greater than 0."
515 )
516
517 if scale < min_val or scale > max_val:
518 raise create_validation_error(
519 f"Parameter {scale} is outside reasonable range [{min_val}, {max_val}]",
520 param_name=param_name,
521 function_name=function_name,
522 expected_type=f"number in range [{min_val}, {max_val}]",
523 actual_value=scale,
524 suggestion=f"Use scaling factors between {min_val} and {max_val} to avoid numerical issues."
525 )
526
527
528def validate_filename(filename: Any, param_name: str = "filename", function_name: str = None,
529 allowed_extensions: List[str] = None):
530 """Validate filename parameters for file operations."""
531 if not isinstance(filename, str):
532 raise create_validation_error(
533 f"Parameter must be a string, got {type(filename).__name__}",
534 param_name=param_name,
535 function_name=function_name,
536 expected_type="string",
537 actual_value=filename,
538 suggestion="Filename must be a string."
539 )
540
541 if not filename.strip():
542 raise create_validation_error(
543 f"Parameter cannot be empty or whitespace-only",
544 param_name=param_name,
545 function_name=function_name,
546 expected_type="non-empty string",
547 actual_value=repr(filename),
548 suggestion="Provide a non-empty filename."
549 )
550
551 # Check for potentially problematic characters in the basename only
552 # (full paths may contain valid characters like ':' in Windows drive letters C:\‍)
553 import os
554 import ntpath
555 # Handle both Windows and Unix paths correctly regardless of current platform
556 # Use ntpath for Windows-style paths (backslash or drive letter pattern)
557 if '\\' in filename or (len(filename) >= 2 and filename[1] == ':'):
558 basename = ntpath.basename(filename)
559 else:
560 basename = os.path.basename(filename)
561 # On Windows, colons are only valid after drive letters, not in filenames
562 # On all platforms, these characters are problematic in filenames
563 invalid_chars = ['<', '>', '"', '|', '?', '*']
564 # Add colon check for the basename (not allowed in filenames on any platform)
565 if ':' in basename:
566 invalid_chars.append(':')
567 if any(char in basename for char in invalid_chars):
568 raise create_validation_error(
569 f"Filename contains invalid characters: {basename}",
570 param_name=param_name,
571 function_name=function_name,
572 expected_type="valid filename",
573 actual_value=filename,
574 suggestion="Avoid characters: < > : \" | ? * in filename"
575 )
576
577 # Validate file extension if specified
578 if allowed_extensions:
579 ext = os.path.splitext(filename)[1].lower()
580 if ext not in allowed_extensions:
581 raise create_validation_error(
582 f"File extension '{ext}' not allowed. Must be one of: {', '.join(allowed_extensions)}",
583 param_name=param_name,
584 function_name=function_name,
585 expected_type=f"filename with extension {allowed_extensions}",
586 actual_value=filename,
587 suggestion=f"Use a filename ending with one of: {', '.join(allowed_extensions)}"
588 )
589
590
591def validate_uuid_list(uuids: Any, param_name: str = "uuids", function_name: str = None,
592 allow_empty: bool = False):
593 """Validate UUID list parameters."""
594 if not isinstance(uuids, list):
595 raise create_validation_error(
596 f"Parameter must be a list, got {type(uuids).__name__}",
597 param_name=param_name,
598 function_name=function_name,
599 expected_type="list of integers",
600 actual_value=uuids,
601 suggestion="Provide a list of UUID integers."
602 )
603
604 if not allow_empty and not uuids:
605 raise create_validation_error(
606 f"Parameter cannot be empty",
607 param_name=param_name,
608 function_name=function_name,
609 expected_type="non-empty list",
610 actual_value=uuids,
611 suggestion="Provide at least one UUID."
612 )
613
614 for i, uuid in enumerate(uuids):
615 if not isinstance(uuid, int):
616 raise create_validation_error(
617 f"UUID at index {i} must be an integer, got {type(uuid).__name__}",
618 param_name=f"{param_name}[{i}]",
619 function_name=function_name,
620 expected_type="integer",
621 actual_value=uuid,
622 suggestion="UUIDs are integers returned by geometry creation methods."
623 )
624
625 if uuid < 0:
626 raise create_validation_error(
627 f"UUID at index {i} must be non-negative, got {uuid}",
628 param_name=f"{param_name}[{i}]",
629 function_name=function_name,
630 expected_type="non-negative integer",
631 actual_value=uuid,
632 suggestion="UUIDs are non-negative integers."
633 )
634
635
636def validate_positive_integer_range(value: Any, min_val: int = 1, max_val: int = 50,
637 param_name: str = "value", function_name: str = None):
638 """Validate positive integer within a specified range."""
639 if not isinstance(value, int):
640 raise create_validation_error(
641 f"Parameter must be an integer, got {type(value).__name__}",
642 param_name=param_name,
643 function_name=function_name,
644 expected_type="integer",
645 actual_value=value,
646 suggestion="Provide an integer value."
647 )
648
649 if value < min_val or value > max_val:
650 raise create_validation_error(
651 f"Parameter must be between {min_val}-{max_val}, got {value}",
652 param_name=param_name,
653 function_name=function_name,
654 expected_type=f"integer in range {min_val}-{max_val}",
655 actual_value=value,
656 suggestion=f"Use a value between {min_val} and {max_val}."
657 )
658
659
660def validate_recursion_level(level: Any, param_name: str = "recursion_level", function_name: str = None):
661 """Validate recursion level for tree generation."""
662 validate_positive_integer_range(level, min_val=1, max_val=10, param_name=param_name, function_name=function_name)
663
664
665def validate_subdivision_count(count: Any, param_name: str = "subdivision_count", function_name: str = None):
666 """Validate subdivision count parameters."""
667 validate_positive_integer_range(count, min_val=1, max_val=20, param_name=param_name, function_name=function_name)
668
669
670# ============================================================================
671# Photosynthesis Plugin Validation Functions
672# ============================================================================
673
674def validate_species_name(species: Any, param_name: str = "species", function_name: str = None):
675 """Validate species name for photosynthesis library."""
676 from ..types.photosynthesis import validate_species_name as _validate_species
677
678 if not isinstance(species, str):
679 raise create_validation_error(
680 f"Parameter must be a string, got {type(species).__name__}",
681 param_name=param_name,
682 function_name=function_name,
683 expected_type="string",
684 actual_value=species,
685 suggestion="Species names must be strings."
686 )
687
688 try:
689 return _validate_species(species)
690 except ValueError as e:
691 raise create_validation_error(
692 str(e),
693 param_name=param_name,
694 function_name=function_name,
695 expected_type="valid species name",
696 actual_value=species,
697 suggestion="Use one of the available species or aliases. See get_available_species()."
698 )
699
700
701def validate_temperature(temperature: Any, param_name: str = "temperature", function_name: str = None):
702 """Validate temperature values for photosynthesis calculations."""
704 value=temperature,
705 quantity_name="Temperature",
706 expected_units="K",
707 min_val=200.0, # Absolute minimum for biological processes
708 max_val=400.0, # Absolute maximum for biological processes
709 param_name=param_name,
710 function_name=function_name
711 )
712
713
714def validate_co2_concentration(co2: Any, param_name: str = "co2_concentration", function_name: str = None):
715 """Validate CO2 concentration values for photosynthesis calculations."""
717 value=co2,
718 quantity_name="CO2 concentration",
719 expected_units="ppm",
720 min_val=50.0, # Well below atmospheric levels
721 max_val=2000.0, # Well above atmospheric levels for controlled environments
722 param_name=param_name,
723 function_name=function_name
724 )
725
726
727def validate_photosynthetic_rate(rate: Any, param_name: str = "photosynthetic_rate", function_name: str = None):
728 """Validate photosynthetic rate values."""
730 value=rate,
731 quantity_name="Photosynthetic rate",
732 expected_units="μmol/m²/s",
733 min_val=0.0, # Cannot be negative
734 max_val=100.0, # Reasonable upper limit for most plants
735 param_name=param_name,
736 function_name=function_name
737 )
738
739
740def validate_conductance(conductance: Any, param_name: str = "conductance", function_name: str = None):
741 """Validate conductance values for photosynthesis calculations."""
743 value=conductance,
744 quantity_name="Conductance",
745 expected_units="mol/m²/s",
746 min_val=0.0, # Cannot be negative
747 max_val=10.0, # Reasonable upper limit
748 param_name=param_name,
749 function_name=function_name
750 )
751
752
753def validate_par_flux(par_flux: Any, param_name: str = "PAR_flux", function_name: str = None):
754 """Validate PAR flux values for photosynthesis calculations."""
756 value=par_flux,
757 quantity_name="PAR flux",
758 expected_units="μmol/m²/s",
759 min_val=0.0, # Cannot be negative
760 max_val=3000.0, # Very high light conditions
761 param_name=param_name,
762 function_name=function_name
763 )
764
765
766def validate_empirical_coefficients(coefficients: Any, param_name: str = "coefficients", function_name: str = None):
767 """Validate empirical model coefficients array or dataclass."""
768 # Accept dataclass instances with to_array() method
769 if hasattr(coefficients, 'to_array') and callable(getattr(coefficients, 'to_array')):
770 return # Dataclass instances are valid
771
772 if not isinstance(coefficients, (list, tuple)):
773 raise create_validation_error(
774 f"Parameter must be a list or tuple, got {type(coefficients).__name__}",
775 param_name=param_name,
776 function_name=function_name,
777 expected_type="list or tuple of numbers",
778 actual_value=coefficients,
779 suggestion="Provide coefficients as a list or tuple of numbers."
780 )
781
782 if len(coefficients) < 10:
783 raise create_validation_error(
784 f"Empirical model coefficients need at least 10 elements, got {len(coefficients)}",
785 param_name=param_name,
786 function_name=function_name,
787 expected_type="list with >= 10 elements",
788 actual_value=f"length {len(coefficients)}",
789 suggestion="Provide all 10 empirical model coefficients: [Tref, Ci_ref, Asat, theta, Tmin, Topt, q, R, ER, kC]."
790 )
791
792 for i, coeff in enumerate(coefficients[:10]):
793 if not is_finite_numeric(coeff):
794 raise create_validation_error(
795 f"Coefficient at index {i} must be a finite number, got {coeff} ({type(coeff).__name__})",
796 param_name=f"{param_name}[{i}]",
797 function_name=function_name,
798 expected_type="finite number",
799 actual_value=coeff,
800 suggestion="All coefficients must be finite numbers (not NaN or infinity)."
801 )
802
803
804def validate_farquhar_coefficients(coefficients: Any, param_name: str = "coefficients", function_name: str = None):
805 """Validate Farquhar model coefficients array or dataclass."""
806 # Accept dataclass instances with to_array() method
807 if hasattr(coefficients, 'to_array') and callable(getattr(coefficients, 'to_array')):
808 return # Dataclass instances are valid
809
810 if not isinstance(coefficients, (list, tuple)):
811 raise create_validation_error(
812 f"Parameter must be a list or tuple, got {type(coefficients).__name__}",
813 param_name=param_name,
814 function_name=function_name,
815 expected_type="list or tuple of numbers",
816 actual_value=coefficients,
817 suggestion="Provide coefficients as a list or tuple of numbers."
818 )
819
820 if len(coefficients) < 18:
821 raise create_validation_error(
822 f"Farquhar model coefficients need at least 18 elements, got {len(coefficients)}",
823 param_name=param_name,
824 function_name=function_name,
825 expected_type="list with >= 18 elements",
826 actual_value=f"length {len(coefficients)}",
827 suggestion="Provide all 18 Farquhar model coefficients including temperature response parameters."
828 )
829
830 for i, coeff in enumerate(coefficients[:18]):
831 if not is_finite_numeric(coeff):
832 raise create_validation_error(
833 f"Coefficient at index {i} must be a finite number, got {coeff} ({type(coeff).__name__})",
834 param_name=f"{param_name}[{i}]",
835 function_name=function_name,
836 expected_type="finite number",
837 actual_value=coeff,
838 suggestion="All coefficients must be finite numbers (not NaN or infinity)."
839 )
840
841
842def validate_vcmax(vcmax: Any, param_name: str = "Vcmax", function_name: str = None):
843 """Validate maximum carboxylation rate (Vcmax) parameter."""
844 if vcmax == -1.0:
845 # Special case: -1 indicates uninitialized parameter (valid in Farquhar model)
846 return
847
849 value=vcmax,
850 quantity_name="Maximum carboxylation rate (Vcmax)",
851 expected_units="μmol/m²/s",
852 min_val=1.0, # Reasonable minimum
853 max_val=300.0, # Reasonable maximum for most plants
854 param_name=param_name,
855 function_name=function_name
856 )
857
858
859def validate_jmax(jmax: Any, param_name: str = "Jmax", function_name: str = None):
860 """Validate maximum electron transport rate (Jmax) parameter."""
861 if jmax == -1.0:
862 # Special case: -1 indicates uninitialized parameter (valid in Farquhar model)
863 return
864
866 value=jmax,
867 quantity_name="Maximum electron transport rate (Jmax)",
868 expected_units="μmol/m²/s",
869 min_val=5.0, # Reasonable minimum
870 max_val=500.0, # Reasonable maximum for most plants
871 param_name=param_name,
872 function_name=function_name
873 )
874
875
876def validate_quantum_efficiency(alpha: Any, param_name: str = "alpha", function_name: str = None):
877 """Validate quantum efficiency (alpha) parameter."""
878 if alpha == -1.0:
879 # Special case: -1 indicates uninitialized parameter (valid in Farquhar model)
880 return
881
883 value=alpha,
884 quantity_name="Quantum efficiency (alpha)",
885 expected_units="unitless",
886 min_val=0.01, # Very low efficiency
887 max_val=1.0, # Theoretical maximum
888 param_name=param_name,
889 function_name=function_name
890 )
891
892
893def validate_dark_respiration(rd: Any, param_name: str = "Rd", function_name: str = None):
894 """Validate dark respiration rate (Rd) parameter."""
895 if rd == -1.0:
896 # Special case: -1 indicates uninitialized parameter (valid in Farquhar model)
897 return
898
900 value=rd,
901 quantity_name="Dark respiration rate (Rd)",
902 expected_units="μmol/m²/s",
903 min_val=0.1, # Reasonable minimum
904 max_val=20.0, # Reasonable maximum
905 param_name=param_name,
906 function_name=function_name
907 )
908
909
910def validate_oxygen_concentration(o2: Any, param_name: str = "oxygen", function_name: str = None):
911 """Validate oxygen concentration for photosynthesis calculations."""
913 value=o2,
914 quantity_name="Oxygen concentration",
915 expected_units="mmol/mol",
916 min_val=50.0, # Very low O2 environment
917 max_val=500.0, # Very high O2 environment
918 param_name=param_name,
919 function_name=function_name
920 )
921
922
923def validate_temperature_response_params(value_at_25c: Any, dha: Any = None, topt: Any = None, dhd: Any = None,
924 param_prefix: str = "", function_name: str = None):
925 """Validate temperature response parameters for photosynthetic processes."""
926 # Validate base value at 25°C
928 value=value_at_25c,
929 quantity_name=f"{param_prefix} value at 25°C",
930 min_val=0.1,
931 param_name=f"{param_prefix}_at_25C",
932 function_name=function_name
933 )
934
935 # Validate optional temperature response parameters
936 if dha is not None and dha >= 0: # -1 means not provided
938 value=dha,
939 quantity_name=f"{param_prefix} activation energy (dHa)",
940 expected_units="kJ/mol",
941 min_val=10.0, # Reasonable minimum activation energy
942 max_val=150.0, # Reasonable maximum activation energy
943 param_name=f"{param_prefix}_dHa",
944 function_name=function_name
945 )
946
947 if topt is not None and topt >= 0: # -1 means not provided
949 value=topt,
950 quantity_name=f"{param_prefix} optimum temperature",
951 expected_units="°C",
952 min_val=15.0, # Reasonable minimum
953 max_val=55.0, # Reasonable maximum
954 param_name=f"{param_prefix}_Topt",
955 function_name=function_name
956 )
957
958 if dhd is not None and dhd >= 0: # -1 means not provided
960 value=dhd,
961 quantity_name=f"{param_prefix} deactivation energy (dHd)",
962 expected_units="kJ/mol",
963 min_val=100.0, # Reasonable minimum deactivation energy
964 max_val=1000.0, # Reasonable maximum deactivation energy
965 param_name=f"{param_prefix}_dHd",
966 function_name=function_name
967 )
968
969
970def validate_camera_label(label: str, param_name: str = "camera_label", function_name: str = None) -> str:
971 """Validate camera label for radiation camera operations."""
972 if not isinstance(label, str):
973 raise create_validation_error(
974 f"Camera label must be a string, got {type(label).__name__}",
975 param_name=param_name,
976 function_name=function_name,
977 expected_type="str",
978 actual_value=label,
979 suggestion="Use a string label for the camera."
980 )
981
982 if not label or label.strip() == "":
983 raise create_validation_error(
984 "Camera label cannot be empty",
985 param_name=param_name,
986 function_name=function_name,
987 expected_type="non-empty string",
988 actual_value=label,
989 suggestion="Provide a non-empty string label for the camera."
990 )
991
992 return label.strip()
993
994
995def validate_band_labels_list(band_labels: List[str], param_name: str = "band_labels", function_name: str = None) -> List[str]:
996 """Validate list of band labels for camera operations."""
997 if not isinstance(band_labels, (list, tuple)):
998 raise create_validation_error(
999 f"Band labels must be a list or tuple, got {type(band_labels).__name__}",
1000 param_name=param_name,
1001 function_name=function_name,
1002 expected_type="List[str]",
1003 actual_value=band_labels,
1004 suggestion="Provide a list of string band labels."
1005 )
1006
1007 if len(band_labels) == 0:
1008 raise create_validation_error(
1009 "Band labels list cannot be empty",
1010 param_name=param_name,
1011 function_name=function_name,
1012 expected_type="non-empty list",
1013 actual_value=band_labels,
1014 suggestion="Provide at least one band label."
1015 )
1016
1017 validated_labels = []
1018 for i, label in enumerate(band_labels):
1019 if not isinstance(label, str):
1020 raise create_validation_error(
1021 f"Band label at index {i} must be a string, got {type(label).__name__}",
1022 param_name=f"{param_name}[{i}]",
1023 function_name=function_name,
1024 expected_type="str",
1025 actual_value=label,
1026 suggestion="All band labels must be strings."
1027 )
1028
1029 if not label or label.strip() == "":
1030 raise create_validation_error(
1031 f"Band label at index {i} cannot be empty",
1032 param_name=f"{param_name}[{i}]",
1033 function_name=function_name,
1034 expected_type="non-empty string",
1035 actual_value=label,
1036 suggestion="All band labels must be non-empty strings."
1037 )
1038
1039 validated_labels.append(label.strip())
1040
1041 return validated_labels
1042
1043
1044def validate_antialiasing_samples(samples: int, param_name: str = "antialiasing_samples", function_name: str = None) -> int:
1045 """Validate antialiasing samples count."""
1046 if not isinstance(samples, int):
1047 raise create_validation_error(
1048 f"Antialiasing samples must be an integer, got {type(samples).__name__}",
1049 param_name=param_name,
1050 function_name=function_name,
1051 expected_type="int",
1052 actual_value=samples,
1053 suggestion="Provide an integer number of antialiasing samples."
1054 )
1055
1056 if samples <= 0:
1057 raise create_validation_error(
1058 f"Antialiasing samples must be positive, got {samples}",
1059 param_name=param_name,
1060 function_name=function_name,
1061 expected_type="positive integer",
1062 actual_value=samples,
1063 suggestion="Use a positive number of antialiasing samples (typically 1-1000)."
1064 )
1065
1066 # Practical upper limit check
1067 if samples > 10000:
1068 raise create_validation_error(
1069 f"Antialiasing samples ({samples}) is very high and may cause performance issues",
1070 param_name=param_name,
1071 function_name=function_name,
1072 expected_type="reasonable integer (1-10000)",
1073 actual_value=samples,
1074 suggestion="Consider using fewer antialiasing samples for better performance."
1075 )
1076
1077 return samples
validate_recursion_level(Any level, str param_name="recursion_level", str function_name=None)
Validate recursion level for tree generation.
Definition plugins.py:661
validate_temperature_response_params(Any value_at_25c, Any dha=None, Any topt=None, Any dhd=None, str param_prefix="", str function_name=None)
Validate temperature response parameters for photosynthetic processes.
Definition plugins.py:925
validate_empirical_coefficients(Any coefficients, str param_name="coefficients", str function_name=None)
Validate empirical model coefficients array or dataclass.
Definition plugins.py:767
validate_source_id_list(List[int] source_ids, str param_name="source_ids", str function_name=None)
Validate list of radiation source IDs.
Definition plugins.py:183
validate_direction_like(Any value, str param_name="direction", str function_name=None)
Validate a direction-like parameter (vec3, SphericalCoord, or 3-element list/tuple).
Definition plugins.py:242
validate_positive_integer_range(Any value, int min_val=1, int max_val=50, str param_name="value", str function_name=None)
Validate positive integer within a specified range.
Definition plugins.py:638
validate_direction_vector(Any direction, str param_name="direction", str function_name=None)
Validate direction vector for radiation sources.
Definition plugins.py:119
str validate_camera_label(str label, str param_name="camera_label", str function_name=None)
Validate camera label for radiation camera operations.
Definition plugins.py:971
validate_filename(Any filename, str param_name="filename", str function_name=None, List[str] allowed_extensions=None)
Validate filename parameters for file operations.
Definition plugins.py:530
validate_uuid_list(Any uuids, str param_name="uuids", str function_name=None, bool allow_empty=False)
Validate UUID list parameters.
Definition plugins.py:593
validate_subdivision_count(Any count, str param_name="subdivision_count", str function_name=None)
Validate subdivision count parameters.
Definition plugins.py:666
validate_wpt_parameters(float scale_factor=1.0, int recursion_level=5, int segment_resolution=10, str param_prefix="WPT")
Validate WeberPennTree generation parameters.
Definition plugins.py:302
validate_conductance(Any conductance, str param_name="conductance", str function_name=None)
Validate conductance values for photosynthesis calculations.
Definition plugins.py:741
validate_quantum_efficiency(Any alpha, str param_name="alpha", str function_name=None)
Validate quantum efficiency (alpha) parameter.
Definition plugins.py:877
validate_flux_value(float flux, str param_name="flux", str function_name=None)
Validate radiation flux value.
Definition plugins.py:63
validate_farquhar_coefficients(Any coefficients, str param_name="coefficients", str function_name=None)
Validate Farquhar model coefficients array or dataclass.
Definition plugins.py:805
validate_segment_resolution(Any resolution, int min_val=3, int max_val=100, str param_name="resolution", str function_name=None)
Validate segment resolution parameters for tree generation.
Definition plugins.py:456
validate_jmax(Any jmax, str param_name="Jmax", str function_name=None)
Validate maximum electron transport rate (Jmax) parameter.
Definition plugins.py:860
validate_size_like(Any value, str param_name="size", str function_name=None)
Validate a size-like parameter (vec2 or 2-element list/tuple).
Definition plugins.py:274
validate_scaling_factor(Any scale, float min_val=0.001, float max_val=1000.0, str param_name="scaling_factor", str function_name=None)
Validate scaling factor parameters.
Definition plugins.py:496
List[str] validate_band_labels_list(List[str] band_labels, str param_name="band_labels", str function_name=None)
Validate list of band labels for camera operations.
Definition plugins.py:996
validate_tree_id(Any tree_id, str param_name="tree_id", str function_name=None)
Validate WeberPennTree tree ID.
Definition plugins.py:432
validate_species_name(Any species, str param_name="species", str function_name=None)
Validate species name for photosynthesis library.
Definition plugins.py:675
validate_band_label(str label, str param_name="band_label", str function_name=None)
Validate radiation band label.
Definition plugins.py:137
validate_time_value(Any time_val, str param_name="time", str function_name=None)
Validate time values for energy balance calculations.
Definition plugins.py:368
validate_temperature(Any temperature, str param_name="temperature", str function_name=None)
Validate temperature values for photosynthesis calculations.
Definition plugins.py:702
validate_wavelength_range(float wavelength_min, float wavelength_max, str param_name_min="wavelength_min", str param_name_max="wavelength_max", str function_name=None)
Validate wavelength range for radiation modeling.
Definition plugins.py:18
validate_physical_quantity(Any value, str quantity_name, str expected_units=None, float min_val=None, float max_val=None, str param_name=None, str function_name=None)
Validate physical quantity values with optional range checking.
Definition plugins.py:394
validate_co2_concentration(Any co2, str param_name="co2_concentration", str function_name=None)
Validate CO2 concentration values for photosynthesis calculations.
Definition plugins.py:715
validate_dark_respiration(Any rd, str param_name="Rd", str function_name=None)
Validate dark respiration rate (Rd) parameter.
Definition plugins.py:894
validate_par_flux(Any par_flux, str param_name="PAR_flux", str function_name=None)
Validate PAR flux values for photosynthesis calculations.
Definition plugins.py:754
validate_ray_count(int count, str param_name="ray_count", str function_name=None)
Validate ray count for radiation simulations.
Definition plugins.py:86
validate_photosynthetic_rate(Any rate, str param_name="photosynthetic_rate", str function_name=None)
Validate photosynthetic rate values.
Definition plugins.py:728
int validate_antialiasing_samples(int samples, str param_name="antialiasing_samples", str function_name=None)
Validate antialiasing samples count.
Definition plugins.py:1045
validate_position_like(Any value, str param_name="position", str function_name=None)
Validate a position-like parameter (vec3 or 3-element list/tuple).
Definition plugins.py:212
validate_oxygen_concentration(Any o2, str param_name="oxygen", str function_name=None)
Validate oxygen concentration for photosynthesis calculations.
Definition plugins.py:911
validate_angle_degrees(Any angle, str param_name="angle", str function_name=None)
Validate angle values in degrees.
Definition plugins.py:479
validate_vcmax(Any vcmax, str param_name="Vcmax", str function_name=None)
Validate maximum carboxylation rate (Vcmax) parameter.
Definition plugins.py:843
validate_source_id(int source_id, str param_name="source_id", str function_name=None)
Validate radiation source ID.
Definition plugins.py:160