xslope 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xslope/__init__.py +1 -0
- xslope/_version.py +4 -0
- xslope/advanced.py +460 -0
- xslope/fem.py +2753 -0
- xslope/fileio.py +671 -0
- xslope/global_config.py +59 -0
- xslope/mesh.py +2719 -0
- xslope/plot.py +1484 -0
- xslope/plot_fem.py +1658 -0
- xslope/plot_seep.py +634 -0
- xslope/search.py +416 -0
- xslope/seep.py +2080 -0
- xslope/slice.py +1075 -0
- xslope/solve.py +1259 -0
- xslope-0.1.2.dist-info/LICENSE +196 -0
- xslope-0.1.2.dist-info/METADATA +56 -0
- xslope-0.1.2.dist-info/NOTICE +14 -0
- xslope-0.1.2.dist-info/RECORD +20 -0
- xslope-0.1.2.dist-info/WHEEL +5 -0
- xslope-0.1.2.dist-info/top_level.txt +1 -0
xslope/search.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
# Copyright 2025 Norman L. Jones
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
import time
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
from shapely.geometry import LineString, Point
|
|
19
|
+
|
|
20
|
+
from . import solve
|
|
21
|
+
from .advanced import rapid_drawdown
|
|
22
|
+
from .slice import generate_slices, get_y_from_intersection
|
|
23
|
+
|
|
24
|
+
def circular_search(slope_data, method_name, rapid=False, tol=1e-2, max_iter=50, shrink_factor=0.5,
|
|
25
|
+
fs_fail=9999, depth_tol_frac=0.03, diagnostic=False):
|
|
26
|
+
"""
|
|
27
|
+
Global 9-point circular search with adaptive grid refinement.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
list of dict: sorted fs_cache by FS
|
|
31
|
+
bool: convergence flag
|
|
32
|
+
list of dict: search path
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
solver = getattr(solve, method_name)
|
|
36
|
+
|
|
37
|
+
start_time = time.time() # Start timing
|
|
38
|
+
|
|
39
|
+
ground_surface = slope_data['ground_surface']
|
|
40
|
+
ground_surface = LineString([(x, y) for x, y in ground_surface.coords])
|
|
41
|
+
y_max = max(y for _, y in ground_surface.coords)
|
|
42
|
+
y_min = slope_data['max_depth']
|
|
43
|
+
delta_y = y_max - y_min
|
|
44
|
+
tol = delta_y * depth_tol_frac
|
|
45
|
+
|
|
46
|
+
circles = slope_data['circles']
|
|
47
|
+
max_depth = slope_data['max_depth']
|
|
48
|
+
|
|
49
|
+
def optimize_depth(x, y, depth_guess, depth_step_init, depth_shrink_factor, tol_frac, fs_fail, diagnostic=False):
|
|
50
|
+
depth_step = min(10.0, depth_step_init)
|
|
51
|
+
best_depth = max(depth_guess, max_depth)
|
|
52
|
+
best_fs = fs_fail
|
|
53
|
+
best_result = None
|
|
54
|
+
depth_tol = depth_step * tol_frac
|
|
55
|
+
iterations = 0
|
|
56
|
+
|
|
57
|
+
while depth_step > depth_tol:
|
|
58
|
+
depths = [
|
|
59
|
+
max(best_depth - depth_step, max_depth),
|
|
60
|
+
best_depth,
|
|
61
|
+
best_depth + depth_step
|
|
62
|
+
]
|
|
63
|
+
fs_results = []
|
|
64
|
+
for d in depths:
|
|
65
|
+
test_circle = {'Xo': x, 'Yo': y, 'Depth': d, 'R': y - d}
|
|
66
|
+
success, result = generate_slices(slope_data, circle=test_circle)
|
|
67
|
+
if not success:
|
|
68
|
+
FS = fs_fail
|
|
69
|
+
df_slices = None
|
|
70
|
+
failure_surface = None
|
|
71
|
+
solver_result = None
|
|
72
|
+
else:
|
|
73
|
+
df_slices, failure_surface = result
|
|
74
|
+
if rapid:
|
|
75
|
+
solver_success, solver_result = rapid_drawdown(df_slices, method_name)
|
|
76
|
+
else:
|
|
77
|
+
solver_success, solver_result = solver(df_slices)
|
|
78
|
+
FS = solver_result['FS'] if solver_success else fs_fail
|
|
79
|
+
fs_results.append((FS, d, df_slices, failure_surface, solver_result))
|
|
80
|
+
|
|
81
|
+
fs_results.sort(key=lambda t: t[0])
|
|
82
|
+
best_fs, best_depth, best_df, best_surface, best_solver_result = fs_results[0]
|
|
83
|
+
|
|
84
|
+
if all(FS == fs_fail for FS, *_ in fs_results):
|
|
85
|
+
if diagnostic:
|
|
86
|
+
print(f"[❌ all fail] x={x:.2f}, y={y:.2f}")
|
|
87
|
+
return best_depth, fs_fail, None, None, None
|
|
88
|
+
|
|
89
|
+
if diagnostic:
|
|
90
|
+
print(f"[✓ depth-opt] x={x:.2f}, y={y:.2f}, depth={best_depth:.2f}, FS={best_fs:.4f}, step={depth_step:.2f}")
|
|
91
|
+
|
|
92
|
+
depth_step *= depth_shrink_factor
|
|
93
|
+
iterations += 1
|
|
94
|
+
if iterations > 50:
|
|
95
|
+
if diagnostic:
|
|
96
|
+
print(f"[⚠️ warning] depth iterations exceeded at (x={x:.2f}, y={y:.2f})")
|
|
97
|
+
break
|
|
98
|
+
|
|
99
|
+
return best_depth, best_fs, best_df, best_surface, best_solver_result
|
|
100
|
+
|
|
101
|
+
def evaluate_grid(x0, y0, grid_size, depth_guess, slope_data, diagnostic=False, fs_cache=None):
|
|
102
|
+
if fs_cache is None:
|
|
103
|
+
fs_cache = {}
|
|
104
|
+
|
|
105
|
+
Xs = [x0 - grid_size, x0, x0 + grid_size]
|
|
106
|
+
Ys = [y0 - grid_size, y0, y0 + grid_size]
|
|
107
|
+
points = [(x, y) for y in Ys for x in Xs]
|
|
108
|
+
|
|
109
|
+
for i, (x, y) in enumerate(points):
|
|
110
|
+
if (x, y) in fs_cache:
|
|
111
|
+
result = fs_cache[(x, y)]
|
|
112
|
+
if diagnostic:
|
|
113
|
+
print(f"[cache hit] grid pt {i + 1}/9 at (x={x:.2f}, y={y:.2f}) → FS={result['FS']:.4f}")
|
|
114
|
+
continue
|
|
115
|
+
|
|
116
|
+
depth_step_init = grid_size * 0.75
|
|
117
|
+
d, FS, df_slices, failure_surface, solver_result = optimize_depth(
|
|
118
|
+
x, y, depth_guess, depth_step_init, depth_shrink_factor=0.25, tol_frac=0.01, fs_fail=fs_fail,
|
|
119
|
+
diagnostic=diagnostic
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
fs_cache[(x, y)] = {
|
|
123
|
+
"Xo": x,
|
|
124
|
+
"Yo": y,
|
|
125
|
+
"Depth": d,
|
|
126
|
+
"FS": FS,
|
|
127
|
+
"slices": df_slices,
|
|
128
|
+
"failure_surface": failure_surface,
|
|
129
|
+
"solver_result": solver_result
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if diagnostic:
|
|
133
|
+
print(f"[grid pt {i + 1}/9] x={x:.2f}, y={y:.2f} → FS={FS:.4f} at d={d:.2f}")
|
|
134
|
+
|
|
135
|
+
sorted_fs = sorted(fs_cache.items(), key=lambda item: item[1]['FS'])
|
|
136
|
+
best_point = sorted_fs[0][1]
|
|
137
|
+
best_index = list(fs_cache.keys()).index((best_point['Xo'], best_point['Yo']))
|
|
138
|
+
|
|
139
|
+
if diagnostic:
|
|
140
|
+
print(f"[★ grid best {best_index + 1}/9] FS={best_point['FS']:.4f} at (x={best_point['Xo']:.2f}, y={best_point['Yo']:.2f})")
|
|
141
|
+
|
|
142
|
+
return fs_cache, best_point
|
|
143
|
+
|
|
144
|
+
# === Step 1: Evaluate starting circles ===
|
|
145
|
+
all_starts = []
|
|
146
|
+
for i, start_circle in enumerate(circles):
|
|
147
|
+
x0 = start_circle['Xo']
|
|
148
|
+
y0 = start_circle['Yo']
|
|
149
|
+
r0 = y0 - start_circle['Depth']
|
|
150
|
+
if diagnostic:
|
|
151
|
+
print(f"\n[⏱ starting circle {i+1}] x={x0:.2f}, y={y0:.2f}, r={r0:.2f}")
|
|
152
|
+
grid_size = r0 * 0.15
|
|
153
|
+
depth_guess = start_circle['Depth']
|
|
154
|
+
fs_cache, best_point = evaluate_grid(x0, y0, grid_size, depth_guess, slope_data, diagnostic=diagnostic)
|
|
155
|
+
all_starts.append((start_circle, best_point, fs_cache))
|
|
156
|
+
|
|
157
|
+
all_starts.sort(key=lambda t: t[1]['FS'])
|
|
158
|
+
start_circle, best_start, fs_cache = all_starts[0]
|
|
159
|
+
x0 = best_start['Xo']
|
|
160
|
+
y0 = best_start['Yo']
|
|
161
|
+
depth_guess = best_start['Depth']
|
|
162
|
+
grid_size = (y0 - depth_guess) * 0.15
|
|
163
|
+
best_fs = best_start['FS']
|
|
164
|
+
|
|
165
|
+
# Include initial jump from user-defined circle to best point on its grid
|
|
166
|
+
search_path = [
|
|
167
|
+
{"x": start_circle['Xo'], "y": start_circle['Yo'], "FS": None},
|
|
168
|
+
{"x": x0, "y": y0, "FS": best_fs}
|
|
169
|
+
]
|
|
170
|
+
converged = False
|
|
171
|
+
|
|
172
|
+
if diagnostic:
|
|
173
|
+
print(f"\n[✅ launch grid] Starting refinement from FS={best_fs:.4f} at ({x0:.2f}, {y0:.2f})")
|
|
174
|
+
|
|
175
|
+
for iteration in range(max_iter):
|
|
176
|
+
print(f"[🔁 iteration {iteration+1}] center=({x0:.2f}, {y0:.2f}), FS={best_fs:.4f}, grid={grid_size:.4f}")
|
|
177
|
+
fs_cache, best_point = evaluate_grid(x0, y0, grid_size, depth_guess, slope_data, diagnostic=diagnostic, fs_cache=fs_cache)
|
|
178
|
+
|
|
179
|
+
if best_point['FS'] < best_fs:
|
|
180
|
+
best_fs = best_point['FS']
|
|
181
|
+
x0 = best_point['Xo']
|
|
182
|
+
y0 = best_point['Yo']
|
|
183
|
+
depth_guess = best_point['Depth']
|
|
184
|
+
search_path.append({"x": x0, "y": y0, "FS": best_fs})
|
|
185
|
+
else:
|
|
186
|
+
grid_size *= shrink_factor
|
|
187
|
+
|
|
188
|
+
if grid_size < tol:
|
|
189
|
+
converged = True
|
|
190
|
+
end_time = time.time()
|
|
191
|
+
elapsed = end_time - start_time
|
|
192
|
+
print(f"[✅ converged] Iter={iteration+1}, FS={best_fs:.4f} at (x={x0:.2f}, y={y0:.2f}, depth={depth_guess:.2f}), elapsed time={elapsed:.2f} seconds")
|
|
193
|
+
break
|
|
194
|
+
|
|
195
|
+
if not converged and diagnostic:
|
|
196
|
+
print(f"\n[❌ max iterations reached] FS={best_fs:.4f} at (x={x0:.2f}, y={y0:.2f})")
|
|
197
|
+
|
|
198
|
+
sorted_fs_cache = sorted(fs_cache.values(), key=lambda d: d['FS'])
|
|
199
|
+
return sorted_fs_cache, converged, search_path
|
|
200
|
+
|
|
201
|
+
def noncircular_search(slope_data, method_name, rapid=False, diagnostic=True, movement_distance=4.0, shrink_factor=0.8, fs_tol=0.001, max_iter=100, move_tol=0.1):
|
|
202
|
+
"""
|
|
203
|
+
Non-circular search using the specified solver.
|
|
204
|
+
|
|
205
|
+
Parameters:
|
|
206
|
+
-----------
|
|
207
|
+
data : dict
|
|
208
|
+
Input data dictionary containing all necessary parameters
|
|
209
|
+
method_name : str
|
|
210
|
+
The method name to use (e.g., 'lowe_karafiath', 'spencer')
|
|
211
|
+
diagnostic : bool
|
|
212
|
+
If True, print diagnostic information during search
|
|
213
|
+
movement_distance : float
|
|
214
|
+
Initial distance to move points in each iteration
|
|
215
|
+
shrink_factor : float
|
|
216
|
+
Factor to reduce movement_distance by when no improvement is found
|
|
217
|
+
fs_tol : float
|
|
218
|
+
Factor of safety convergence tolerance
|
|
219
|
+
max_iter : int
|
|
220
|
+
Maximum number of iterations
|
|
221
|
+
move_tol : float
|
|
222
|
+
Minimum movement distance for convergence (AND logic with fs_tol)
|
|
223
|
+
|
|
224
|
+
Returns:
|
|
225
|
+
--------
|
|
226
|
+
tuple : (fs_cache, converged, search_path)
|
|
227
|
+
fs_cache : dict of all evaluated surfaces and their FS values
|
|
228
|
+
converged : bool indicating if search converged
|
|
229
|
+
search_path : list of surfaces evaluated during search
|
|
230
|
+
"""
|
|
231
|
+
# Get the solver function from solve module
|
|
232
|
+
solver = getattr(solve, method_name)
|
|
233
|
+
def move_point(points, i, dx, dy, movement_type, ground_surface, max_depth):
|
|
234
|
+
"""Move a point while respecting constraints"""
|
|
235
|
+
# Get current point
|
|
236
|
+
point = points[i]
|
|
237
|
+
|
|
238
|
+
# Calculate new position
|
|
239
|
+
new_x = point[0] + dx
|
|
240
|
+
new_y = point[1] + dy
|
|
241
|
+
|
|
242
|
+
# For endpoints, ensure they stay on ground surface
|
|
243
|
+
if i == 0 or i == len(points)-1:
|
|
244
|
+
# Create vertical line at new_x
|
|
245
|
+
vertical_line = LineString([(new_x, 0), (new_x, 1000)]) # Arbitrary high y value
|
|
246
|
+
intersection = ground_surface.intersection(vertical_line)
|
|
247
|
+
y = get_y_from_intersection(intersection)
|
|
248
|
+
if y is None:
|
|
249
|
+
return False
|
|
250
|
+
new_y = y
|
|
251
|
+
else:
|
|
252
|
+
# For middle points, ensure they stay below ground surface but above max_depth
|
|
253
|
+
if new_y > ground_surface.interpolate(ground_surface.project(Point(new_x, new_y))).y:
|
|
254
|
+
return False
|
|
255
|
+
if new_y < max_depth:
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
# Check x-ordering constraints
|
|
259
|
+
if i > 0 and new_x <= points[i-1][0]: # Don't move past left neighbor
|
|
260
|
+
return False
|
|
261
|
+
if i < len(points)-1 and new_x >= points[i+1][0]: # Don't move past right neighbor
|
|
262
|
+
return False
|
|
263
|
+
|
|
264
|
+
# Update point
|
|
265
|
+
points[i] = [new_x, new_y]
|
|
266
|
+
return True
|
|
267
|
+
|
|
268
|
+
def evaluate_surface(points, distance, fs_cache=None):
|
|
269
|
+
"""Evaluate factor of safety for current surface configuration"""
|
|
270
|
+
if fs_cache is None:
|
|
271
|
+
fs_cache = {}
|
|
272
|
+
|
|
273
|
+
# Create non_circ format from points
|
|
274
|
+
non_circ = [{'X': x, 'Y': y, 'Movement': movements[i]} for i, (x, y) in enumerate(points)]
|
|
275
|
+
|
|
276
|
+
# Generate slices and compute FS
|
|
277
|
+
success, result = generate_slices(slope_data, non_circ=non_circ)
|
|
278
|
+
if not success:
|
|
279
|
+
return float('inf'), None, None, None, fs_cache
|
|
280
|
+
|
|
281
|
+
df_slices, failure_surface = result
|
|
282
|
+
if rapid:
|
|
283
|
+
solver_success, solver_result = rapid_drawdown(df_slices, method_name)
|
|
284
|
+
else:
|
|
285
|
+
solver_success, solver_result = solver(df_slices)
|
|
286
|
+
FS = solver_result['FS'] if solver_success else float('inf')
|
|
287
|
+
|
|
288
|
+
# Cache result
|
|
289
|
+
key = tuple(map(tuple, points))
|
|
290
|
+
fs_cache[key] = {
|
|
291
|
+
'points': points.copy(),
|
|
292
|
+
'FS': FS,
|
|
293
|
+
'slices': df_slices,
|
|
294
|
+
'failure_surface': failure_surface,
|
|
295
|
+
'solver_result': solver_result
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return FS, df_slices, failure_surface, solver_result, fs_cache
|
|
299
|
+
|
|
300
|
+
# Get initial surface from non_circ data
|
|
301
|
+
non_circ = slope_data['non_circ']
|
|
302
|
+
points = np.array([[p['X'], p['Y']] for p in non_circ])
|
|
303
|
+
movements = [p['Movement'] for p in non_circ]
|
|
304
|
+
ground_surface = slope_data['ground_surface']
|
|
305
|
+
|
|
306
|
+
# Initialize cache and search path
|
|
307
|
+
fs_cache = {}
|
|
308
|
+
search_path = []
|
|
309
|
+
|
|
310
|
+
# Evaluate initial surface
|
|
311
|
+
FS, df_slices, failure_surface, solver_result, fs_cache = evaluate_surface(
|
|
312
|
+
points, movement_distance, fs_cache)
|
|
313
|
+
|
|
314
|
+
# Initialize best surface with initial evaluation
|
|
315
|
+
best_points = points.copy()
|
|
316
|
+
best_fs = FS
|
|
317
|
+
best_df = df_slices
|
|
318
|
+
best_surface = failure_surface
|
|
319
|
+
best_solver_result = solver_result
|
|
320
|
+
|
|
321
|
+
# Track convergence
|
|
322
|
+
converged = False
|
|
323
|
+
start_time = time.time()
|
|
324
|
+
prev_fs = best_fs
|
|
325
|
+
|
|
326
|
+
if diagnostic:
|
|
327
|
+
print(f"\n[✅ starting search] Initial FS={best_fs:.4f}\n")
|
|
328
|
+
print("Initial failure surface:")
|
|
329
|
+
for i, point in enumerate(points):
|
|
330
|
+
print(f"Point {i}: ({point[0]:.2f}, {point[1]:.2f})")
|
|
331
|
+
print("\nGround surface:")
|
|
332
|
+
for i, point in enumerate(ground_surface.coords):
|
|
333
|
+
print(f"Point {i}: ({point[0]:.2f}, {point[1]:.2f})")
|
|
334
|
+
|
|
335
|
+
# Main search loop
|
|
336
|
+
for iteration in range(max_iter):
|
|
337
|
+
improved = False
|
|
338
|
+
|
|
339
|
+
if diagnostic:
|
|
340
|
+
print(f"\nIteration {iteration + 1}")
|
|
341
|
+
print("Current surface points:")
|
|
342
|
+
for i, point in enumerate(best_surface.coords):
|
|
343
|
+
print(f"Point {i}: ({point[0]:.2f}, {point[1]:.2f})")
|
|
344
|
+
|
|
345
|
+
# Try moving each point
|
|
346
|
+
for i in range(len(points)):
|
|
347
|
+
# Try both positive and negative directions
|
|
348
|
+
for direction in [-1, 1]:
|
|
349
|
+
test_points = points.copy()
|
|
350
|
+
|
|
351
|
+
# Get movement direction based on point type
|
|
352
|
+
if i == 0 or i == len(points)-1: # End points
|
|
353
|
+
dx = direction * movement_distance
|
|
354
|
+
dy = 0 # y will be determined by ground surface
|
|
355
|
+
elif movements[i] == 'Horiz':
|
|
356
|
+
dx = direction * movement_distance
|
|
357
|
+
dy = 0
|
|
358
|
+
elif movements[i] == 'Free':
|
|
359
|
+
# For free points, move perpendicular to tangent
|
|
360
|
+
dx_tangent = points[i+1][0] - points[i-1][0] if i > 0 and i < len(points)-1 else 1
|
|
361
|
+
dy_tangent = points[i+1][1] - points[i-1][1] if i > 0 and i < len(points)-1 else 0
|
|
362
|
+
length = np.sqrt(dx_tangent**2 + dy_tangent**2)
|
|
363
|
+
if length > 0:
|
|
364
|
+
dx = -dy_tangent/length * direction * movement_distance
|
|
365
|
+
dy = dx_tangent/length * direction * movement_distance
|
|
366
|
+
else:
|
|
367
|
+
dx = direction * movement_distance
|
|
368
|
+
dy = 0
|
|
369
|
+
else: # Fixed
|
|
370
|
+
continue
|
|
371
|
+
|
|
372
|
+
# Try to move the point
|
|
373
|
+
if move_point(test_points, i, dx, dy, movements[i], ground_surface, slope_data['max_depth']):
|
|
374
|
+
# Evaluate new surface
|
|
375
|
+
FS, df_slices, failure_surface, solver_result, fs_cache = evaluate_surface(
|
|
376
|
+
test_points, movement_distance, fs_cache)
|
|
377
|
+
|
|
378
|
+
if FS < best_fs:
|
|
379
|
+
best_fs = FS
|
|
380
|
+
best_points = test_points.copy()
|
|
381
|
+
best_df = df_slices
|
|
382
|
+
best_surface = failure_surface
|
|
383
|
+
best_solver_result = solver_result
|
|
384
|
+
improved = True
|
|
385
|
+
if diagnostic:
|
|
386
|
+
print(f"[✓ improved] iter={iteration}, point={i}, FS={FS:.4f}")
|
|
387
|
+
|
|
388
|
+
# print iteration results
|
|
389
|
+
print(f"iteration {iteration+1} FS={best_fs:.4f}")
|
|
390
|
+
|
|
391
|
+
# Check convergence based on FS change and movement distance (AND logic)
|
|
392
|
+
fs_change = abs(best_fs - prev_fs)
|
|
393
|
+
if fs_change < fs_tol and movement_distance < move_tol:
|
|
394
|
+
converged = True
|
|
395
|
+
if diagnostic:
|
|
396
|
+
print(f"[✓ converged] FS change {fs_change:.6f} < tolerance {fs_tol} and movement_distance {movement_distance:.4f} < move_tol {move_tol}")
|
|
397
|
+
break
|
|
398
|
+
prev_fs = best_fs
|
|
399
|
+
|
|
400
|
+
if not improved or fs_change < fs_tol:
|
|
401
|
+
movement_distance *= shrink_factor
|
|
402
|
+
if True:
|
|
403
|
+
print(f"[↘️ shrinking] movement_distance={movement_distance:.4f}")
|
|
404
|
+
|
|
405
|
+
points = best_points.copy()
|
|
406
|
+
|
|
407
|
+
end_time = time.time()
|
|
408
|
+
elapsed = end_time - start_time
|
|
409
|
+
|
|
410
|
+
if converged:
|
|
411
|
+
print(f"\n[✅ converged] Iter={iteration+1}, FS={best_fs:.4f}, elapsed time={elapsed:.2f} seconds")
|
|
412
|
+
else:
|
|
413
|
+
print(f"\n[❌ max iterations reached] FS={best_fs:.4f}, elapsed time={elapsed:.2f} seconds")
|
|
414
|
+
|
|
415
|
+
sorted_fs_cache = sorted(fs_cache.values(), key=lambda d: d['FS'])
|
|
416
|
+
return sorted_fs_cache, converged, search_path
|