11"""Utility functions for generating an equidistant reference spiral."""
22
3- import hashlib
4- import pathlib
3+ import functools
54
65import numpy as np
76from scipy import integrate , optimize
87
98from graphomotor .core import config
109
11- logger = config .get_logger ()
12-
1310
1411def _arc_length_integrand (t : float , spiral_config : config .SpiralConfig ) -> float :
1512 """Calculate the differential arc length at angle t for an Archimedean spiral.
1613
1714 Args:
1815 t: Angle parameter.
19- spiral_config: Configuration parameters for the spiral .
16+ spiral_config: Spiral configuration .
2017
2118 Returns:
2219 Differential arc length value.
@@ -25,138 +22,50 @@ def _arc_length_integrand(t: float, spiral_config: config.SpiralConfig) -> float
2522 return np .sqrt (r_t ** 2 + spiral_config .growth_rate ** 2 )
2623
2724
28- def _calculate_arc_length (theta : float , spiral_config : config .SpiralConfig ) -> float :
29- """Calculate the arc length of the spiral from start_angle to theta.
25+ def _calculate_arc_length_between (
26+ theta_start : float , theta_end : float , spiral_config : config .SpiralConfig
27+ ) -> float :
28+ """Calculate the arc length of the spiral between two theta values.
3029
3130 Args:
32- theta: The angle in radians.
33- spiral_config: Configuration parameters for the spiral.
31+ theta_start: Starting angle in radians.
32+ theta_end: Ending angle in radians.
33+ spiral_config: Spiral configuration.
3434
3535 Returns:
36- The arc length of the spiral from start_angle to theta .
36+ The arc length of the spiral from theta_start to theta_end .
3737 """
3838 return integrate .quad (
3939 lambda t : _arc_length_integrand (t , spiral_config ),
40- spiral_config . start_angle ,
41- theta ,
40+ theta_start ,
41+ theta_end ,
4242 )[0 ]
4343
4444
45- def _find_theta_for_arc_length (
46- target_arc_length : float , spiral_config : config .SpiralConfig
45+ def _find_theta_for_incremental_arc_length (
46+ target_increment : float ,
47+ current_theta : float ,
48+ spiral_config : config .SpiralConfig ,
4749) -> float :
48- """Find the theta value for a given arc length using numerical root finding .
50+ """Find the theta value for a given incremental arc length from current position .
4951
5052 Args:
51- target_arc_length: Target arc length.
52- spiral_config: Configuration parameters for the spiral.
53+ target_increment: Target arc length increment from current position.
54+ current_theta: Current theta position.
55+ spiral_config: Spiral configuration.
5356
5457 Returns:
55- Angle theta corresponding to the arc length.
58+ Angle theta corresponding to the target cumulative arc length.
5659 """
5760 solution = optimize .root_scalar (
58- lambda theta : _calculate_arc_length (theta , spiral_config ) - target_arc_length ,
59- bracket = [spiral_config .start_angle , spiral_config .end_angle ],
61+ lambda theta : _calculate_arc_length_between (current_theta , theta , spiral_config )
62+ - target_increment ,
63+ bracket = (current_theta , spiral_config .end_angle ),
6064 )
6165 return solution .root
6266
6367
64- def _get_spiral_cache_key (spiral_config : config .SpiralConfig ) -> str :
65- """Generate a cache key based on spiral configuration parameters.
66-
67- Args:
68- spiral_config: Configuration parameters for the spiral.
69-
70- Returns:
71- Hash string representing the configuration.
72- """
73- config_str = (
74- f"{ spiral_config .center_x } _{ spiral_config .center_y } _"
75- f"{ spiral_config .start_radius } _{ spiral_config .growth_rate } _"
76- f"{ spiral_config .start_angle } _{ spiral_config .end_angle } _"
77- f"{ spiral_config .num_points } "
78- )
79- return hashlib .md5 (config_str .encode ()).hexdigest ()
80-
81-
82- def _get_cache_path (spiral_config : config .SpiralConfig ) -> pathlib .Path :
83- """Get the cache file path for a given spiral configuration.
84-
85- Args:
86- spiral_config: Configuration parameters for the spiral.
87-
88- Returns:
89- Path to the cache file.
90- """
91- cache_key = _get_spiral_cache_key (spiral_config )
92- package_cache_dir = pathlib .Path (__file__ ).parent .parent / "cache"
93-
94- try :
95- package_cache_dir .mkdir (parents = True , exist_ok = True )
96- test_file = package_cache_dir / ".write_test"
97- test_file .touch ()
98- test_file .unlink ()
99- except (PermissionError , OSError ):
100- logger .warning (
101- "Package cache directory is not writable. "
102- "Cannot save reference spiral to cache."
103- )
104-
105- return package_cache_dir / f"reference_spiral_{ cache_key } .npy"
106-
107-
108- def _load_reference_spiral (spiral_config : config .SpiralConfig ) -> np .ndarray | None :
109- """Load a pre-computed reference spiral from disk.
110-
111- Args:
112- spiral_config: Configuration parameters for the spiral.
113-
114- Returns:
115- Reference spiral array if found, None otherwise.
116- """
117- cache_path = _get_cache_path (spiral_config )
118-
119- if cache_path .exists ():
120- try :
121- spiral = np .load (cache_path )
122- logger .info (f"Loaded pre-computed reference spiral from { cache_path } " )
123- return spiral
124- except Exception as e :
125- logger .warning (f"Error loading cached spiral from { cache_path } : { e } " )
126- return None
127-
128- return None
129-
130-
131- def _compute_reference_spiral (
132- spiral_config : config .SpiralConfig ,
133- ) -> np .ndarray :
134- """Generate a reference spiral using numerical computation.
135-
136- This is the computation-heavy implementation that performs numerical integration and
137- root finding to create equidistant points along the spiral.
138-
139- Args:
140- spiral_config: Configuration parameters for the spiral.
141-
142- Returns:
143- Array with shape (N, 2) containing Cartesian coordinates of the spiral points.
144- """
145- total_arc_length = _calculate_arc_length (spiral_config .end_angle , spiral_config )
146-
147- arc_length_values = np .linspace (0 , total_arc_length , spiral_config .num_points )
148-
149- theta_values = np .array (
150- [_find_theta_for_arc_length (s , spiral_config ) for s in arc_length_values ]
151- )
152-
153- r_values = spiral_config .start_radius + spiral_config .growth_rate * theta_values
154- x_values = spiral_config .center_x + r_values * np .cos (theta_values )
155- y_values = spiral_config .center_y + r_values * np .sin (theta_values )
156-
157- return np .column_stack ((x_values , y_values ))
158-
159-
68+ @functools .lru_cache (maxsize = 48 )
16069def generate_reference_spiral (spiral_config : config .SpiralConfig ) -> np .ndarray :
16170 """Generate a reference spiral with equidistant points along its arc length.
16271
@@ -183,8 +92,7 @@ def generate_reference_spiral(spiral_config: config.SpiralConfig) -> np.ndarray:
18392 - Cartesian coordinates: x = cx + r·cos(θ), y = cy + r·sin(θ)
18493
18594 Parameters are defined in the SpiralConfig class:
186- - Center coordinates: (cx, cy) = (spiral_config.center_x,
187- spiral_config.center_y)
95+ - Center coordinates: cx, cy = spiral_config.center_x, spiral_config.center_y
18896 - Start radius: a = spiral_config.start_radius
18997 - Growth rate: b = spiral_config.growth_rate
19098 - Total rotation: θ = spiral_config.end_angle - spiral_config.start_angle
@@ -196,17 +104,24 @@ def generate_reference_spiral(spiral_config: config.SpiralConfig) -> np.ndarray:
196104 Returns:
197105 Array with shape (N, 2) containing Cartesian coordinates of the spiral points.
198106 """
199- cached_spiral = _load_reference_spiral ( spiral_config )
200- if cached_spiral is not None :
201- return cached_spiral
107+ total_arc_length = _calculate_arc_length_between (
108+ spiral_config . start_angle , spiral_config . end_angle , spiral_config
109+ )
202110
203- logger .info ("No cached reference spiral found, generating new reference spiral..." )
204- spiral = _compute_reference_spiral (spiral_config )
111+ arc_length_increment = total_arc_length / (spiral_config .num_points - 1 )
205112
206- cache_path = _get_cache_path (spiral_config )
207- cache_path . parent . mkdir ( parents = True , exist_ok = True )
113+ theta_values = np . zeros (spiral_config . num_points )
114+ theta_values [ 0 ] = spiral_config . start_angle
208115
209- logger .info (f"Saving generated reference spiral to cache: { cache_path } " )
210- np .save (cache_path , spiral )
116+ for i in range (1 , spiral_config .num_points ):
117+ theta_values [i ] = _find_theta_for_incremental_arc_length (
118+ arc_length_increment ,
119+ theta_values [i - 1 ],
120+ spiral_config ,
121+ )
211122
212- return spiral
123+ r_values = spiral_config .start_radius + spiral_config .growth_rate * theta_values
124+ x_values = spiral_config .center_x + r_values * np .cos (theta_values )
125+ y_values = spiral_config .center_y + r_values * np .sin (theta_values )
126+
127+ return np .column_stack ((x_values , y_values ))
0 commit comments