@@ -275,6 +275,140 @@ def neighborhood(self):
275275        return  range (start , stop )
276276
277277
278+ class  ternary_search :
279+     """ 
280+     an iterator context for finding a local minimum using ternary search. 
281+ 
282+     For an interval [a, b] we evaluate f(x) and the points 
283+     x1 = a + (b - a) / 3 
284+     x2 = a + 2 * (b - a) / 3 
285+     if f(x1) < f(x2), we keep [a, x2] 
286+     if f(x1) > f(x2), we keep [x1, b] 
287+     """ 
288+ 
289+     def  __init__ (
290+         self ,
291+         start ,
292+         stop ,
293+         smallerf = lambda  x , best : x  <=  best ,
294+         suppress_bounds_warning = False ,
295+         log_level = 5 ,
296+     ):
297+         """ 
298+         Create a fresh local minimum ternary search context. 
299+ 
300+         :param start: starting point 
301+         :param stop: end point (exclusive) 
302+         :param smallerf: a function to decide if ``lhs`` is smaller than ``rhs`` 
303+         :param suppress_bounds_warning: do not warn if a boundary is picked as optimal 
304+ 
305+         """ 
306+ 
307+         if  stop  <  start :
308+             raise  ValueError (f"Incorrect bounds { start }   > { stop }  ." )
309+ 
310+         self ._suppress_bounds_warning  =  suppress_bounds_warning 
311+         self ._log_level  =  log_level 
312+         self ._start  =  start 
313+         self ._stop  =  stop  -  1 
314+         self ._x1  =  start  +  (stop  -  start ) //  3 
315+         self ._x2  =  start  +  (2  *  (stop  -  start )) //  3 
316+         self ._fx1  =  None 
317+         self ._fx2  =  None 
318+         self ._initial_bounds  =  Bounds (start , stop  -  1 )
319+         self ._smallerf  =  smallerf 
320+         self ._last_x  =  None 
321+         self ._best  =  Bounds (None , None )
322+         self ._vals  =  {}
323+ 
324+     def  __enter__ (self ):
325+         """ """ 
326+         return  self 
327+ 
328+     def  __exit__ (self , type , value , traceback ):
329+         """ """ 
330+         pass 
331+ 
332+     def  __iter__ (self ):
333+         """ """ 
334+         return  self 
335+ 
336+     def  __next__ (self ):
337+         if  self ._x1  is  not   None  and  self ._fx1  is  None :
338+             self ._last_x  =  self ._x1 
339+             return  self ._last_x 
340+         if  self ._x2  is  not   None  and  self ._fx2  is  None :
341+             self ._last_x  =  self ._x2 
342+             return  self ._last_x 
343+         if  self ._best .low  in  self ._initial_bounds  and  not  self ._suppress_bounds_warning :
344+             # We warn the user if the optimal solution is at the edge and thus possibly not optimal. 
345+             msg  =  (
346+                 f'warning: "optimal" solution { self ._best .low }   matches a bound ∈ { self ._initial_bounds }  .' ,
347+             )
348+             Logging .log ("bins" , self ._log_level , msg )
349+         raise  StopIteration 
350+ 
351+     @property  
352+     def  x (self ):
353+         return  self ._best .low 
354+ 
355+     @property  
356+     def  y (self ):
357+         return  self ._best .high 
358+ 
359+     def  update (self , res ):
360+         Logging .log ("bins" , self ._log_level , f"({ self ._last_x }  , { repr (res )}  )" )
361+ 
362+         self ._vals [self ._last_x ] =  res 
363+ 
364+         # We got nothing yet 
365+         if  self ._best .low  is  None :
366+             self ._best  =  Bounds (self ._last_x , res )
367+ 
368+         # We found something better 
369+         if  res  is  not   False  and  self ._smallerf (res , self ._best .high ):
370+             # store it 
371+             self ._best  =  Bounds (self ._last_x , res )
372+ 
373+         if  self ._last_x  ==  self ._x1 :
374+             self ._fx1  =  res 
375+ 
376+         if  self ._last_x  ==  self ._x2 :
377+             self ._fx2  =  res 
378+ 
379+         # we need to exit this loop either with something to do, or having calculated f for every point in [start, stop] 
380+         # if stop - start > 2, we are guaranteed to shrink 
381+         # to avoid getting stuck, we handle the cases stop - start <= 2 separately. 
382+ 
383+         while  self ._fx1  is  not   None  and  self ._fx2  is  not   None  and  (self ._stop  -  self ._start ) >  2 :
384+             # drop the right third 
385+             if  self ._smallerf (self ._fx1 , self ._fx2 ):
386+                 self ._start  =  self ._start 
387+                 self ._stop  =  self ._x2 
388+             # drop the left third 
389+             else :
390+                 self ._start  =  self ._x1 
391+                 self ._stop  =  self ._stop 
392+             self ._x1  =  self ._start  +  (self ._stop  -  self ._start ) //  3 
393+             self ._x2  =  self ._start  +  (2  *  (self ._stop  -  self ._start )) //  3 
394+ 
395+             # if already seen, load the value: otherwise, mark None 
396+             self ._fx1  =  self ._vals .get (self ._x1 , None )
397+             self ._fx2  =  self ._vals .get (self ._x2 , None )
398+ 
399+         # at most three integers remain: exhaustively search over them 
400+         if  self ._stop  -  self ._start  <=  2 :
401+             # print(self._start, self._stop) 
402+             next  =  [x  for  x  in  range (self ._start , self ._stop  +  1 ) if  x  not  in   self ._vals ]
403+             if  next :
404+                 # we assign remaining points arbitrarily to x1 and x2 
405+                 self ._x1  =  next [0 ]
406+                 self ._fx1  =  None 
407+                 if  len (next ) >  1 :
408+                     self ._x2  =  next [1 ]
409+                     self ._fx2  =  None 
410+ 
411+ 
278412class  early_abort_range :
279413    """ 
280414    An iterator context for finding a local minimum using linear search. 
0 commit comments