3from typing
import Any, List
4from enum
import IntEnum
8 """Helios primitive type enumeration."""
20 """Provenance of a polymesh object's per-vertex normals (helios-core 1.3.83)."""
26 """Granularity at which coincident vertices are treated as one shared vertex (helios-core 1.3.84).
28 Object types with no distinguished axis -- a Polymesh, a Tile -- report the same
29 topology for either mode.
36 WELD_CROSS_SECTION_ONLY = 1
38class int2(ctypes.Structure):
39 _fields_ = [(
'x', ctypes.c_int32), (
'y', ctypes.c_int32)]
42 return f
'int2({self.x}, {self.y})'
45 return f
'int2({self.x}, {self.y})'
48 """Create instance - only pass cls to prevent TypeError on Windows."""
49 return ctypes.Structure.__new__(cls)
53 if not isinstance(x, int):
54 raise ValueError(f
"int2.x must be an integer, got {type(x).__name__}: {x}")
55 if not isinstance(y, int):
56 raise ValueError(f
"int2.y must be an integer, got {type(y).__name__}: {y}")
62 self.
x = input_list[0]
63 self.
y = input_list[1]
66 return [self.
x, self.
y]
70class int3(ctypes.Structure):
71 _fields_ = [(
'x', ctypes.c_int32), (
'y', ctypes.c_int32), (
'z', ctypes.c_int32)]
74 return f
'int3({self.x}, {self.y}, {self.z})'
77 return f
'int3({self.x}, {self.y}, {self.z})'
80 """Create instance - only pass cls to prevent TypeError on Windows."""
81 return ctypes.Structure.__new__(cls)
83 def __init__(self, x:int=0, y:int=0, z:int=0):
85 if not isinstance(x, int):
86 raise ValueError(f
"int3.x must be an integer, got {type(x).__name__}: {x}")
87 if not isinstance(y, int):
88 raise ValueError(f
"int3.y must be an integer, got {type(y).__name__}: {y}")
89 if not isinstance(z, int):
90 raise ValueError(f
"int3.z must be an integer, got {type(z).__name__}: {z}")
96 def from_list(self, input_list:List[int]):
97 self.
x = input_list[0]
98 self.
y = input_list[1]
99 self.
z = input_list[2]
101 def to_list(self) -> List[int]:
102 return [self.
x, self.
y, self.
z]
106class int4(ctypes.Structure):
107 _fields_ = [(
'x', ctypes.c_int32), (
'y', ctypes.c_int32), (
'z', ctypes.c_int32), (
'w', ctypes.c_int32)]
110 return f
'int4({self.x}, {self.y}, {self.z}, {self.w})'
113 return f
'int4({self.x}, {self.y}, {self.z}, {self.w})'
115 def __new__(cls, x=None, y=None, z=None, w=None):
116 """Create instance - only pass cls to prevent TypeError on Windows."""
117 return ctypes.Structure.__new__(cls)
119 def __init__(self, x:int=0, y:int=0, z:int=0, w:int=0):
121 if not isinstance(x, int):
122 raise ValueError(f
"int4.x must be an integer, got {type(x).__name__}: {x}")
123 if not isinstance(y, int):
124 raise ValueError(f
"int4.y must be an integer, got {type(y).__name__}: {y}")
125 if not isinstance(z, int):
126 raise ValueError(f
"int4.z must be an integer, got {type(z).__name__}: {z}")
127 if not isinstance(w, int):
128 raise ValueError(f
"int4.w must be an integer, got {type(w).__name__}: {w}")
135 def from_list(self, input_list:List[int]):
136 self.
x = input_list[0]
137 self.
y = input_list[1]
138 self.
z = input_list[2]
139 self.
w = input_list[3]
142 return [self.
x, self.
y, self.
z, self.
w]
146class vec2(ctypes.Structure):
147 _fields_ = [(
'x', ctypes.c_float), (
'y', ctypes.c_float)]
150 return f
'vec2({self.x}, {self.y})'
153 return f
'vec2({self.x}, {self.y})'
156 """Create instance - only pass cls to prevent TypeError on Windows."""
157 return ctypes.Structure.__new__(cls)
162 raise ValueError(f
"vec2.x must be a finite number, got {type(x).__name__}: {x}. "
163 f
"Vector components must be finite (not NaN or infinity).")
165 raise ValueError(f
"vec2.y must be a finite number, got {type(y).__name__}: {y}. "
166 f
"Vector components must be finite (not NaN or infinity).")
172 self.
x = input_list[0]
173 self.
y = input_list[1]
175 def to_list(self) -> List[float]:
176 return [self.
x, self.
y]
179 """Return the magnitude (length) of the vector."""
181 return math.sqrt(self.
x * self.
x + self.
y * self.
y)
184 """Return a normalized copy of this vector (unit length)."""
188 return vec2(self.
x / mag, self.
y / mag)
192 """Check if value is a finite number (not NaN or inf)."""
194 float_value = float(value)
195 return math.isfinite(float_value)
196 except (ValueError, TypeError, OverflowError):
199class vec3(ctypes.Structure):
200 _fields_ = [(
'x', ctypes.c_float), (
'y', ctypes.c_float), (
'z', ctypes.c_float)]
203 return f
'vec3({self.x}, {self.y}, {self.z})'
206 return f
'vec3({self.x}, {self.y}, {self.z})'
208 def __new__(cls, x=None, y=None, z=None):
209 """Create instance - only pass cls to prevent TypeError on Windows."""
210 return ctypes.Structure.__new__(cls)
212 def __init__(self, x:float=0, y:float=0, z:float=0):
215 raise ValueError(f
"vec3.x must be a finite number, got {type(x).__name__}: {x}. "
216 f
"Vector components must be finite (not NaN or infinity).")
218 raise ValueError(f
"vec3.y must be a finite number, got {type(y).__name__}: {y}. "
219 f
"Vector components must be finite (not NaN or infinity).")
221 raise ValueError(f
"vec3.z must be a finite number, got {type(z).__name__}: {z}. "
222 f
"Vector components must be finite (not NaN or infinity).")
228 def from_list(self, input_list:List[float]):
229 self.
x = input_list[0]
230 self.
y = input_list[1]
231 self.
z = input_list[2]
233 def to_list(self) -> List[float]:
234 return [self.
x, self.
y, self.
z]
237 return (self.
x, self.
y, self.
z)
240 """Return the magnitude (length) of the vector."""
242 return math.sqrt(self.
x * self.
x + self.
y * self.
y + self.
z * self.
z)
245 """Return a normalized copy of this vector (unit length)."""
249 return vec3(self.
x / mag, self.
y / mag, self.
z / mag)
253 """Check if value is a finite number (not NaN or inf)."""
255 float_value = float(value)
256 return math.isfinite(float_value)
257 except (ValueError, TypeError, OverflowError):
261class vec4(ctypes.Structure):
262 _fields_ = [(
'x', ctypes.c_float), (
'y', ctypes.c_float), (
'z', ctypes.c_float), (
'w', ctypes.c_float)]
265 return f
'vec4({self.x}, {self.y}, {self.z}, {self.w})'
268 return f
'vec4({self.x}, {self.y}, {self.z}, {self.w})'
270 def __new__(cls, x=None, y=None, z=None, w=None):
271 """Create instance - only pass cls to prevent TypeError on Windows."""
272 return ctypes.Structure.__new__(cls)
274 def __init__(self, x:float=0, y:float=0, z:float=0, w:float=0):
277 raise ValueError(f
"vec4.x must be a finite number, got {type(x).__name__}: {x}. "
278 f
"Vector components must be finite (not NaN or infinity).")
280 raise ValueError(f
"vec4.y must be a finite number, got {type(y).__name__}: {y}. "
281 f
"Vector components must be finite (not NaN or infinity).")
283 raise ValueError(f
"vec4.z must be a finite number, got {type(z).__name__}: {z}. "
284 f
"Vector components must be finite (not NaN or infinity).")
286 raise ValueError(f
"vec4.w must be a finite number, got {type(w).__name__}: {w}. "
287 f
"Vector components must be finite (not NaN or infinity).")
294 def from_list(self, input_list:List[float]):
295 self.
x = input_list[0]
296 self.
y = input_list[1]
297 self.
z = input_list[2]
298 self.
w = input_list[3]
300 def to_list(self) -> List[float]:
301 return [self.
x, self.
y, self.
z, self.
w]
305 """Check if value is a finite number (not NaN or inf)."""
307 float_value = float(value)
308 return math.isfinite(float_value)
309 except (ValueError, TypeError, OverflowError):
315 _fields_ = [(
'r', ctypes.c_float), (
'g', ctypes.c_float), (
'b', ctypes.c_float)]
318 return f
'RGBcolor({self.r}, {self.g}, {self.b})'
321 return f
'RGBcolor({self.r}, {self.g}, {self.b})'
323 def __new__(cls, r=None, g=None, b=None):
324 """Create instance - only pass cls to prevent TypeError on Windows."""
325 return ctypes.Structure.__new__(cls)
327 def __init__(self, r:float=0, g:float=0, b:float=0):
329 self._validate_color_component(r,
'r')
330 self._validate_color_component(g,
'g')
331 self._validate_color_component(b,
'b')
337 def from_list(self, input_list:List[float]):
338 self.
r = input_list[0]
339 self.
g = input_list[1]
340 self.
b = input_list[2]
343 return [self.
r, self.
g, self.
b]
345 def scale(self, factor: float) ->
'RGBcolor':
346 """Return a scaled copy of this color, clamped to [0, 1]."""
348 min(1.0, max(0.0, self.
r * factor)),
349 min(1.0, max(0.0, self.
g * factor)),
350 min(1.0, max(0.0, self.
b * factor))
355 """Check if value is a finite number (not NaN or inf)."""
357 float_value = float(value)
358 return math.isfinite(float_value)
359 except (ValueError, TypeError, OverflowError):
364 """Validate a color component is finite and in range [0,1]."""
365 if not RGBcolor._is_finite_numeric(value):
366 raise ValueError(f
"RGBcolor.{component_name} must be a finite number, "
367 f
"got {type(value).__name__}: {value}. "
368 f
"Color components must be finite values between 0 and 1.")
370 if not (0.0 <= value <= 1.0):
371 raise ValueError(f
"RGBcolor.{component_name}={value} is outside valid range [0,1]. "
372 f
"Color components must be normalized values between 0 and 1.")
378 _fields_ = [(
'r', ctypes.c_float), (
'g', ctypes.c_float), (
'b', ctypes.c_float), (
'a', ctypes.c_float)]
381 return f
'RGBAcolor({self.r}, {self.g}, {self.b}, {self.a})'
384 return f
'RGBAcolor({self.r}, {self.g}, {self.b}, {self.a})'
386 def __new__(cls, r=None, g=None, b=None, a=None):
387 """Create instance - only pass cls to prevent TypeError on Windows."""
388 return ctypes.Structure.__new__(cls)
390 def __init__(self, r:float=0, g:float=0, b:float=0, a:float=0):
402 def from_list(self, input_list:List[float]):
403 self.
r = input_list[0]
404 self.
g = input_list[1]
405 self.
b = input_list[2]
406 self.a = input_list[3]
409 return [self.
r, self.
g, self.
b, self.
a]
411 def scale(self, factor: float) ->
'RGBAcolor':
412 """Return a scaled copy of this color, clamped to [0, 1]. Alpha unchanged."""
414 min(1.0, max(0.0, self.
r * factor)),
415 min(1.0, max(0.0, self.
g * factor)),
416 min(1.0, max(0.0, self.
b * factor)),
422 """Check if value is a finite number (not NaN or inf)."""
424 float_value = float(value)
425 return math.isfinite(float_value)
426 except (ValueError, TypeError, OverflowError):
431 """Validate a color component is finite and in range [0,1]."""
432 if not RGBAcolor._is_finite_numeric(value):
433 raise ValueError(f
"RGBAcolor.{component_name} must be a finite number, "
434 f
"got {type(value).__name__}: {value}. "
435 f
"Color components must be finite values between 0 and 1.")
437 if not (0.0 <= value <= 1.0):
438 raise ValueError(f
"RGBAcolor.{component_name}={value} is outside valid range [0,1]. "
439 f
"Color components must be normalized values between 0 and 1.")
445 (
'radius', ctypes.c_float),
446 (
'elevation', ctypes.c_float),
447 (
'zenith', ctypes.c_float),
448 (
'azimuth', ctypes.c_float)
452 return f
'SphericalCoord({self.radius}, {self.elevation}, {self.zenith}, {self.azimuth})'
455 return f
'SphericalCoord({self.radius}, {self.elevation}, {self.zenith}, {self.azimuth})'
457 def __new__(cls, radius=None, elevation=None, azimuth=None):
458 """Create instance - only pass cls to prevent TypeError on Windows."""
459 return ctypes.Structure.__new__(cls)
461 def __init__(self, radius:float=1, elevation:float=0, azimuth:float=0):
463 Initialize SphericalCoord fields with validation.
464 Do not call super().__init__() for Windows compatibility.
467 radius: Radius (default: 1)
468 elevation: Elevation angle in radians (default: 0)
469 azimuth: Azimuthal angle in radians (default: 0)
471 Note: zenith is automatically computed as (Ï€/2 - elevation) to match C++ behavior
475 raise ValueError(f
"SphericalCoord.radius must be a positive finite number, "
476 f
"got {type(radius).__name__}: {radius}. "
477 f
"Radius must be greater than 0.")
480 raise ValueError(f
"SphericalCoord.elevation must be a finite number, "
481 f
"got {type(elevation).__name__}: {elevation}. "
482 f
"Elevation angle must be finite (not NaN or infinity).")
485 raise ValueError(f
"SphericalCoord.azimuth must be a finite number, "
486 f
"got {type(azimuth).__name__}: {azimuth}. "
487 f
"Azimuth angle must be finite (not NaN or infinity).")
490 self.
radius = float(radius)
496 self.
radius = input_list[0]
498 self.
zenith = input_list[2]
501 def to_list(self) -> List[float]:
506 """Check if value is a finite number (not NaN or inf)."""
508 float_value = float(value)
509 return math.isfinite(float_value)
510 except (ValueError, TypeError, OverflowError):
517 Axis rotation structure for specifying shoot orientation in PlantArchitecture.
519 Represents rotation using pitch, yaw, and roll angles in **radians**, exactly as the
520 native ``helios::AxisRotation`` does: the values are passed through unchanged and the
521 plant-architecture library itself builds rotations such as ``0.05 * pi`` and ``2 * pi``.
522 Use ``math.radians()`` to convert from degrees. Used to define the orientation of
523 shoots, stems, and branches during plant construction.
526 (
'pitch', ctypes.c_float),
527 (
'yaw', ctypes.c_float),
528 (
'roll', ctypes.c_float)
532 return f
'AxisRotation({self.pitch}, {self.yaw}, {self.roll})'
535 return f
'AxisRotation({self.pitch}, {self.yaw}, {self.roll})'
537 def __new__(cls, pitch=None, yaw=None, roll=None):
539 Create AxisRotation instance.
540 Only pass cls to parent __new__ to prevent TypeError on Windows.
542 return ctypes.Structure.__new__(cls)
544 def __init__(self, pitch:float=0, yaw:float=0, roll:float=0):
546 Initialize AxisRotation fields with validation.
547 Do not call super().__init__() for Windows compatibility.
550 pitch: Pitch angle in radians (rotation about transverse axis)
551 yaw: Yaw angle in radians (rotation about vertical axis)
552 roll: Roll angle in radians (rotation about longitudinal axis)
555 ValueError: If any angle value is not finite
559 raise ValueError(f
"AxisRotation.pitch must be a finite number, got {type(pitch).__name__}: {pitch}. "
560 f
"Rotation angles must be finite (not NaN or infinity).")
562 raise ValueError(f
"AxisRotation.yaw must be a finite number, got {type(yaw).__name__}: {yaw}. "
563 f
"Rotation angles must be finite (not NaN or infinity).")
565 raise ValueError(f
"AxisRotation.roll must be a finite number, got {type(roll).__name__}: {roll}. "
566 f
"Rotation angles must be finite (not NaN or infinity).")
568 self.
pitch = float(pitch)
569 self.
yaw = float(yaw)
570 self.
roll = float(roll)
572 def from_list(self, input_list:List[float]):
573 """Initialize from list [pitch, yaw, roll]"""
574 if len(input_list) < 3:
575 raise ValueError(
"AxisRotation.from_list requires a list with at least 3 elements [pitch, yaw, roll]")
576 self.
pitch = input_list[0]
578 self.
roll = input_list[2]
581 """Convert to list [pitch, yaw, roll]"""
586 """Check if value is a finite number (not NaN or inf)."""
588 float_value = float(value)
589 return math.isfinite(float_value)
590 except (ValueError, TypeError, OverflowError):
597 Parameters controlling the adaptive sub-patch refinement of an adaptive tile object.
599 Sub-patch edge length grows with distance from ``target``, from ``subpatch_size_min`` at the
600 target itself up to ``subpatch_size_max`` at the point of the tile farthest from it.
601 Refinement is performed by recursive quadtree subdivision, so the achieved sizes are the
602 requested sizes rounded onto a power-of-two ladder; both ends are typically within about 20%
603 of the request. Query what was actually achieved with
604 ``Context.getAdaptiveTileObjectSubpatchSizeRange()``.
607 target: Point of maximum refinement, in tile-local coordinates relative to the tile center
608 and before rotation is applied (default: (0, 0), the tile center). A target outside the
609 tile is permitted, but the finest requested sub-patch size will then not be reached
610 anywhere and helios issues a warning.
611 subpatch_size_min: Requested edge length of the finest sub-patches, which occur at the
612 target point (default: 0.05, i.e. 5 cm for a scene measured in meters).
613 subpatch_size_max: Requested edge length of the coarsest sub-patches, which occur farthest
614 from the target (default: 1.0). Must be no more than half the smaller tile dimension.
615 transition_exponent: Exponent controlling how rapidly sub-patch size grows with distance
616 from the target (default: 0.35, which suits a typical ground plane). Useful values run
617 from about 0.25 to 1. This does not change the achieved size range, but it is by far
618 the most consequential parameter for the sub-patch count -- on a 50 m tile refined from
619 2 m to 2 cm, 0.25 gives roughly 9 thousand sub-patches and 2 gives roughly 2 million.
620 Use ``Context.predictAdaptiveTileObjectSubpatchCount()`` to check before building.
623 >>> refinement = AdaptiveTileRefinement(target=vec2(0, 0), subpatch_size_min=0.02,
624 ... subpatch_size_max=2.0)
625 >>> refinement.subpatch_size_min
630 (
'subpatch_size_min', ctypes.c_float),
631 (
'subpatch_size_max', ctypes.c_float),
632 (
'transition_exponent', ctypes.c_float)
636 return (f
'AdaptiveTileRefinement(target=vec2({self.target.x}, {self.target.y}), '
637 f
'subpatch_size_min={self.subpatch_size_min}, '
638 f
'subpatch_size_max={self.subpatch_size_max}, '
639 f
'transition_exponent={self.transition_exponent})')
644 def __new__(cls, target=None, subpatch_size_min=None, subpatch_size_max=None,
645 transition_exponent=None):
646 """Create instance - only pass cls to prevent TypeError on Windows."""
647 return ctypes.Structure.__new__(cls)
649 def __init__(self, target:
'vec2' =
None, subpatch_size_min: float = 0.05,
650 subpatch_size_max: float = 1.0, transition_exponent: float = 0.35):
652 Initialize AdaptiveTileRefinement fields with validation.
653 Do not call super().__init__() for Windows compatibility.
655 Defaults match helios::AdaptiveTileRefinement.
658 ValueError: If target is not a vec2, if either size is not a positive finite number,
659 if subpatch_size_min exceeds subpatch_size_max, if their ratio exceeds the largest
660 supported value of 16777216, or if transition_exponent is not positive and finite.
663 target =
vec2(0.0, 0.0)
664 if not isinstance(target, vec2):
665 raise ValueError(f
"AdaptiveTileRefinement.target must be a vec2, "
666 f
"got {type(target).__name__}: {target}")
668 raise ValueError(f
"AdaptiveTileRefinement.target must be finite, got {target}")
671 raise ValueError(f
"AdaptiveTileRefinement.subpatch_size_min must be a positive finite "
672 f
"number, got {type(subpatch_size_min).__name__}: {subpatch_size_min}")
674 raise ValueError(f
"AdaptiveTileRefinement.subpatch_size_max must be a positive finite "
675 f
"number, got {type(subpatch_size_max).__name__}: {subpatch_size_max}")
676 if float(subpatch_size_min) > float(subpatch_size_max):
677 raise ValueError(f
"AdaptiveTileRefinement.subpatch_size_min of {subpatch_size_min} is "
678 f
"greater than subpatch_size_max of {subpatch_size_max}.")
679 if float(subpatch_size_max) / float(subpatch_size_min) > 16777216.0:
680 raise ValueError(f
"AdaptiveTileRefinement size ratio of "
681 f
"{float(subpatch_size_max) / float(subpatch_size_min)} exceeds the "
682 f
"largest supported ratio of 16777216. Increase subpatch_size_min or "
683 f
"decrease subpatch_size_max.")
684 if not self.
_is_finite_numeric(transition_exponent)
or float(transition_exponent) <= 0:
685 raise ValueError(f
"AdaptiveTileRefinement.transition_exponent must be a positive "
686 f
"finite number, got {type(transition_exponent).__name__}: "
687 f
"{transition_exponent}")
694 def from_list(self, input_list: List[float]):
695 """Initialize from list [target_x, target_y, subpatch_size_min, subpatch_size_max, transition_exponent]"""
696 if len(input_list) < 5:
697 raise ValueError(
"AdaptiveTileRefinement.from_list requires a list with at least 5 "
698 "elements [target_x, target_y, subpatch_size_min, subpatch_size_max, "
699 "transition_exponent]")
700 self.
target =
vec2(float(input_list[0]), float(input_list[1]))
707 Convert to the flat 5-element form used across the C ABI.
709 Element order matches the order helios itself uses to serialize the struct to XML:
710 [target_x, target_y, subpatch_size_min, subpatch_size_max, transition_exponent]
717 """Check if value is a finite number (not NaN or inf)."""
719 float_value = float(value)
720 return math.isfinite(float_value)
721 except (ValueError, TypeError, OverflowError):
727 """Make an int2 from two integers"""
730def make_SphericalCoord(elevation_radians: float, azimuth_radians: float) -> SphericalCoord:
732 Make a SphericalCoord by specifying elevation and azimuth (C++ API compatibility).
735 elevation_radians: Elevation angle in radians
736 azimuth_radians: Azimuthal angle in radians
739 SphericalCoord with radius=1, and automatically computed zenith
741 return SphericalCoord(radius=1, elevation=elevation_radians, azimuth=azimuth_radians)
743def make_int3(x: int, y: int, z: int) -> int3:
744 """Make an int3 from three integers"""
747def make_int4(x: int, y: int, z: int, w: int) -> int4:
748 """Make an int4 from four integers"""
749 return int4(x, y, z, w)
751def make_vec2(x: float, y: float) -> vec2:
752 """Make a vec2 from two floats"""
756 """Make a vec3 from three floats"""
759def make_vec4(x: float, y: float, z: float, w: float) -> vec4:
760 """Make a vec4 from four floats"""
761 return vec4(x, y, z, w)
764 """Make an RGBcolor from three floats"""
767def make_RGBAcolor(r: float, g: float, b: float, a: float) -> RGBAcolor:
768 """Make an RGBAcolor from four floats"""
772 """Make an AxisRotation from three angles in degrees"""
776class Time(ctypes.Structure):
777 """Helios Time structure for representing time values."""
778 _fields_ = [(
'second', ctypes.c_int32), (
'minute', ctypes.c_int32), (
'hour', ctypes.c_int32)]
781 return f
'Time({self.hour:02d}:{self.minute:02d}:{self.second:02d})'
784 return f
'{self.hour:02d}:{self.minute:02d}:{self.second:02d}'
786 def __new__(cls, hour=None, minute=None, second=None):
787 """Create instance - only pass cls to prevent TypeError on Windows."""
788 return ctypes.Structure.__new__(cls)
790 def __init__(self, hour: int = 0, minute: int = 0, second: int = 0):
792 Initialize Time fields with validation.
793 Do not call super().__init__() for Windows compatibility.
797 minute: Minute (0-59)
798 second: Second (0-59)
801 if not isinstance(hour, int):
802 raise ValueError(f
"Time.hour must be an integer, got {type(hour).__name__}: {hour}")
803 if not isinstance(minute, int):
804 raise ValueError(f
"Time.minute must be an integer, got {type(minute).__name__}: {minute}")
805 if not isinstance(second, int):
806 raise ValueError(f
"Time.second must be an integer, got {type(second).__name__}: {second}")
808 if hour < 0
or hour > 23:
809 raise ValueError(f
"Time.hour must be between 0 and 23, got: {hour}")
810 if minute < 0
or minute > 59:
811 raise ValueError(f
"Time.minute must be between 0 and 59, got: {minute}")
812 if second < 0
or second > 59:
813 raise ValueError(f
"Time.second must be between 0 and 59, got: {second}")
820 def from_list(self, input_list: List[int]):
821 """Initialize from a list [hour, minute, second]"""
822 if len(input_list) < 3:
823 raise ValueError(
"Time.from_list requires a list with at least 3 elements [hour, minute, second]")
824 self.hour = input_list[0]
825 self.minute = input_list[1]
826 self.second = input_list[2]
828 def to_list(self) -> List[int]:
829 """Convert to list [hour, minute, second]"""
830 return [self.hour, self.minute, self.second]
832 def __eq__(self, other) -> bool:
833 """Check equality with another Time object"""
834 if not isinstance(other, Time):
836 return (self.hour == other.hour
and
837 self.minute == other.minute
and
838 self.second == other.second)
840 def __ne__(self, other) -> bool:
841 """Check inequality with another Time object"""
842 return not self.__eq__(other)
845class Date(ctypes.Structure):
846 """Helios Date structure for representing date values."""
847 _fields_ = [(
'day', ctypes.c_int32), (
'month', ctypes.c_int32), (
'year', ctypes.c_int32)]
850 return f
'Date({self.year}-{self.month:02d}-{self.day:02d})'
853 return f
'{self.year}-{self.month:02d}-{self.day:02d}'
855 def __new__(cls, year=None, month=None, day=None):
856 """Create instance - only pass cls to prevent TypeError on Windows."""
857 return ctypes.Structure.__new__(cls)
859 def __init__(self, year: int = 2023, month: int = 1, day: int = 1):
861 Initialize Date fields with validation.
862 Do not call super().__init__() for Windows compatibility.
865 year: Year (1900-3000)
870 if not isinstance(year, int):
871 raise ValueError(f
"Date.year must be an integer, got {type(year).__name__}: {year}")
872 if not isinstance(month, int):
873 raise ValueError(f
"Date.month must be an integer, got {type(month).__name__}: {month}")
874 if not isinstance(day, int):
875 raise ValueError(f
"Date.day must be an integer, got {type(day).__name__}: {day}")
877 if year < 1900
or year > 3000:
878 raise ValueError(f
"Date.year must be between 1900 and 3000, got: {year}")
879 if month < 1
or month > 12:
880 raise ValueError(f
"Date.month must be between 1 and 12, got: {month}")
881 if day < 1
or day > 31:
882 raise ValueError(f
"Date.day must be between 1 and 31, got: {day}")
889 def from_list(self, input_list: List[int]):
890 """Initialize from a list [year, month, day]"""
891 if len(input_list) < 3:
892 raise ValueError(
"Date.from_list requires a list with at least 3 elements [year, month, day]")
893 self.year = input_list[0]
894 self.month = input_list[1]
895 self.day = input_list[2]
897 def to_list(self) -> List[int]:
898 """Convert to list [year, month, day]"""
899 return [self.year, self.month, self.day]
901 def JulianDay(self) -> int:
902 """Calculate Julian day number for this date."""
903 a = (14 - self.month) // 12
904 y = self.year + 4800 - a
905 m = self.month + 12 * a - 3
906 return self.day + (153 * m + 2) // 5 + 365 * y + y // 4 - y // 100 + y // 400 - 32045
908 def incrementDay(self) -> 'Date':
909 """Return a new Date object incremented by one day."""
911 days_in_month = calendar.monthrange(self.year, self.month)[1]
913 new_day = self.day + 1
914 new_month = self.month
917 if new_day > days_in_month:
924 return Date(new_year, new_month, new_day)
927 """Check if this date's year is a leap year."""
928 return (self.
year % 4 == 0
and self.
year % 100 != 0)
or (self.
year % 400 == 0)
930 def __eq__(self, other) -> bool:
931 """Check equality with another Date object"""
932 if not isinstance(other, Date):
934 return (self.
year == other.year
and
935 self.
month == other.month
and
936 self.
day == other.day)
938 def __ne__(self, other) -> bool:
939 """Check inequality with another Date object"""
943def make_Time(hour: int, minute: int, second: int) -> Time:
944 """Make a Time from hour, minute, second"""
945 return Time(hour, minute, second)
947def make_Date(year: int, month: int, day: int) -> Date:
948 """Make a Date from year, month, day"""
949 return Date(year, month, day)
953 """Geographic location for solar position and radiation calculations.
955 Mirrors helios::Location: latitude in degrees (+N / -S), longitude in degrees
956 (+W / -E per Helios convention), UTC offset in hours (+moving West), and
957 altitude of the local Cartesian origin in meters above sea level (default 0).
959 Fields are validated against the same ranges as helios::Location::validate():
961 | Field | Range | Note |
962 | ----- | ----- | ---- |
963 | latitude | -90 to 90 | |
964 | longitude | -180 to 180 | Helios counts longitude positive moving West |
965 | utc_offset | -14 to 12 | Asymmetric; see below |
966 | altitude | any finite value | No non-arbitrary bound exists for a scene |
968 The UTC offset spans -14 to +12 rather than -12 to +12 because Helios counts the
969 offset positive moving West: the real-world span of UTC-12 through UTC+14
970 (Kiribati keeps the latter) inverts to +12 through -14.
972 __slots__ = (
"latitude",
"longitude",
"utc_offset",
"altitude")
974 def __init__(self, latitude: float = 38.55, longitude: float = 121.76, utc_offset: float = 8.0, altitude: float = 0.0):
975 if not isinstance(latitude, (int, float)):
976 raise ValueError(f
"latitude must be a number, got {type(latitude).__name__}")
977 if not isinstance(longitude, (int, float)):
978 raise ValueError(f
"longitude must be a number, got {type(longitude).__name__}")
979 if not isinstance(utc_offset, (int, float)):
980 raise ValueError(f
"utc_offset must be a number, got {type(utc_offset).__name__}")
981 if not isinstance(altitude, (int, float)):
982 raise ValueError(f
"altitude must be a number, got {type(altitude).__name__}")
988 latitude, longitude = float(latitude), float(longitude)
989 utc_offset, altitude = float(utc_offset), float(altitude)
991 if not (-90.0 <= latitude <= 90.0):
993 f
"Latitude of {latitude} degrees is out of range (should be -90 to 90)."
995 if not (-180.0 <= longitude <= 180.0):
997 f
"Longitude of {longitude} degrees is out of range (should be -180 to "
998 f
"180). Note that Helios counts longitude positive in the Western "
1001 if not (-14.0 <= utc_offset <= 12.0):
1003 f
"UTC offset of {utc_offset} hours is out of range (should be -14 to "
1004 f
"12). Note that Helios counts the UTC offset positive moving West, so "
1005 f
"UTC-8 is an offset of +8."
1007 if not math.isfinite(altitude):
1009 f
"Altitude of {altitude} meters is not a finite value."
1012 object.__setattr__(self,
"latitude", latitude)
1013 object.__setattr__(self,
"longitude", longitude)
1014 object.__setattr__(self,
"utc_offset", utc_offset)
1015 object.__setattr__(self,
"altitude", altitude)
1017 def __setattr__(self, name, value):
1019 raise AttributeError(f
"Location is immutable; cannot reassign '{name}'")
1021 def __repr__(self) -> str:
1022 return (f
"Location(latitude={self.latitude}, longitude={self.longitude}, "
1023 f
"utc_offset={self.utc_offset}, altitude={self.altitude})")
1025 def __eq__(self, other) -> bool:
1026 if not isinstance(other, Location):
1028 return (self.latitude == other.latitude
and
1029 self.longitude == other.longitude
and
1030 self.utc_offset == other.utc_offset
and
1031 self.altitude == other.altitude)
1033 def __ne__(self, other) -> bool:
1034 return not self.__eq__(other)
1036 def __hash__(self) -> int:
1037 return hash((self.latitude, self.longitude, self.utc_offset, self.altitude))
1040def make_Location(latitude: float, longitude: float, utc_offset: float, altitude: float = 0.0) -> Location:
1041 """Make a Location from latitude (deg), longitude (deg), UTC offset (hours), and altitude (m)."""
1042 return Location(latitude, longitude, utc_offset, altitude)
Parameters controlling the adaptive sub-patch refinement of an adaptive tile object.
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
List[float] to_list(self)
Convert to the flat 5-element form used across the C ABI.
__new__(cls, target=None, subpatch_size_min=None, subpatch_size_max=None, transition_exponent=None)
Create only pass cls to prevent TypeError on Windows.
from_list(self, List[float] input_list)
Initialize from list [target_x, target_y, subpatch_size_min, subpatch_size_max, transition_exponent].
__init__(self, 'vec2' target=None, float subpatch_size_min=0.05, float subpatch_size_max=1.0, float transition_exponent=0.35)
Initialize AdaptiveTileRefinement fields with validation.
subpatch_size_min
Requested edge length of the finest sub-patches, which occur at the.
subpatch_size_max
Requested edge length of the coarsest sub-patches, which occur farthest.
target
Point of maximum refinement, in tile-local coordinates relative to the tile center.
transition_exponent
Exponent controlling how rapidly sub-patch size grows with distance.
Axis rotation structure for specifying shoot orientation in PlantArchitecture.
__init__(self, float pitch=0, float yaw=0, float roll=0)
Initialize AxisRotation fields with validation.
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
List[float] to_list(self)
Convert to list [pitch, yaw, roll].
__new__(cls, pitch=None, yaw=None, roll=None)
Create AxisRotation instance.
from_list(self, List[float] input_list)
Initialize from list [pitch, yaw, roll].
Helios Date structure for representing date values.
__init__(self, int year=2023, int month=1, int day=1)
Initialize Date fields with validation.
bool isLeapYear(self)
Check if this date's year is a leap year.
bool __ne__(self, other)
Check inequality with another Date object.
bool __eq__(self, other)
Check equality with another Date object.
Geographic location for solar position and radiation calculations.
Helios primitive type enumeration.
List[float] to_list(self)
_validate_color_component(value, component_name)
Validate a color component is finite and in range [0,1].
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
'RGBAcolor' scale(self, float factor)
Return a scaled copy of this color, clamped to [0, 1].
__init__(self, float r=0, float g=0, float b=0, float a=0)
__new__(cls, r=None, g=None, b=None, a=None)
Create only pass cls to prevent TypeError on Windows.
'RGBcolor' scale(self, float factor)
Return a scaled copy of this color, clamped to [0, 1].
__new__(cls, r=None, g=None, b=None)
Create only pass cls to prevent TypeError on Windows.
__init__(self, float r=0, float g=0, float b=0)
from_list(self, List[float] input_list)
List[float] to_list(self)
_validate_color_component(value, component_name)
Validate a color component is finite and in range [0,1].
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
List[float] to_list(self)
from_list(self, List[float] input_list)
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
__init__(self, float radius=1, float elevation=0, float azimuth=0)
Initialize SphericalCoord fields with validation.
__new__(cls, radius=None, elevation=None, azimuth=None)
Create only pass cls to prevent TypeError on Windows.
Helios Time structure for representing time values.
__new__(cls, hour=None, minute=None, second=None)
Create only pass cls to prevent TypeError on Windows.
List[int] to_list(self)
Convert to list [hour, minute, second].
from_list(self, List[int] input_list)
Initialize from a list [hour, minute, second].
__init__(self, int hour=0, int minute=0, int second=0)
Initialize Time fields with validation.
Provenance of a polymesh object's per-vertex normals (helios-core 1.3.83).
Granularity at which coincident vertices are treated as one shared vertex (helios-core 1....
from_list(self, List[int] input_list)
__init__(self, int x=0, int y=0)
__new__(cls, x=None, y=None)
Create only pass cls to prevent TypeError on Windows.
__init__(self, int x=0, int y=0, int z=0)
from_list(self, List[int] input_list)
__new__(cls, x=None, y=None, z=None)
Create only pass cls to prevent TypeError on Windows.
from_list(self, List[int] input_list)
__init__(self, int x=0, int y=0, int z=0, int w=0)
__new__(cls, x=None, y=None, z=None, w=None)
Create only pass cls to prevent TypeError on Windows.
from_list(self, List[float] input_list)
'vec2' normalize(self)
Return a normalized copy of this vector (unit length).
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
float magnitude(self)
Return the magnitude (length) of the vector.
__new__(cls, x=None, y=None)
Create only pass cls to prevent TypeError on Windows.
List[float] to_list(self)
__init__(self, float x=0, float y=0)
List[float] to_list(self)
__init__(self, float x=0, float y=0, float z=0)
from_list(self, List[float] input_list)
'vec3' normalize(self)
Return a normalized copy of this vector (unit length).
__new__(cls, x=None, y=None, z=None)
Create only pass cls to prevent TypeError on Windows.
float magnitude(self)
Return the magnitude (length) of the vector.
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
List[float] to_list(self)
bool _is_finite_numeric(value)
Check if value is a finite number (not NaN or inf).
__init__(self, float x=0, float y=0, float z=0, float w=0)
__new__(cls, x=None, y=None, z=None, w=None)
Create only pass cls to prevent TypeError on Windows.
from_list(self, List[float] input_list)
RGBAcolor make_RGBAcolor(float r, float g, float b, float a)
Make an RGBAcolor from four floats.
AxisRotation make_AxisRotation(float pitch, float yaw, float roll)
Make an AxisRotation from three angles in degrees.
int3 make_int3(int x, int y, int z)
Make an int3 from three integers.
Time make_Time(int hour, int minute, int second)
Make a Time from hour, minute, second.
RGBcolor make_RGBcolor(float r, float g, float b)
Make an RGBcolor from three floats.
Date make_Date(int year, int month, int day)
Make a Date from year, month, day.
int4 make_int4(int x, int y, int z, int w)
Make an int4 from four integers.
SphericalCoord make_SphericalCoord(float elevation_radians, float azimuth_radians)
Make a SphericalCoord by specifying elevation and azimuth (C++ API compatibility).
vec2 make_vec2(float x, float y)
Make a vec2 from two floats.
vec4 make_vec4(float x, float y, float z, float w)
Make a vec4 from four floats.
Location make_Location(float latitude, float longitude, float utc_offset, float altitude=0.0)
Make a Location from latitude (deg), longitude (deg), UTC offset (hours), and altitude (m).
int2 make_int2(int x, int y)
Make an int2 from two integers.
vec3 make_vec3(float x, float y, float z)
Make a vec3 from three floats.