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

1#!/usr/bin/env python3 

2 

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 """ 

24 

25 # Import standard modules ... 

26 import math 

27 

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 

37 

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 

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

71 

72 if debug: 

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

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 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 

109 

110 # Update the middle location ... 

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

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

113 

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

124 

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

148 

149 if debug: 

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

151 

152 # Find the new location ... 

153 newMidLon = midLon + conv * math.sin(math.radians(minAng)) # [°] 

154 newMidLat = midLat + conv * math.cos(math.radians(minAng)) # [°] 

155 

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

167 

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 

173 

174 # Update values ... 

175 maxDist = newMaxDist # [°] 

176 midLon = newMidLon # [°] 

177 midLat = newMidLat # [°] 

178 

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 

183 

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}°.") 

186 

187 # ************************************************************************** 

188 

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

190 if pad > 0.0: 

191 # Add padding ... 

192 maxDist += pad # [°] 

193 

194 if debug: 

195 print(f"INFO: Maximum (padded) Euclidean distance is finally {maxDist:.6f}°.") 

196 

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 )