3232from numpy import sqrt
3333from numpy import spacing
3434from numpy import append
35- from numpy import min , max
35+
3636from spotpython .utils .convert import get_shape
3737from spotpython .utils .init import fun_control_init , optimizer_control_init , surrogate_control_init , design_control_init
3838from spotpython .utils .compare import selectNew
@@ -211,6 +211,11 @@ def __init__(
211211 self .optimizer_control = optimizer_control
212212 self .surrogate_control = surrogate_control
213213
214+ self .counter = 0
215+ self .success_rate = 0.0
216+ self .success_counter = 0
217+ self .window_size = 100
218+ self .min_success_rate = 0.01
214219 # small value:
215220 self .eps = sqrt (spacing (1 ))
216221
@@ -383,7 +388,6 @@ def _set_additional_attributes(self) -> None:
383388 self .y = None
384389
385390 # Logging information:
386- self .counter = 0
387391 self .min_y = None
388392 self .min_X = None
389393 self .min_mean_X = None
@@ -393,6 +397,119 @@ def _set_additional_attributes(self) -> None:
393397 self .var_y = None
394398 self .y_mo = None
395399
400+ def _get_counter (self ) -> int :
401+ """Return the current evaluation counter, always as an integer >= 0.
402+
403+ Returns:
404+ (int): current evaluation counter
405+
406+ Examples:
407+ >>> from spotpython.spot import spot
408+ >>> S = spot.Spot(fun=lambda x: x)
409+ >>> print(S._get_counter())
410+ 0
411+ """
412+ return int (getattr (self , "counter" , 0 ) or 0 )
413+
414+ def _set_counter (self , value ):
415+ """Set the evaluation counter, ensuring it is a non-negative integer.
416+
417+ Args:
418+ value (int): new evaluation counter value
419+
420+ Raises:
421+ ValueError: If the provided value is not a non-negative integer.
422+
423+ Returns:
424+ None
425+
426+ Examples:
427+ >>> from spotpython.spot import spot
428+ >>> S = spot.Spot(fun=lambda x: x)
429+ >>> S._set_counter(5)
430+ >>> print(S._get_counter())
431+ 5
432+ """
433+ if not isinstance (value , int ) or value < 0 :
434+ raise ValueError ("Counter must be a non-negative integer." )
435+ self .counter = value
436+
437+ def _increment_counter (self , n = 1 ):
438+ """Increment the evaluation counter by a specified amount.
439+
440+ Args:
441+ n (int, optional): amount to increment the counter by. Defaults to 1.
442+
443+ Returns:
444+ None
445+
446+ Examples:
447+ >>> from spotpython.spot import spot
448+ >>> S = spot.Spot(fun=lambda x: x)
449+ >>> S._increment_counter()
450+ >>> print(S._get_counter())
451+ 1
452+ """
453+ if not isinstance (n , int ) or n < 1 :
454+ raise ValueError ("Increment must be a positive integer." )
455+ if not hasattr (self , "counter" ) or self .counter is None :
456+ self .counter = 0
457+ self .counter += n
458+
459+ def _update_success_rate (self , y_new ) -> None :
460+ """
461+ Updates the rolling success rate of the optimization process.
462+ A success is counted only if the new value y0 is better (smaller) than the best found y value so far.
463+ The success rate is calculated based on the last `window_size` successes.
464+
465+ Args:
466+ y_new (np.ndarray): The new function values to consider for the success rate update.
467+
468+ Returns:
469+ float: The rolling success rate of the optimization process.
470+ """
471+ # Initialize or update the rolling history of successes (1 for success, 0 for failure)
472+ if not hasattr (self , "_success_history" ) or self ._success_history is None :
473+ self ._success_history = []
474+
475+ # Track the best y value so far
476+ best_y = min (self .y ) if self .y is not None and len (self .y ) > 0 else float ("inf" )
477+ successes = []
478+
479+ for val in y_new :
480+ if val < best_y :
481+ successes .append (1 )
482+ best_y = val # update best_y if new minimum is found
483+ else :
484+ successes .append (0 )
485+
486+ # Add new successes to the history
487+ self ._success_history .extend (successes )
488+ # Keep only the last window_size successes
489+ self ._success_history = self ._success_history [- self .window_size :]
490+
491+ # Calculate the rolling success rate
492+ window_size = len (self ._success_history )
493+ num_successes = sum (self ._success_history )
494+ self .success_rate = num_successes / window_size if window_size > 0 else 0.0
495+ # Optionally, print for debugging:
496+ # print(f"Updated rolling success rate (last {window_size}): {self.success_rate}")
497+
498+ def _get_success_rate (self ) -> float :
499+ """
500+ Get the current success rate of the optimization process.
501+
502+ Returns:
503+ float: The current success rate.
504+
505+ Examples:
506+ >>> from spotpython.spot import spot
507+ >>> S = spot.Spot(fun=lambda x: x)
508+ >>> print(S._get_success_rate())
509+ 0.0
510+ """
511+ return float (getattr (self , "success_rate" , 0.0 ) or 0.0 )
512+
396513 def _design_setup (self , design ) -> None :
397514 """
398515 Design related information:
@@ -876,13 +993,13 @@ def get_new_X0(self) -> np.array:
876993 # No new X0 found on surrogate:
877994 # use morris-mitchell ("mm") or random design as fallback
878995 if self .acquisition_failure_strategy == "mm" :
879- X0 = propose_mmphi_intensive_minimizing_point (X = self .X , n_candidates = 1000 , q = 2 , p = 2 , seed = 1 , lower = self .lower , upper = self .upper )
996+ X0 = propose_mmphi_intensive_minimizing_point (X = self .X , n_candidates = 100 , q = 2 , p = 2 , seed = 1 , lower = self .lower , upper = self .upper )
880997 # ensure that X0 is repeated according to repeats=self.design_control["repeats"]
881998 X0 = repeat (X0 , self .design_control ["repeats" ], axis = 0 )
882999 print ("Using mmphi minimizing point as fallback." )
8831000 else :
8841001 # fallback to spacefilling design (acquisition_failure_strategy == "random"):
885- self .design = SpaceFilling (k = self .k , seed = self .fun_control ["seed" ] + self .counter )
1002+ self .design = SpaceFilling (k = self .k , seed = self .fun_control ["seed" ] + self ._get_counter () )
8861003 X0 = self .generate_design (size = self .n_points , repeats = self .design_control ["repeats" ], lower = self .lower , upper = self .upper )
8871004 print ("Using spacefilling design as fallback." )
8881005 X0 = repair_non_numeric (X0 , self .var_type )
@@ -1265,7 +1382,7 @@ def evaluate_initial_design(self) -> None:
12651382 # TODO: Error if only nan values are returned
12661383 logger .debug ("New y value: %s" , self .y )
12671384
1268- self .counter = self .y .size
1385+ self ._set_counter ( self .y .size )
12691386 self .X , self .y = remove_nan (self .X , self .y , stop_on_zero_return = True )
12701387
12711388 if self .X .shape [0 ] == 0 :
@@ -1338,8 +1455,8 @@ def update_stats(self) -> None:
13381455 """
13391456 self .min_y = min (self .y )
13401457 self .min_X = self .X [argmin (self .y )]
1341- self .counter = self .y .size
1342- self .fun_control .update ({"counter" : self .counter })
1458+ self ._set_counter ( self .y .size )
1459+ self .fun_control .update ({"counter" : self ._get_counter () })
13431460 # Update aggregated x and y values (if noise):
13441461 if self .noise :
13451462 Z = aggregate_mean_var (X = self .X , y = self .y )
@@ -1595,17 +1712,9 @@ def update_design(self) -> None:
15951712 # in this case increase the success_counter. Calculate the success rate, which is defined as
15961713 # the number of successful improvements divided by the total number of function evaluations over
15971714 # the last window_size evaluations:
1598- # self.success_window_size = 10
1599- # if y0.shape[0] > 0:
1600- # for y_val in y0:
1601- # if y_val < self.min_y:
1602- # self.success_counter += 1
1603- # total_evaluations = self.counter + y0.shape[0]
1604- # window_size = min(total_evaluations, self.success_window_size)
1605- # self.success_rate = self.success_counter / window_size
1606- # print(f"Success rate over the last {window_size} evaluations: {self.success_rate:.4f}")
1715+ self ._update_success_rate (y_new = y0 )
16071716 # Append New Solutions (only if they are not nan):
1608- if y0 .shape [0 ] > 0 :
1717+ if ( y0 .shape [0 ] > 0 ) and ( self . _get_success_rate () >= self . min_success_rate ) :
16091718 self .X = np .append (self .X , X0 , axis = 0 )
16101719 self .y = np .append (self .y , y0 )
16111720 else :
@@ -1789,7 +1898,7 @@ def _init_spot_writer(self) -> None:
17891898 print ("No tensorboard log created." )
17901899
17911900 def should_continue (self , timeout_start ) -> bool :
1792- return (self .counter < self .fun_evals ) and (time .time () < timeout_start + self .max_time * 60 )
1901+ return (self ._get_counter () < self .fun_evals ) and (time .time () < timeout_start + self .max_time * 60 )
17931902
17941903 def generate_random_point (self ):
17951904 """Generate a random point in the design space.
@@ -1824,13 +1933,28 @@ def generate_random_point(self):
18241933 assert np.all(X0 <= S.upper)
18251934 assert y0 >= 0
18261935 """
1827- X0 = self .generate_design (
1828- size = 1 ,
1829- repeats = 1 ,
1830- lower = self .lower ,
1831- upper = self .upper ,
1832- )
1833- X0 = repair_non_numeric (X = X0 , var_type = self .var_type )
1936+ # X0 = self.generate_design(
1937+ # size=1,
1938+ # repeats=1,
1939+ # lower=self.lower,
1940+ # upper=self.upper,
1941+ # )
1942+
1943+ # No new X0 found:
1944+ # use morris-mitchell ("mm") or random design as fallback
1945+ if self .acquisition_failure_strategy == "mm" :
1946+ X0 = propose_mmphi_intensive_minimizing_point (X = self .X , n_candidates = 1000 , q = 2 , p = 2 , seed = 1 , lower = self .lower , upper = self .upper )
1947+ # ensure that X0 is repeated according to repeats=self.design_control["repeats"]
1948+ X0 = repeat (X0 , self .design_control ["repeats" ], axis = 0 )
1949+ print ("Using mmphi minimizing point as fallback." )
1950+ else :
1951+ # fallback to spacefilling design (acquisition_failure_strategy == "random"):
1952+ self .design = SpaceFilling (k = self .k , seed = self .fun_control ["seed" ] + self ._get_counter ())
1953+ X0 = self .generate_design (size = self .n_points , repeats = self .design_control ["repeats" ], lower = self .lower , upper = self .upper )
1954+ print ("Using spacefilling design as fallback." )
1955+ X0 = repair_non_numeric (X0 , self .var_type )
1956+
1957+ # X0 = repair_non_numeric(X=X0, var_type=self.var_type)
18341958 X_all = self .to_all_dim_if_needed (X0 )
18351959 logger .debug ("In Spot() generate_random_point(), before calling self.fun: X_all: %s" , X_all )
18361960 logger .debug ("In Spot() generate_random_point(), before calling self.fun: fun_control: %s" , self .fun_control )
@@ -1858,9 +1982,9 @@ def show_progress_if_needed(self, timeout_start) -> None:
18581982 if not self .show_progress :
18591983 return
18601984 if isfinite (self .fun_evals ):
1861- progress_bar (progress = self .counter / self .fun_evals , y = self .min_y , filename = self .progress_file )
1985+ progress_bar (progress = self ._get_counter () / self .fun_evals , y = self .min_y , filename = self .progress_file , success_rate = self . _get_success_rate () )
18621986 else :
1863- progress_bar (progress = (time .time () - timeout_start ) / (self .max_time * 60 ), y = self .min_y , filename = self .progress_file )
1987+ progress_bar (progress = (time .time () - timeout_start ) / (self .max_time * 60 ), y = self .min_y , filename = self .progress_file , success_rate = self . _get_success_rate () )
18641988
18651989 def generate_design (self , size , repeats , lower , upper ) -> np .array :
18661990 """Generate a design with `size` points in the interval [lower, upper].
@@ -1908,9 +2032,9 @@ def update_writer(self) -> None:
19082032 X_min = self .min_X .copy ()
19092033 # y_min: best y value so far
19102034 # y_last: last y value, can be worse than y_min
1911- self .spot_writer .add_scalars ("spot_y" , {"min" : y_min , "last" : y_last }, self .counter )
2035+ self .spot_writer .add_scalars ("spot_y" , {"min" : y_min , "last" : y_last }, self ._get_counter () )
19122036 # X_min: X value of the best y value so far
1913- self .spot_writer .add_scalars ("spot_X" , {f"X_{ i } " : X_min [i ] for i in range (self .k )}, self .counter )
2037+ self .spot_writer .add_scalars ("spot_X" , {f"X_{ i } " : X_min [i ] for i in range (self .k )}, self ._get_counter () )
19142038 else :
19152039 # get the last n y values:
19162040 y_last_n = self .y [- self .fun_repeats :].copy ()
@@ -1920,13 +2044,13 @@ def update_writer(self) -> None:
19202044 X_min_mean = self .min_mean_X .copy ()
19212045 # y_min_var: variance of the min y value so far
19222046 y_min_var = self .min_var_y .copy ()
1923- self .spot_writer .add_scalar ("spot_y_min_var" , y_min_var , self .counter )
2047+ self .spot_writer .add_scalar ("spot_y_min_var" , y_min_var , self ._get_counter () )
19242048 # y_min_mean: best mean y value so far (see above)
1925- self .spot_writer .add_scalar ("spot_y" , y_min_mean , self .counter )
2049+ self .spot_writer .add_scalar ("spot_y" , y_min_mean , self ._get_counter () )
19262050 # last n y values (noisy):
1927- self .spot_writer .add_scalars ("spot_y" , {f"y_last_n{ i } " : y_last_n [i ] for i in range (self .fun_repeats )}, self .counter )
2051+ self .spot_writer .add_scalars ("spot_y" , {f"y_last_n{ i } " : y_last_n [i ] for i in range (self .fun_repeats )}, self ._get_counter () )
19282052 # X_min_mean: X value of the best mean y value so far (see above)
1929- self .spot_writer .add_scalars ("spot_X_noise" , {f"X_min_mean{ i } " : X_min_mean [i ] for i in range (self .k )}, self .counter )
2053+ self .spot_writer .add_scalars ("spot_X_noise" , {f"X_min_mean{ i } " : X_min_mean [i ] for i in range (self .k )}, self ._get_counter () )
19302054 # get last value of self.X and convert to dict. take the values from self.var_name as keys:
19312055 X_last = self .X [- 1 ].copy ()
19322056 config = {self .var_name [i ]: X_last [i ] for i in range (self .k )}
@@ -2209,9 +2333,9 @@ def plot_model(self, y_min=None, y_max=None) -> None:
22092333 plt .legend (loc = "best" )
22102334 # plt.title(self.surrogate.__class__.__name__ + ". " + str(self.counter) + ": " + str(self.min_y))
22112335 if self .noise :
2212- plt .title ("fun_evals: " + str (self .counter ) + ". min_y (noise): " + str (np .round (self .min_y , 6 )) + " min_mean_y: " + str (np .round (self .min_mean_y , 6 )))
2336+ plt .title ("fun_evals: " + str (self ._get_counter () ) + ". min_y (noise): " + str (np .round (self .min_y , 6 )) + " min_mean_y: " + str (np .round (self .min_mean_y , 6 )))
22132337 else :
2214- plt .title ("fun_evals: " + str (self .counter ) + ". min_y: " + str (np .round (self .min_y , 6 )))
2338+ plt .title ("fun_evals: " + str (self ._get_counter () ) + ". min_y: " + str (np .round (self .min_y , 6 )))
22152339 plt .show ()
22162340
22172341 def print_results (self , print_screen = True , dict = None ) -> list [str ]:
0 commit comments