77from typing import NamedTuple
88
99import numpy as np
10+ import pandas as pd
1011
1112from tranquilo .acceptance_sample_size import (
1213 get_acceptance_sample_sizes ,
@@ -20,6 +21,7 @@ def get_acceptance_decider(acceptance_decider, acceptance_options):
2021 "classic" : _accept_classic ,
2122 "naive_noisy" : accept_naive_noisy ,
2223 "noisy" : accept_noisy ,
24+ "classic_line_search" : accept_classic_line_search ,
2325 }
2426
2527 out = get_component (
@@ -85,6 +87,142 @@ def accept_naive_noisy(
8587 return out
8688
8789
90+ def accept_classic_line_search (
91+ subproblem_solution ,
92+ state ,
93+ history ,
94+ * ,
95+ wrapped_criterion ,
96+ min_improvement ,
97+ batch_size ,
98+ sample_points ,
99+ search_radius_factor ,
100+ rng ,
101+ ):
102+ # ==================================================================================
103+ # Quick return if batch_size is 1
104+
105+ if batch_size == 1 :
106+ return _accept_classic (
107+ subproblem_solution = subproblem_solution ,
108+ state = state ,
109+ history = history ,
110+ wrapped_criterion = wrapped_criterion ,
111+ min_improvement = min_improvement ,
112+ )
113+
114+ # ==================================================================================
115+ # Add candidate to history
116+
117+ candidate_x = subproblem_solution .x
118+ candidate_index = history .add_xs (candidate_x )
119+
120+ eval_info = {candidate_index : 1 }
121+
122+ # ==================================================================================
123+ # Determine whether the candidate it sufficiently close to the border of the
124+ # trustregion, in which case we perform a line search
125+
126+ perform_line_search = _is_on_border (state .trustregion , x = candidate_x , rtol = 1e-1 )
127+
128+ if perform_line_search :
129+ alpha_grid = _generate_alpha_grid (batch_size )
130+
131+ line_search_xs = _sample_on_line (
132+ start_point = state .x , direction_point = candidate_x , alpha_grid = alpha_grid
133+ )
134+ else :
135+ line_search_xs = None
136+
137+ # ==================================================================================
138+ # Check whether there are any unallocated evaluations left, and if yes perform a
139+ # speculative sampling
140+
141+ n_evals_line_search = 0 if line_search_xs is None else len (line_search_xs )
142+ n_unallocated_evals = batch_size - 1 - n_evals_line_search
143+
144+ if n_unallocated_evals > 0 :
145+ speculative_xs = _generate_speculative_sample (
146+ new_center = candidate_x ,
147+ search_radius_factor = search_radius_factor ,
148+ trustregion = state .trustregion ,
149+ sample_points = sample_points ,
150+ n_points = n_unallocated_evals ,
151+ history = history ,
152+ rng = rng ,
153+ )
154+ else :
155+ speculative_xs = None
156+
157+ # ==================================================================================
158+ # Consolidate newly sampled points
159+
160+ if line_search_xs is not None and speculative_xs is not None :
161+ new_xs = np .vstack ([line_search_xs , speculative_xs ])
162+ elif line_search_xs is not None :
163+ new_xs = line_search_xs
164+ elif speculative_xs is not None :
165+ new_xs = speculative_xs
166+
167+ # ==================================================================================
168+ # Add new points to history and evaluate criterion
169+
170+ new_indices = history .add_xs (new_xs )
171+
172+ for idx in new_indices :
173+ eval_info [idx ] = 1
174+
175+ wrapped_criterion (eval_info )
176+
177+ # ==================================================================================
178+ # Calculate rho
179+
180+ candidate_fval = np .mean (history .get_fvals (candidate_index ))
181+
182+ actual_improvement = - (candidate_fval - state .fval )
183+
184+ rho = calculate_rho (
185+ actual_improvement = actual_improvement ,
186+ expected_improvement = subproblem_solution .expected_improvement ,
187+ )
188+
189+ # ==================================================================================
190+ # Check if there are any better points
191+
192+ new_fvals = history .get_fvals (new_indices )
193+ new_fvals = pd .Series ({i : np .mean (fvals ) for i , fvals in new_fvals .items ()})
194+ new_fval_argmin = new_fvals .idxmin ()
195+
196+ found_better_candidate = new_fvals .loc [new_fval_argmin ] < candidate_fval
197+
198+ # If a better point was found, update the candidates
199+ if found_better_candidate :
200+ candidate_x = history .get_xs (new_fval_argmin )
201+ candidate_fval = new_fvals .loc [new_fval_argmin ]
202+ candidate_index = new_fval_argmin
203+
204+ # ==================================================================================
205+ # Calculate the overall improvement using a potentially updated candidate and draw
206+ # the acceptance conclusions based on that.
207+
208+ overall_improvement = - (candidate_fval - state .fval )
209+ is_accepted = overall_improvement >= min_improvement
210+
211+ # ==================================================================================
212+ # Return results
213+
214+ res = _get_acceptance_result (
215+ candidate_x = candidate_x ,
216+ candidate_fval = candidate_fval ,
217+ candidate_index = candidate_index ,
218+ rho = rho ,
219+ is_accepted = is_accepted ,
220+ old_state = state ,
221+ n_evals = 1 ,
222+ )
223+ return res
224+
225+
88226def _accept_simple (
89227 subproblem_solution ,
90228 state ,
@@ -247,3 +385,100 @@ def calculate_rho(actual_improvement, expected_improvement):
247385 else :
248386 rho = actual_improvement / expected_improvement
249387 return rho
388+
389+
390+ # ======================================================================================
391+ # Helper functions for line search
392+ # ======================================================================================
393+
394+
395+ def _generate_speculative_sample (
396+ new_center , trustregion , sample_points , n_points , history , search_radius_factor , rng
397+ ):
398+ """Generative a speculative sample.
399+
400+ Args:
401+ new_center (np.ndarray): New center of the trust region.
402+ trustregion (Region): Current trust region.
403+ sample_points (callable): Function to sample points.
404+ n_points (int): Number of points to sample.
405+ history (History): Tranquilo history.
406+ search_radius_factor (float): Factor to multiply the trust region radius by to
407+ get the search radius.
408+ rng (np.random.Generator): Random number generator.
409+
410+ Returns:
411+ np.ndarray: Speculative sample.
412+
413+ """
414+ search_region = trustregion ._replace (
415+ center = new_center , radius = search_radius_factor * trustregion .radius
416+ )
417+
418+ old_indices = history .get_x_indices_in_region (search_region )
419+
420+ old_xs = history .get_xs (old_indices )
421+
422+ model_xs = old_xs
423+
424+ new_xs = sample_points (
425+ search_region ,
426+ n_points = n_points ,
427+ existing_xs = model_xs ,
428+ rng = rng ,
429+ )
430+ return new_xs
431+
432+
433+ def _sample_on_line (start_point , direction_point , alpha_grid ):
434+ """Sample points on a line defined by startind and direction points.
435+
436+ Args:
437+ start_point (np.ndarray): Starting point of the line.
438+ direction_point (np.ndarray): Direction point of the line.
439+ alpha_grid (np.ndarray): Grid of alphas to sample points on the line. 0
440+ corresponds to the starting point and 1 corresponds to the direction point.
441+ Points larger than 1 are beyond the direction point.
442+
443+ Returns:
444+ np.ndarray: Sampled points on the line.
445+
446+ """
447+ xs = start_point + alpha_grid .reshape (- 1 , 1 ) * (direction_point - start_point )
448+ return xs
449+
450+
451+ def _is_on_border (trustregion , x , rtol ):
452+ """Check whether a point is sufficiently close to the border of a trust region.
453+
454+ Args:
455+ trustregion (Region): Trust region.
456+ x (np.ndarray): Point to check.
457+ rtol (float): Relative tolerance.
458+
459+ Returns:
460+ bool: True if the point is sufficiently close to the border of the trust region.
461+
462+ """
463+ if trustregion .shape == "sphere" :
464+ candidate_on_border = _is_on_sphere_border (trustregion , x = x , rtol = rtol )
465+ else :
466+ candidate_on_border = _is_on_cube_border (trustregion , x = x , rtol = rtol )
467+ return candidate_on_border
468+
469+
470+ def _is_on_sphere_border (trustregion , x , rtol ):
471+ x_center_dist = np .linalg .norm (x - trustregion .center , ord = 2 )
472+ return np .isclose (x_center_dist , trustregion .radius , rtol = rtol )
473+
474+
475+ def _is_on_cube_border (trustregion , x , rtol ):
476+ cube_bounds = trustregion .cube_bounds
477+ is_on_lower_border = np .isclose (x , cube_bounds .lower , rtol = rtol ).any ()
478+ is_on_upper_border = np .isclose (x , cube_bounds .upper , rtol = rtol ).any ()
479+ return is_on_lower_border or is_on_upper_border
480+
481+
482+ def _generate_alpha_grid (batch_size ):
483+ n_points = min (batch_size , 4 ) - 1
484+ return 2 ** np .arange (1 , n_points + 1 , dtype = float )
0 commit comments