Skip to content

Commit 2250b58

Browse files
0.34.6
1 parent 5ae5500 commit 2250b58

7 files changed

Lines changed: 891 additions & 1212 deletions

File tree

notebooks/spot_aquisition_mm_rosenbrock_6d.ipynb

Lines changed: 319 additions & 2 deletions
Large diffs are not rendered by default.

notebooks/spot_aquisition_random_rosenbrock_6d.ipynb

Lines changed: 346 additions & 1171 deletions
Large diffs are not rendered by default.

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ build-backend = "setuptools.build_meta"
77

88
[project]
99
name = "spotpython"
10-
version = "0.34.5"
10+
version = "0.34.6"
1111
authors = [
1212
{ name="T. Bartz-Beielstein", email="tbb@bartzundbartz.de" }
1313
]

src/spotpython/spot/spot.py

Lines changed: 159 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@
3232
from numpy import sqrt
3333
from numpy import spacing
3434
from numpy import append
35-
from numpy import min, max
35+
3636
from spotpython.utils.convert import get_shape
3737
from spotpython.utils.init import fun_control_init, optimizer_control_init, surrogate_control_init, design_control_init
3838
from 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]:

src/spotpython/utils/progress.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from sys import stdout
22

33

4-
def progress_bar(progress: float, bar_length: int = 10, message: str = "spotpython tuning:", y=None, filename=None) -> None:
4+
def progress_bar(progress: float, success_rate: float, bar_length: int = 10, message: str = "spotpython tuning:", y=None, filename=None) -> None:
55
"""
66
Displays or updates a console progress bar.
77
@@ -10,6 +10,8 @@ def progress_bar(progress: float, bar_length: int = 10, message: str = "spotpyth
1010
a float between 0 and 1. Any int will be converted to a float.
1111
A value under 0 represents a halt.
1212
A value at 1 or bigger represents 100%.
13+
success_rate (float):
14+
a float between 0 and 1 representing the success rate
1315
bar_length (int):
1416
length of the progress bar
1517
message (str):
@@ -30,7 +32,7 @@ def progress_bar(progress: float, bar_length: int = 10, message: str = "spotpyth
3032
progress = 1
3133
status = "Done...\r\n"
3234
block = int(round(bar_length * progress))
33-
text = f"{message} [{'#' * block + '-' * (bar_length - block)}] {progress * 100:.2f}% {status}\r\n"
35+
text = f"{message} [{'#' * block + '-' * (bar_length - block)}] {progress * 100:.2f}%. Success rate: {success_rate * 100:.2f}% {status}\r\n"
3436
if filename is not None:
3537
file.write(text)
3638
file.flush()

test/test_get_spot_attributes_as_df.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ def test_get_spot_attributes_as_df():
3434

3535
# Define expected attribute names (ensure these match your Spot class' attributes)
3636
expected_attributes = ['X',
37-
'acquisition_failure_strategy',
37+
'_success_history',
38+
'acquisition_failure_strategy',
3839
'all_lower',
3940
'all_upper',
4041
'all_var_name',
@@ -59,6 +60,7 @@ def test_get_spot_attributes_as_df():
5960
'min_X',
6061
'min_mean_X',
6162
'min_mean_y',
63+
'min_success_rate',
6264
'min_y',
6365
'n_points',
6466
'noise',
@@ -72,6 +74,8 @@ def test_get_spot_attributes_as_df():
7274
'show_models',
7375
'show_progress',
7476
'spot_writer',
77+
'success_counter',
78+
'success_rate',
7579
'surrogate',
7680
'surrogate_control',
7781
'tkagg',
@@ -82,6 +86,7 @@ def test_get_spot_attributes_as_df():
8286
'var_type',
8387
'var_y',
8488
'verbosity',
89+
'window_size',
8590
'y',
8691
'y_mo']
8792

0 commit comments

Comments
 (0)