""" Helix geometry calculations for the Felix Framework. This module implements the core mathematical model for the helical agent path, translating the 3D geometric model from thefelix.md into computational form. Mathematical Foundation: - Parametric helix with exponential radius tapering - Position vector r(t) = (R(t)cos(θ(t)), R(t)sin(θ(t)), Ht) - Parameter t ∈ [0,1] where t=0 is bottom, t=1 is top - Tapering function R(t) = R_bottom * (R_top/R_bottom)^t - Angular function θ(t) = 2πnt where n is number of turns For complete mathematical specification, see: - docs/mathematical_model.md: Formal parametric equations and geometric properties - docs/hypothesis_mathematics.md: Statistical formulations for research hypotheses - thefelix.md: Original OpenSCAD geometric prototype - validate_openscad.py: Numerical validation against OpenSCAD (<1e-12 precision) Implementation validates against OpenSCAD model with mathematical precision. """ import math from typing import Tuple class HelixGeometry: """ Core helix mathematical model for agent positioning. Implements the same parametric equations as the OpenSCAD prototype, allowing validation against the geometric visualization. """ def __init__(self, top_radius: float, bottom_radius: float, height: float, turns: int): """ Initialize helix with geometric parameters. Args: top_radius: Radius at the top of the helix (t=0) bottom_radius: Radius at the bottom of the helix (t=1) height: Total vertical height of the helix turns: Number of complete rotations Raises: ValueError: If parameters are invalid """ self._validate_parameters(top_radius, bottom_radius, height, turns) self.top_radius = top_radius self.bottom_radius = bottom_radius self.height = height self.turns = turns def _validate_parameters(self, top_radius: float, bottom_radius: float, height: float, turns: int) -> None: """Validate helix parameters for mathematical consistency.""" if top_radius <= bottom_radius: raise ValueError("top_radius must be greater than bottom_radius") if height <= 0: raise ValueError("height must be positive") if turns <= 0: raise ValueError("turns must be positive") def get_position(self, t: float) -> Tuple[float, float, float]: """ Calculate 3D position along helix path. Implements the parametric helix equation: r(t) = (R(t)cos(θ(t)), R(t)sin(θ(t)), Ht) Where: - R(t) = R_bottom * (R_top/R_bottom)^t (exponential tapering) - θ(t) = 2πnt (angular progression) - z(t) = Ht (linear height progression) Mathematical reference: docs/mathematical_model.md, Section 1.2 Args: t: Parameter value between 0 (bottom) and 1 (top) Returns: Tuple of (x, y, z) coordinates Raises: ValueError: If t is outside [0,1] range """ if not (0.0 <= t <= 1.0): raise ValueError("t must be between 0 and 1") # Calculate height (linear interpolation: t=0 is top, t=1 is bottom) # Invert so agents start at wide top and descend to narrow bottom z = self.height * (1.0 - t) # Calculate radius at this height (exponential tapering) radius = self.get_radius(z) # Calculate angle (linear progression through turns) # Total rotation: turns * 360 degrees = turns * 2π radians angle_radians = t * self.turns * 2.0 * math.pi # Calculate Cartesian coordinates x = radius * math.cos(angle_radians) y = radius * math.sin(angle_radians) return (x, y, z) def get_position_at_t(self, t: float) -> Tuple[float, float, float]: """ Alias for get_position method to maintain API consistency. Calculate 3D position along helix path at parameter t. Args: t: Parameter value between 0 (bottom) and 1 (top) Returns: Tuple of (x, y, z) coordinates """ return self.get_position(t) def get_radius(self, z: float) -> float: """ Calculate radius at given height using exponential tapering. Implements the tapering function: R(z) = R_bottom * (R_top/R_bottom)^(z/height) This creates exponential tapering that naturally focuses agent density toward the narrow end, supporting Hypothesis H3 (attention focusing). Mathematical reference: docs/mathematical_model.md, Section 2 Hypothesis reference: docs/hypothesis_mathematics.md, Section H3.2 Args: z: Height value (0 = bottom, height = top) Returns: Radius at the specified height """ # Ensure z is within valid range z = max(0.0, min(z, self.height)) # Exponential tapering formula from OpenSCAD radius_ratio = self.top_radius / self.bottom_radius height_fraction = z / self.height radius = self.bottom_radius * pow(radius_ratio, height_fraction) return radius def get_angle_at_t(self, t: float) -> float: """ Calculate rotation angle (in radians) at parameter t. Args: t: Parameter value between 0 and 1 Returns: Angle in radians """ if not (0.0 <= t <= 1.0): raise ValueError("t must be between 0 and 1") return t * self.turns * 2.0 * math.pi def get_tangent_vector(self, t: float) -> Tuple[float, float, float]: """ Calculate tangent vector to helix at parameter t. Useful for agent orientation and movement direction. Args: t: Parameter value between 0 and 1 Returns: Normalized tangent vector (dx/dt, dy/dt, dz/dt) """ if not (0.0 <= t <= 1.0): raise ValueError("t must be between 0 and 1") # Small epsilon for numerical differentiation eps = 1e-8 t1 = max(0.0, t - eps) t2 = min(1.0, t + eps) x1, y1, z1 = self.get_position(t1) x2, y2, z2 = self.get_position(t2) # Calculate direction vector dx = x2 - x1 dy = y2 - y1 dz = z2 - z1 # Normalize length = math.sqrt(dx*dx + dy*dy + dz*dz) if length > 0: dx /= length dy /= length dz /= length return (dx, dy, dz) def approximate_arc_length(self, t_start: float = 0.0, t_end: float = 1.0, segments: int = 1000) -> float: """ Approximate arc length of helix segment using linear interpolation. Args: t_start: Starting parameter value t_end: Ending parameter value segments: Number of segments for approximation Returns: Approximate arc length """ if not (0.0 <= t_start <= t_end <= 1.0): raise ValueError("Invalid t_start or t_end values") if segments < 1: raise ValueError("segments must be positive") total_length = 0.0 dt = (t_end - t_start) / segments prev_x, prev_y, prev_z = self.get_position(t_start) for i in range(1, segments + 1): t = t_start + i * dt x, y, z = self.get_position(t) # Calculate distance from previous point distance = math.sqrt((x - prev_x)**2 + (y - prev_y)**2 + (z - prev_z)**2) total_length += distance prev_x, prev_y, prev_z = x, y, z return total_length def __repr__(self) -> str: """String representation for debugging.""" return (f"HelixGeometry(top_radius={self.top_radius}, " f"bottom_radius={self.bottom_radius}, " f"height={self.height}, turns={self.turns})")