Coverage for pyguymer3/geo/find_middle_of_locsSrc/find_middle_of_locs_geodesicCircle.py: 63%

59 statements  

« prev     ^ index     » next       coverage.py v7.9.2, created at 2025-07-08 18:47 +0000

1#!/usr/bin/env python3 

2 

3# Define function ... 

4def find_middle_of_locs_geodesicCircle( 

5 lons, 

6 lats, 

7 /, 

8 *, 

9 angConv = 0.1, 

10 conv = 1.0e3, 

11 debug = __debug__, 

12 eps = 1.0e-12, 

13 iRefine = 0, 

14 midLat = None, 

15 midLon = None, 

16 nAng = 9, 

17 nIter = 100, 

18 nRefine = 1, 

19 pad = 10.0e3, 

20 useSciPy = False, 

21): 

22 """Find the middle of some locations such that they are encompassed by the 

23 smallest Geodesic circle possible. 

24 """ 

25 

26 # Import special modules ... 

27 try: 

28 import numpy 

29 except: 

30 raise Exception("\"numpy\" is not installed; run \"pip install --user numpy\"") from None 

31 try: 

32 import scipy 

33 except: 

34 raise Exception("\"scipy\" is not installed; run \"pip install --user scipy\"") from None 

35 

36 # Import sub-functions ... 

37 from .find_middle_of_locs_euclideanBox import find_middle_of_locs_euclideanBox 

38 from ..calc_loc_from_loc_and_bearing_and_dist import calc_loc_from_loc_and_bearing_and_dist 

39 from ..find_min_max_dist_bearing import find_min_max_dist_bearing 

40 from ..max_dist import max_dist 

41 from ...consts import RESOLUTION_OF_EARTH 

42 

43 # ************************************************************************** 

44 

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" 

48 

49 # ************************************************************************** 

50 

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 ) # [°], [°] 

60 

61 # Find the maximum Geodesic distance from the middle to any location ... 

62 maxDist = max_dist( 

63 lons, 

64 lats, 

65 midLon, 

66 midLat, 

67 eps = eps, 

68 nIter = nIter, 

69 space = "GeodesicSpace", 

70 ) # [m] 

71 

72 if debug: 

73 print(f"INFO: The initial middle is ({midLon:.6f}°, {midLat:.6f}°) and the initial maximum Geodesic distance is {0.001 * maxDist:,.1f} km.") 

74 

75 # ************************************************************************** 

76 

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 Geodesic distance ... 

82 ans = scipy.optimize.minimize( 

83 lambda x: max_dist( 

84 lons, 

85 lats, 

86 x[0], 

87 x[1], 

88 eps = eps, 

89 nIter = nIter, 

90 space = "GeodesicSpace", 

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 / RESOLUTION_OF_EARTH, 

103 ) 

104 if not ans.success: 

105 print(lons) 

106 print(lats) 

107 print(ans) 

108 raise Exception("failed to converge") from None 

109 

110 # Update the middle location ... 

111 midLon = ans.x[0] # [°] 

112 midLat = ans.x[1] # [°] 

113 

114 # Find the maximum Geodesic distance from the middle to any location ... 

115 maxDist = max_dist( 

116 lons, 

117 lats, 

118 midLon, 

119 midLat, 

120 eps = eps, 

121 nIter = nIter, 

122 space = "GeodesicSpace", 

123 ) # [m] 

124 

125 if debug: 

126 print(f"INFO: The middle is finally ({midLon:.6f}°, {midLat:.6f}°) and the maximum Geodesic distance is finally {0.001 * maxDist:,.1f} km.") 

127 else: 

128 # Loop over iterations ... 

129 for iIter in range(nIter): 

130 # Find the angle towards the minimum maximum Geodesic 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 = eps, 

141 first = True, 

142 iIter = 0, 

143 nAng = nAng, 

144 nIter = nIter, 

145 space = "GeodesicSpace", 

146 startAng = 180.0, 

147 ) # [°] 

148 

149 if debug: 

150 print(f"INFO: #{iIter + 1:,d}/{nIter:,d}: Moving middle {0.001 * conv:,.1f} km towards {minAng:.1f}° ...") 

151 

152 # Find the new location ... 

153 newMidLon, newMidLat, _ = calc_loc_from_loc_and_bearing_and_dist( 

154 midLon, 

155 midLat, 

156 minAng, 

157 conv, 

158 eps = eps, 

159 nIter = nIter, 

160 ) # [°], [°] 

161 

162 # Find the maximum Geodesic distance from the middle to any location ... 

163 newMaxDist = max_dist( 

164 lons, 

165 lats, 

166 newMidLon, 

167 newMidLat, 

168 eps = eps, 

169 nIter = nIter, 

170 space = "GeodesicSpace", 

171 ) # [m] 

172 

173 # Stop iterating if the answer isn't getting any better ... 

174 if newMaxDist > maxDist: 

175 if debug: 

176 print(f"INFO: #{iIter + 1:,d}/{nIter:,d}: The middle is finally ({midLon:.6f}°, {midLat:.6f}°) and the maximum Geodesic distance is finally {0.001 * maxDist:,.1f} km.") 

177 break 

178 

179 # Update values ... 

180 maxDist = newMaxDist # [m] 

181 midLon = newMidLon # [°] 

182 midLat = newMidLat # [°] 

183 

184 # Stop if the end of the loop has been reached but the answer has 

185 # not converged ... 

186 if iIter == nIter - 1: 

187 raise Exception(f"failed to converge; the middle is currently ({midLon:.6f}°, {midLat:.6f}°); nIter = {nIter:,d}") from None 

188 

189 if debug: 

190 print(f"INFO: #{iIter + 1:,d}/{nIter:,d}: The middle is now ({midLon:.6f}°, {midLat:.6f}°) and the maximum Geodesic distance is now {0.001 * maxDist:,.1f} km.") 

191 

192 # ************************************************************************** 

193 

194 # Check if a padding needs to be added ... 

195 if pad > 0.0: 

196 # Add padding ... 

197 maxDist += pad # [m] 

198 

199 if debug: 

200 print(f"INFO: Maximum (padded) Geodesic distance is finally {0.001 * maxDist:,.1f} km.") 

201 

202 # Return answer ... 

203 if iRefine == nRefine - 1: 

204 return midLon, midLat, maxDist 

205 return find_middle_of_locs_geodesicCircle( 

206 lons, 

207 lats, 

208 angConv = angConv, 

209 conv = 0.5 * conv, 

210 debug = debug, 

211 eps = eps, 

212 iRefine = iRefine + 1, 

213 midLat = midLat, 

214 midLon = midLon, 

215 nAng = nAng, 

216 nIter = nIter, 

217 nRefine = nRefine, 

218 pad = pad, 

219 useSciPy = useSciPy, 

220 )