@@ -117,7 +117,22 @@ def smooth_data(data, method='savitzky-golay', window_length=31, poly_order=3):
117
117
raise NotImplementedError ('Smooth method not implemented, please check keyword argument' )
118
118
119
119
120
- def find_integration_limits (r , rdf , rho = None , peak_search_limit = 10.0 , search_method = 'first' ):
120
+ def clean_base_indices (lefts , rights ):
121
+ """
122
+ Clean peak base limits
123
+ See https://github.com/scipy/scipy/issues/19232
124
+ """
125
+ _lefts = np .copy (lefts )
126
+ _rights = np .copy (rights )
127
+ for i in range (len (lefts )- 1 ):
128
+ if lefts [i ] == lefts [i + 1 ]:
129
+ _lefts [i + 1 ] = rights [i ]
130
+ if rights [i ] == rights [i + 1 ]:
131
+ _rights [i ] = lefts [i + 1 ]
132
+ return _lefts , _rights
133
+
134
+
135
+ def find_integration_limits (r , rdf , rho = None , peak_search_limit = 10.0 , search_method = None ):
121
136
'''
122
137
Computes the integration limits used in the calculation of the
123
138
first coordination number for a monatomic system.
@@ -173,48 +188,122 @@ def find_integration_limits(r, rdf, rho=None, peak_search_limit=10.0, search_met
173
188
# avoid the extra import may be less expensive
174
189
peak_list , _ = scipy .signal .find_peaks (rdf [r < peak_search_limit ])
175
190
176
- # Select 1st peak after r0 as 1st coordination sphere
191
+ # Get index of first peak after r0 (last RDF = 0)
177
192
peak_idx_first = np .argmax (peak_list > brent_a )
178
193
179
- # Select most prominent peak as 1st coordination sphere and get positions
180
- # at the base either side
194
+ # Get peak prominences and positions at the bases either side
181
195
peak_prom , left_bases , right_bases = scipy .signal .peak_prominences (rdf , peak_list )
196
+ # Check if left bases merge peaks and fix if necessary
197
+ if len (np .unique (left_bases )) < len (left_bases ):
198
+ left_bases , right_bases = clean_base_indices (left_bases , right_bases )
199
+ # Get index of most prominent peak
182
200
peak_idx_prom = np .argmax (peak_prom )
183
-
184
- if search_method == 'first' :
185
- peak_idx = peak_idx_first
186
- elif search_method == 'prominent' :
201
+ # Could check relative prominence here? e.g. np.diff(peak_prom)/peak_prom[0:-1]
202
+ # Currently, simply assume most prominent peak is the right target. It should
203
+ # be the first peak with significant prominence.
204
+
205
+ # If the most prominent peak is not the first after last RDF = 0 it
206
+ # is usually because small oscillation at RDF ~ 0 need to be discounted
207
+ if peak_idx_prom > peak_idx_first :
208
+ # Take most prominent peak as position to use
209
+ peak_idx = peak_idx_prom
210
+ # By definition peak_idx here must be >= 1
211
+ # Re-define r_0 as minimum to left of peak to ignore prior oscillations
212
+ # Find minimum in Tr as sharper
213
+ r_0 = scipy .optimize .minimize_scalar (Tr_interp ,
214
+ method = 'bounded' ,
215
+ bounds = (r [peak_list [peak_idx - 1 ]],
216
+ r [peak_list [peak_idx ]])).x
217
+
218
+ # If the most prominent peak occurs before the 'first' peak after the last
219
+ # RDF = 0 it is usually because the right base of the prominent peak dips
220
+ # below RDF = 0. The left base may also be at RDF < 0.
221
+ elif peak_idx_prom < peak_idx_first :
222
+ # Take the most prominent peak as position to use
187
223
peak_idx = peak_idx_prom
224
+ # Re-define r_0
225
+ # Find minimum preceding peak
226
+ # Set upper bound as peak position
227
+ # (-1 r-step to account for exact peak position)
228
+ r_0_ub = r [peak_list [peak_idx ]- 1 ]
229
+ if peak_idx > 0 :
230
+ # If preceding peaks have been found take the max of the next one as
231
+ # lower bound to find r_0 (+1 r-step to account for exact position)
232
+ r_0_lb = r [peak_list [peak_idx - 1 ]+ 1 ]
233
+ else :
234
+ # If no previous peaks found take the last change in sign of gradient
235
+ # (-1 r-step again)
236
+ r_0_lb = r [np .argwhere (np .sign (np .diff (Tr [np .where (r <= r_0_ub )])) == - 1 )[- 2 ]]
237
+ # Find minimum
238
+ preceding_minimum = scipy .optimize .minimize_scalar (Tr_interp ,
239
+ method = 'bounded' ,
240
+ bounds = (r_0_lb , r_0_ub )).x
241
+ # Check if minimum at RDF < 0
242
+ if rdf_interp (preceding_minimum ) < 0 :
243
+ # Redefine r_0 as last RDF = 0 crosing before peak
244
+ # First find last sign-changing interval in discrete data from
245
+ # preceding minimum to peak position
246
+ check_interval = np .ravel (np .where ((r >= preceding_minimum ) & (r <= r_0_ub )))
247
+ # Pad check_interval if len < 3
248
+ if len (check_interval ) < 3 :
249
+ check_interval = np .pad (check_interval , 1 , 'edge' )
250
+ check_interval [0 ] -= 1
251
+ check_interval [- 1 ] += 1
252
+ brent_a = check_interval [np .argwhere (np .sign (rdf [check_interval ]) == - 1 )[- 1 ]]
253
+ # Find root using scipy.optimize.brentq
254
+ r_0 = scipy .optimize .brentq (rdf_interp , r [brent_a ], r [brent_a + 1 ])
255
+ else :
256
+ r_0 = preceding_minimum
257
+
188
258
else :
189
- raise AttributeError ('\' method\' must be \' first\' or \' prominent\' ' )
259
+ # If peak idx match use located r_0
260
+ peak_idx = peak_idx_first
190
261
262
+ # Get left and right base of peak
191
263
peak_bases = (r [left_bases [peak_idx ]], r [right_bases [peak_idx ]])
264
+ # Refine rp_max (peak centre in r g(r))
265
+ rp_max = scipy .optimize .minimize_scalar (lambda x : - Tr_interp (x ), method = 'bounded' , bounds = peak_bases ).x
266
+ # Refine r_max (peak centre in r^2 g(r))
267
+ r_max = scipy .optimize .minimize_scalar (lambda x : - rdf_interp (x ), method = 'bounded' , bounds = peak_bases ).x
192
268
193
269
# Get approximate position of next peak to provide upper bound on r_min
194
270
try :
195
271
next_peak = r [peak_list [peak_idx + 1 ]]
196
272
# Handle IndexError if chosen peak is last in peak_list
197
273
except IndexError :
198
274
# Re-do peak search in g(r) if density (rho) available
275
+ # This seems useful in rare cases, but usually the search_limit needs to be increased
199
276
if rho != None :
200
277
with np .errstate (divide = 'ignore' , invalid = 'ignore' ):
201
278
gr = rdf / (4 * np .pi * rho * r ** 2 )
202
279
gr_peak_list , _ = scipy .signal .find_peaks (gr [r < peak_search_limit ])
203
- # Take the first peak after r0
204
- next_peak = r [gr_peak_list [np .argmax (gr_peak_list > brent_a )+ 1 ]]
280
+ # Take the next peak after peak_list[peak_idx]
281
+ try :
282
+ next_peak = r [np .where ((r [gr_peak_list ] > r_max ) & (r [gr_peak_list ] > rp_max ))][0 ]
283
+ except IndexError :
284
+ # Using peak_search_limit is non-ideal
285
+ # Consider warning here to suggest increasing peak_search_limit
286
+ next_peak = peak_search_limit
205
287
# If rho not available use
206
288
else :
289
+ # Using peak_search_limit is non-ideal
290
+ # Consider warning here to suggest increasing peak_search_limit
291
+ # Programmatically increasing psl may lead to recursion issues
207
292
next_peak = peak_search_limit
208
293
209
- # Refine rp_max (peak centre in r g(r))
210
- rp_max = scipy .optimize .minimize_scalar (lambda x : - Tr_interp (x ), method = 'bounded' , bounds = peak_bases ).x
211
- # Refine r_max (peak centre in r^2 g(r))
212
- r_max = scipy .optimize .minimize_scalar (lambda x : - rdf_interp (x ), method = 'bounded' , bounds = peak_bases ).x
213
-
214
294
# Refine r_min (position of 1st minimum after 1st peak)
215
295
# Note: r_min should be global minimum but optimisation may return local min
216
296
r_min = scipy .optimize .minimize_scalar (rdf_interp , method = 'bounded' , bounds = (r_max , next_peak )).x
217
297
298
+ # Check r_min is not at RDF < 0 in case where peak precedes final crossing of RDF = 0
299
+ if (peak_idx_prom < peak_idx_first ) and (rdf_interp (r_min ) < 0 ):
300
+ # Re-define r_min to preceding crossing of RDF = 0
301
+ # Find first sign changing interval between peak and r_min
302
+ check_interval = np .where ((r >= rp_max ) & (r <= r_min ))
303
+ brent_b = np .ravel (check_interval )[np .argwhere (np .sign (rdf [check_interval ]) == - 1 )[0 ]]
304
+ # Find root using scipy.optimize.brentq
305
+ r_min = scipy .optimize .brentq (rdf_interp , r [brent_b - 1 ], r [brent_b ])
306
+
218
307
return r_0 , rp_max , r_max , r_min
219
308
220
309
0 commit comments