Coverage for pyguymer3/geo/find_middle_of_locsSrc/find_middle_of_locs_euclideanCircle.py: 63%
59 statements
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
« prev ^ index » next coverage.py v7.9.2, created at 2025-07-08 18:47 +0000
1#!/usr/bin/env python3
3# Define function ...
4def find_middle_of_locs_euclideanCircle(
5 lons,
6 lats,
7 /,
8 *,
9 angConv = 0.1,
10 conv = 0.01,
11 debug = __debug__,
12 iRefine = 0,
13 midLat = None,
14 midLon = None,
15 nAng = 9,
16 nIter = 100,
17 nRefine = 1,
18 pad = 0.1,
19 useSciPy = False,
20):
21 """Find the middle of some locations such that they are encompassed by the
22 smallest Euclidean circle possible.
23 """
25 # Import standard modules ...
26 import math
28 # Import special modules ...
29 try:
30 import numpy
31 except:
32 raise Exception("\"numpy\" is not installed; run \"pip install --user numpy\"") from None
33 try:
34 import scipy
35 except:
36 raise Exception("\"scipy\" is not installed; run \"pip install --user scipy\"") from None
38 # Import sub-functions ...
39 from .find_middle_of_locs_euclideanBox import find_middle_of_locs_euclideanBox
40 from ..find_min_max_dist_bearing import find_min_max_dist_bearing
41 from ..max_dist import max_dist
43 # **************************************************************************
45 # Check arguments ...
46 assert isinstance(lons, numpy.ndarray), "\"lons\" is not a NumPy array"
47 assert isinstance(lats, numpy.ndarray), "\"lats\" is not a NumPy array"
49 # **************************************************************************
51 # Calculate the middle of the Euclidean bounding box to use as a decent
52 # starting guess (if the user has not provided one) ...
53 if midLon is None or midLat is None:
54 midLon, midLat, _ = find_middle_of_locs_euclideanBox(
55 lons,
56 lats,
57 debug = debug,
58 pad = -1.0,
59 ) # [°], [°]
61 # Find the maximum Euclidean distance from the middle to any location ...
62 maxDist = max_dist(
63 lons,
64 lats,
65 midLon,
66 midLat,
67 eps = None,
68 nIter = None,
69 space = "EuclideanSpace",
70 ) # [°]
72 if debug:
73 print(f"INFO: The initial middle is ({midLon:.6f}°, {midLat:.6f}°) and the initial maximum Euclidean distance is {maxDist:.6f}°.")
75 # **************************************************************************
77 # Check if the input is already converged or if the user wants to use SciPy ...
78 if maxDist < conv:
79 pass
80 elif useSciPy:
81 # Use SciPy to find the minimum maximum Euclidean distance ...
82 ans = scipy.optimize.minimize(
83 lambda x: max_dist(
84 lons,
85 lats,
86 x[0],
87 x[1],
88 eps = None,
89 nIter = None,
90 space = "EuclideanSpace",
91 ),
92 [midLon, midLat],
93 bounds = [
94 (-180.0, +180.0),
95 ( -90.0, +90.0),
96 ],
97 method = "L-BFGS-B",
98 options = {
99 "disp" : debug,
100 "maxiter" : nIter,
101 },
102 tol = conv,
103 )
104 if not ans.success:
105 print(lons)
106 print(lats)
107 print(ans)
108 raise Exception("failed to converge") from None
110 # Update the middle location ...
111 midLon = ans.x[0] # [°]
112 midLat = ans.x[1] # [°]
114 # Find the maximum Euclidean distance from the middle to any location ...
115 maxDist = max_dist(
116 lons,
117 lats,
118 midLon,
119 midLat,
120 eps = None,
121 nIter = None,
122 space = "EuclideanSpace",
123 ) # [°]
125 if debug:
126 print(f"INFO: The middle is finally ({midLon:.6f}°, {midLat:.6f}°) and the maximum Euclidean distance is finally {maxDist:.6f}°.")
127 else:
128 # Loop over iterations ...
129 for iIter in range(nIter):
130 # Find the angle towards the minimum maximum Euclidean distance ...
131 minAng = find_min_max_dist_bearing(
132 midLon,
133 midLat,
134 lons,
135 lats,
136 angConv = angConv,
137 angHalfRange = 180.0,
138 debug = debug,
139 dist = conv,
140 eps = None,
141 first = True,
142 iIter = 0,
143 nAng = nAng,
144 nIter = nIter,
145 space = "EuclideanSpace",
146 startAng = 180.0,
147 ) # [°]
149 if debug:
150 print(f"INFO: #{iIter + 1:,d}/{nIter:,d}: Moving middle {conv:.6f}° towards {minAng:.1f}° ...")
152 # Find the new location ...
153 newMidLon = midLon + conv * math.sin(math.radians(minAng)) # [°]
154 newMidLat = midLat + conv * math.cos(math.radians(minAng)) # [°]
156 # Find the maximum Euclidean distance from the middle to any
157 # location ...
158 newMaxDist = max_dist(
159 lons,
160 lats,
161 newMidLon,
162 newMidLat,
163 eps = None,
164 nIter = None,
165 space = "EuclideanSpace",
166 ) # [°]
168 # Stop iterating if the answer isn't getting any better ...
169 if newMaxDist > maxDist:
170 if debug:
171 print(f"INFO: #{iIter + 1:,d}/{nIter:,d}: The middle is finally ({midLon:.6f}°, {midLat:.6f}°) and the maximum Euclidean distance is finally {maxDist:.6f}°.")
172 break
174 # Update values ...
175 maxDist = newMaxDist # [°]
176 midLon = newMidLon # [°]
177 midLat = newMidLat # [°]
179 # Stop if the end of the loop has been reached but the answer has
180 # not converged ...
181 if iIter == nIter - 1:
182 raise Exception(f"failed to converge; the middle is currently ({midLon:.6f}°, {midLat:.6f}°); nIter = {nIter:,d}") from None
184 if debug:
185 print(f"INFO: #{iIter + 1:,d}/{nIter:,d}: The middle is now ({midLon:.6f}°, {midLat:.6f}°) and the maximum Euclidean distance is now {maxDist:.6f}°.")
187 # **************************************************************************
189 # Check if a padding needs to be added ...
190 if pad > 0.0:
191 # Add padding ...
192 maxDist += pad # [°]
194 if debug:
195 print(f"INFO: Maximum (padded) Euclidean distance is finally {maxDist:.6f}°.")
197 # Return answer ...
198 if iRefine == nRefine - 1:
199 return midLon, midLat, maxDist
200 return find_middle_of_locs_euclideanCircle(
201 lons,
202 lats,
203 angConv = angConv,
204 conv = 0.5 * conv,
205 debug = debug,
206 iRefine = iRefine + 1,
207 midLat = midLat,
208 midLon = midLon,
209 nAng = nAng,
210 nIter = nIter,
211 nRefine = nRefine,
212 pad = pad,
213 useSciPy = useSciPy,
214 )