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**.
520 Use ``math.radians()`` to convert from degrees. Used to define the orientation of
521 shoots, stems, and branches during plant construction.
524 (
'pitch', ctypes.c_float),
525 (
'yaw', ctypes.c_float),
526 (
'roll', ctypes.c_float)
530 return f
'AxisRotation({self.pitch}, {self.yaw}, {self.roll})'
533 return f
'AxisRotation({self.pitch}, {self.yaw}, {self.roll})'
535 def __new__(cls, pitch=None, yaw=None, roll=None):
537 Create AxisRotation instance.
538 Only pass cls to parent __new__ to prevent TypeError on Windows.
540 return ctypes.Structure.__new__(cls)
542 def __init__(self, pitch:float=0, yaw:float=0, roll:float=0):
544 Initialize AxisRotation fields with validation.
545 Do not call super().__init__() for Windows compatibility.
548 pitch: Pitch angle in radians (rotation about transverse axis)
549 yaw: Yaw angle in radians (rotation about vertical axis)
550 roll: Roll angle in radians (rotation about longitudinal axis)
553 ValueError: If any angle value is not finite
557 raise ValueError(f
"AxisRotation.pitch must be a finite number, got {type(pitch).__name__}: {pitch}. "
558 f
"Rotation angles must be finite (not NaN or infinity).")
560 raise ValueError(f
"AxisRotation.yaw must be a finite number, got {type(yaw).__name__}: {yaw}. "
561 f
"Rotation angles must be finite (not NaN or infinity).")
563 raise ValueError(f
"AxisRotation.roll must be a finite number, got {type(roll).__name__}: {roll}. "
564 f
"Rotation angles must be finite (not NaN or infinity).")
566 self.
pitch = float(pitch)
567 self.
yaw = float(yaw)
568 self.
roll = float(roll)
570 def from_list(self, input_list:List[float]):
571 """Initialize from list [pitch, yaw, roll]"""
572 if len(input_list) < 3:
573 raise ValueError(
"AxisRotation.from_list requires a list with at least 3 elements [pitch, yaw, roll]")
574 self.
pitch = input_list[0]
576 self.
roll = input_list[2]
579 """Convert to list [pitch, yaw, roll]"""
584 """Check if value is a finite number (not NaN or inf)."""
586 float_value = float(value)
587 return math.isfinite(float_value)
588 except (ValueError, TypeError, OverflowError):
595 Parameters controlling the adaptive sub-patch refinement of an adaptive tile object.
597 Sub-patch edge length grows with distance from ``target``, from ``subpatch_size_min`` at the
598 target itself up to ``subpatch_size_max`` at the point of the tile farthest from it.
599 Refinement is performed by recursive quadtree subdivision, so the achieved sizes are the
600 requested sizes rounded onto a power-of-two ladder; both ends are typically within about 20%
601 of the request. Query what was actually achieved with
602 ``Context.getAdaptiveTileObjectSubpatchSizeRange()``.
605 target: Point of maximum refinement, in tile-local coordinates relative to the tile center
606 and before rotation is applied (default: (0, 0), the tile center). A target outside the
607 tile is permitted, but the finest requested sub-patch size will then not be reached
608 anywhere and helios issues a warning.
609 subpatch_size_min: Requested edge length of the finest sub-patches, which occur at the
610 target point (default: 0.05, i.e. 5 cm for a scene measured in meters).
611 subpatch_size_max: Requested edge length of the coarsest sub-patches, which occur farthest
612 from the target (default: 1.0). Must be no more than half the smaller tile dimension.
613 transition_exponent: Exponent controlling how rapidly sub-patch size grows with distance
614 from the target (default: 0.35, which suits a typical ground plane). Useful values run
615 from about 0.25 to 1. This does not change the achieved size range, but it is by far
616 the most consequential parameter for the sub-patch count -- on a 50 m tile refined from
617 2 m to 2 cm, 0.25 gives roughly 9 thousand sub-patches and 2 gives roughly 2 million.
618 Use ``Context.predictAdaptiveTileObjectSubpatchCount()`` to check before building.
621 >>> refinement = AdaptiveTileRefinement(target=vec2(0, 0), subpatch_size_min=0.02,
622 ... subpatch_size_max=2.0)
623 >>> refinement.subpatch_size_min
628 (
'subpatch_size_min', ctypes.c_float),
629 (
'subpatch_size_max', ctypes.c_float),
630 (
'transition_exponent', ctypes.c_float)
634 return (f
'AdaptiveTileRefinement(target=vec2({self.target.x}, {self.target.y}), '
635 f
'subpatch_size_min={self.subpatch_size_min}, '
636 f
'subpatch_size_max={self.subpatch_size_max}, '
637 f
'transition_exponent={self.transition_exponent})')
642 def __new__(cls, target=None, subpatch_size_min=None, subpatch_size_max=None,
643 transition_exponent=None):
644 """Create instance - only pass cls to prevent TypeError on Windows."""
645 return ctypes.Structure.__new__(cls)
647 def __init__(self, target:
'vec2' =
None, subpatch_size_min: float = 0.05,
648 subpatch_size_max: float = 1.0, transition_exponent: float = 0.35):
650 Initialize AdaptiveTileRefinement fields with validation.
651 Do not call super().__init__() for Windows compatibility.
653 Defaults match helios::AdaptiveTileRefinement.
656 ValueError: If target is not a vec2, if either size is not a positive finite number,
657 if subpatch_size_min exceeds subpatch_size_max, if their ratio exceeds the largest
658 supported value of 16777216, or if transition_exponent is not positive and finite.
661 target =
vec2(0.0, 0.0)
662 if not isinstance(target, vec2):
663 raise ValueError(f
"AdaptiveTileRefinement.target must be a vec2, "
664 f
"got {type(target).__name__}: {target}")
666 raise ValueError(f
"AdaptiveTileRefinement.target must be finite, got {target}")
669 raise ValueError(f
"AdaptiveTileRefinement.subpatch_size_min must be a positive finite "
670 f
"number, got {type(subpatch_size_min).__name__}: {subpatch_size_min}")
672 raise ValueError(f
"AdaptiveTileRefinement.subpatch_size_max must be a positive finite "
673 f
"number, got {type(subpatch_size_max).__name__}: {subpatch_size_max}")
674 if float(subpatch_size_min) > float(subpatch_size_max):
675 raise ValueError(f
"AdaptiveTileRefinement.subpatch_size_min of {subpatch_size_min} is "
676 f
"greater than subpatch_size_max of {subpatch_size_max}.")
677 if float(subpatch_size_max) / float(subpatch_size_min) > 16777216.0:
678 raise ValueError(f
"AdaptiveTileRefinement size ratio of "
679 f
"{float(subpatch_size_max) / float(subpatch_size_min)} exceeds the "
680 f
"largest supported ratio of 16777216. Increase subpatch_size_min or "
681 f
"decrease subpatch_size_max.")
682 if not self.
_is_finite_numeric(transition_exponent)
or float(transition_exponent) <= 0:
683 raise ValueError(f
"AdaptiveTileRefinement.transition_exponent must be a positive "
684 f
"finite number, got {type(transition_exponent).__name__}: "
685 f
"{transition_exponent}")
692 def from_list(self, input_list: List[float]):
693 """Initialize from list [target_x, target_y, subpatch_size_min, subpatch_size_max, transition_exponent]"""
694 if len(input_list) < 5:
695 raise ValueError(
"AdaptiveTileRefinement.from_list requires a list with at least 5 "
696 "elements [target_x, target_y, subpatch_size_min, subpatch_size_max, "
697 "transition_exponent]")
698 self.
target =
vec2(float(input_list[0]), float(input_list[1]))
705 Convert to the flat 5-element form used across the C ABI.
707 Element order matches the order helios itself uses to serialize the struct to XML:
708 [target_x, target_y, subpatch_size_min, subpatch_size_max, transition_exponent]
715 """Check if value is a finite number (not NaN or inf)."""
717 float_value = float(value)
718 return math.isfinite(float_value)
719 except (ValueError, TypeError, OverflowError):
725 """Make an int2 from two integers"""
728def make_SphericalCoord(elevation_radians: float, azimuth_radians: float) -> SphericalCoord:
730 Make a SphericalCoord by specifying elevation and azimuth (C++ API compatibility).
733 elevation_radians: Elevation angle in radians
734 azimuth_radians: Azimuthal angle in radians
737 SphericalCoord with radius=1, and automatically computed zenith
739 return SphericalCoord(radius=1, elevation=elevation_radians, azimuth=azimuth_radians)
741def make_int3(x: int, y: int, z: int) -> int3:
742 """Make an int3 from three integers"""
745def make_int4(x: int, y: int, z: int, w: int) -> int4:
746 """Make an int4 from four integers"""
747 return int4(x, y, z, w)
749def make_vec2(x: float, y: float) -> vec2:
750 """Make a vec2 from two floats"""
754 """Make a vec3 from three floats"""
757def make_vec4(x: float, y: float, z: float, w: float) -> vec4:
758 """Make a vec4 from four floats"""
759 return vec4(x, y, z, w)
762 """Make an RGBcolor from three floats"""
765def make_RGBAcolor(r: float, g: float, b: float, a: float) -> RGBAcolor:
766 """Make an RGBAcolor from four floats"""
770 """Make an AxisRotation from three angles in radians"""
774class Time(ctypes.Structure):
775 """Helios Time structure for representing time values."""
776 _fields_ = [(
'second', ctypes.c_int32), (
'minute', ctypes.c_int32), (
'hour', ctypes.c_int32)]
779 return f
'Time({self.hour:02d}:{self.minute:02d}:{self.second:02d})'
782 return f
'{self.hour:02d}:{self.minute:02d}:{self.second:02d}'
784 def __new__(cls, hour=None, minute=None, second=None):
785 """Create instance - only pass cls to prevent TypeError on Windows."""
786 return ctypes.Structure.__new__(cls)
788 def __init__(self, hour: int = 0, minute: int = 0, second: int = 0):
790 Initialize Time fields with validation.
791 Do not call super().__init__() for Windows compatibility.
795 minute: Minute (0-59)
796 second: Second (0-59)
799 if not isinstance(hour, int):
800 raise ValueError(f
"Time.hour must be an integer, got {type(hour).__name__}: {hour}")
801 if not isinstance(minute, int):
802 raise ValueError(f
"Time.minute must be an integer, got {type(minute).__name__}: {minute}")
803 if not isinstance(second, int):
804 raise ValueError(f
"Time.second must be an integer, got {type(second).__name__}: {second}")
806 if hour < 0
or hour > 23:
807 raise ValueError(f
"Time.hour must be between 0 and 23, got: {hour}")
808 if minute < 0
or minute > 59:
809 raise ValueError(f
"Time.minute must be between 0 and 59, got: {minute}")
810 if second < 0
or second > 59:
811 raise ValueError(f
"Time.second must be between 0 and 59, got: {second}")
818 def from_list(self, input_list: List[int]):
819 """Initialize from a list [hour, minute, second]"""
820 if len(input_list) < 3:
821 raise ValueError(
"Time.from_list requires a list with at least 3 elements [hour, minute, second]")
822 self.hour = input_list[0]
823 self.minute = input_list[1]
824 self.second = input_list[2]
826 def to_list(self) -> List[int]:
827 """Convert to list [hour, minute, second]"""
828 return [self.hour, self.minute, self.second]
830 def __eq__(self, other) -> bool:
831 """Check equality with another Time object"""
832 if not isinstance(other, Time):
834 return (self.hour == other.hour
and
835 self.minute == other.minute
and
836 self.second == other.second)
838 def __ne__(self, other) -> bool:
839 """Check inequality with another Time object"""
840 return not self.__eq__(other)
843class Date(ctypes.Structure):
844 """Helios Date structure for representing date values."""
845 _fields_ = [(
'day', ctypes.c_int32), (
'month', ctypes.c_int32), (
'year', ctypes.c_int32)]
848 return f
'Date({self.year}-{self.month:02d}-{self.day:02d})'
851 return f
'{self.year}-{self.month:02d}-{self.day:02d}'
853 def __new__(cls, year=None, month=None, day=None):
854 """Create instance - only pass cls to prevent TypeError on Windows."""
855 return ctypes.Structure.__new__(cls)
857 def __init__(self, year: int = 2023, month: int = 1, day: int = 1):
859 Initialize Date fields with validation.
860 Do not call super().__init__() for Windows compatibility.
863 year: Year (1900-3000)
868 if not isinstance(year, int):
869 raise ValueError(f
"Date.year must be an integer, got {type(year).__name__}: {year}")
870 if not isinstance(month, int):
871 raise ValueError(f
"Date.month must be an integer, got {type(month).__name__}: {month}")
872 if not isinstance(day, int):
873 raise ValueError(f
"Date.day must be an integer, got {type(day).__name__}: {day}")
875 if year < 1900
or year > 3000:
876 raise ValueError(f
"Date.year must be between 1900 and 3000, got: {year}")
877 if month < 1
or month > 12:
878 raise ValueError(f
"Date.month must be between 1 and 12, got: {month}")
879 if day < 1
or day > 31:
880 raise ValueError(f
"Date.day must be between 1 and 31, got: {day}")
887 def from_list(self, input_list: List[int]):
888 """Initialize from a list [year, month, day]"""
889 if len(input_list) < 3:
890 raise ValueError(
"Date.from_list requires a list with at least 3 elements [year, month, day]")
891 self.year = input_list[0]
892 self.month = input_list[1]
893 self.day = input_list[2]
895 def to_list(self) -> List[int]:
896 """Convert to list [year, month, day]"""
897 return [self.year, self.month, self.day]
899 def JulianDay(self) -> int:
900 """Calculate Julian day number for this date."""
901 a = (14 - self.month) // 12
902 y = self.year + 4800 - a
903 m = self.month + 12 * a - 3
904 return self.day + (153 * m + 2) // 5 + 365 * y + y // 4 - y // 100 + y // 400 - 32045
906 def incrementDay(self) -> 'Date':
907 """Return a new Date object incremented by one day."""
909 days_in_month = calendar.monthrange(self.year, self.month)[1]
911 new_day = self.day + 1
912 new_month = self.month
915 if new_day > days_in_month:
922 return Date(new_year, new_month, new_day)
925 """Check if this date's year is a leap year."""
926 return (self.
year % 4 == 0
and self.
year % 100 != 0)
or (self.
year % 400 == 0)
928 def __eq__(self, other) -> bool:
929 """Check equality with another Date object"""
930 if not isinstance(other, Date):
932 return (self.
year == other.year
and
933 self.
month == other.month
and
934 self.
day == other.day)
936 def __ne__(self, other) -> bool:
937 """Check inequality with another Date object"""
941def make_Time(hour: int, minute: int, second: int) -> Time:
942 """Make a Time from hour, minute, second"""
943 return Time(hour, minute, second)
945def make_Date(year: int, month: int, day: int) -> Date:
946 """Make a Date from year, month, day"""
947 return Date(year, month, day)
951 """Geographic location for solar position and radiation calculations.
953 Mirrors helios::Location: latitude in degrees (+N / -S), longitude in degrees
954 (+W / -E per Helios convention), UTC offset in hours (+moving West), and
955 altitude of the local Cartesian origin in meters above sea level (default 0).
957 Fields are validated against the same ranges as helios::Location::validate():
959 | Field | Range | Note |
960 | ----- | ----- | ---- |
961 | latitude | -90 to 90 | |
962 | longitude | -180 to 180 | Helios counts longitude positive moving West |
963 | utc_offset | -14 to 12 | Asymmetric; see below |
964 | altitude | any finite value | No non-arbitrary bound exists for a scene |
966 The UTC offset spans -14 to +12 rather than -12 to +12 because Helios counts the
967 offset positive moving West: the real-world span of UTC-12 through UTC+14
968 (Kiribati keeps the latter) inverts to +12 through -14.
970 __slots__ = (
"latitude",
"longitude",
"utc_offset",
"altitude")
972 def __init__(self, latitude: float = 38.55, longitude: float = 121.76, utc_offset: float = 8.0, altitude: float = 0.0):
973 if not isinstance(latitude, (int, float)):
974 raise ValueError(f
"latitude must be a number, got {type(latitude).__name__}")
975 if not isinstance(longitude, (int, float)):
976 raise ValueError(f
"longitude must be a number, got {type(longitude).__name__}")
977 if not isinstance(utc_offset, (int, float)):
978 raise ValueError(f
"utc_offset must be a number, got {type(utc_offset).__name__}")
979 if not isinstance(altitude, (int, float)):
980 raise ValueError(f
"altitude must be a number, got {type(altitude).__name__}")
986 latitude, longitude = float(latitude), float(longitude)
987 utc_offset, altitude = float(utc_offset), float(altitude)
989 if not (-90.0 <= latitude <= 90.0):
991 f
"Latitude of {latitude} degrees is out of range (should be -90 to 90)."
993 if not (-180.0 <= longitude <= 180.0):
995 f
"Longitude of {longitude} degrees is out of range (should be -180 to "
996 f
"180). Note that Helios counts longitude positive in the Western "
999 if not (-14.0 <= utc_offset <= 12.0):
1001 f
"UTC offset of {utc_offset} hours is out of range (should be -14 to "
1002 f
"12). Note that Helios counts the UTC offset positive moving West, so "
1003 f
"UTC-8 is an offset of +8."
1005 if not math.isfinite(altitude):
1007 f
"Altitude of {altitude} meters is not a finite value."
1010 object.__setattr__(self,
"latitude", latitude)
1011 object.__setattr__(self,
"longitude", longitude)
1012 object.__setattr__(self,
"utc_offset", utc_offset)
1013 object.__setattr__(self,
"altitude", altitude)
1015 def __setattr__(self, name, value):
1017 raise AttributeError(f
"Location is immutable; cannot reassign '{name}'")
1019 def __repr__(self) -> str:
1020 return (f
"Location(latitude={self.latitude}, longitude={self.longitude}, "
1021 f
"utc_offset={self.utc_offset}, altitude={self.altitude})")
1023 def __eq__(self, other) -> bool:
1024 if not isinstance(other, Location):
1026 return (self.latitude == other.latitude
and
1027 self.longitude == other.longitude
and
1028 self.utc_offset == other.utc_offset
and
1029 self.altitude == other.altitude)
1031 def __ne__(self, other) -> bool:
1032 return not self.__eq__(other)
1034 def __hash__(self) -> int:
1035 return hash((self.latitude, self.longitude, self.utc_offset, self.altitude))
1038def make_Location(latitude: float, longitude: float, utc_offset: float, altitude: float = 0.0) -> Location:
1039 """Make a Location from latitude (deg), longitude (deg), UTC offset (hours), and altitude (m)."""
1040 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 radians.
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.