2121from collections .abc import Callable
2222
2323import matplotlib .pyplot as plt
24+ import pandas as pd
2425import torch
2526from ax import (
2627 Experiment ,
7273large_dataset_var = QOI_ESTIMATOR .var (ests )
7374
7475
75- # %%
7676def run_trials (
7777 experiment : Experiment ,
7878 warm_up_generator : Callable [[Experiment ], GeneratorRun ],
7979 doe_generator : Callable [[Experiment ], GeneratorRun ],
8080 warm_up_runs : int = 3 ,
8181 doe_runs : int = 15 ,
82- ) -> None :
82+ stopping_criteria : Callable [[Experiment ], bool ] | None = None ,
83+ ) -> int :
8384 """Helper function for running trials for an experiment and returning the QOI results using QoI metric.
8485
8586 Args:
@@ -88,8 +89,12 @@ def run_trials(
8889 doe_generator: The generator being used to perform the DoE.
8990 warm_up_runs: Number of warm-up runs to perform before starting the DoE.
9091 doe_runs: Number of DoE runs to perform.
92+ stopping_criteria: Optional function that takes an experiment and returns True if a given
93+ stopping criteria is met. If this stopping criteria is not met after `doe_runs` iterations, the function will
94+ return the number of iterations run.
9195
9296 """
97+ # Warm-up phase
9398 for i in range (doe_runs + 1 ):
9499 if i == 0 :
95100 for _ in range (warm_up_runs ):
@@ -105,12 +110,48 @@ def run_trials(
105110 _ = trial .mark_completed ()
106111 print (f"iter { i } done" )
107112
113+ # Check stopping criteria after each DoE iteration
114+ if stopping_criteria is not None and stopping_criteria (experiment ):
115+ print (f"Stopping criteria met after { i } DoE iterations" )
116+ return i
117+
118+ return doe_runs + warm_up_runs
119+
120+
121+ def sem_stopping_criteria (experiment : Experiment , sem_threshold : float = 0.145 , metric_name : str = "QoIMetric" ) -> bool :
122+ """Stopping criteria based on standard error of the mean (SEM) of QoI metric of the GP.
123+
124+ Args:
125+ experiment: The experiment to check
126+ sem_threshold: SEM threshold for stopping criteria
127+ metric_name: Name of the metric to check for stopping criteria
128+
129+ Returns:
130+ True if stopping criteria is met (SEM below threshold), False otherwise
131+ """
132+ metrics = experiment .fetch_data ()
133+ qoi_metrics = metrics .df [metrics .df ["metric_name" ] == metric_name ]
134+
135+ if len (qoi_metrics ) == 0 :
136+ print (f"No { metric_name } data found in the experiment." )
137+ return False
138+
139+ # Get the latest QoI metric result
140+ latest_qoi = qoi_metrics .iloc [- 1 ]
141+
142+ if pd .notna (latest_qoi ["sem" ]) and latest_qoi ["sem" ] <= sem_threshold :
143+ print (f"SEM threshold met: { latest_qoi ['sem' ]:.4f} <= { sem_threshold } " )
144+ return True
145+
146+ return False
147+
108148
109149# %% [markdown]
110150# How many iterations to run in the following DOEs
111151
112152# %%
113- n_iter = 30
153+ n_iter_sobol = 100
154+ n_iter_doe = 30
114155warm_up_runs = 8
115156
116157# %% [markdown]
@@ -150,12 +191,14 @@ def sobol_generator_run(_: Experiment) -> GeneratorRun:
150191
151192sobol_generator_run = create_sobol_generator (sobol )
152193
153- run_trials (
194+
195+ last_itr_sobol = run_trials (
154196 experiment = exp_sobol ,
155197 warm_up_generator = sobol_generator_run ,
156198 doe_generator = sobol_generator_run ,
157199 warm_up_runs = warm_up_runs ,
158- doe_runs = n_iter ,
200+ doe_runs = n_iter_sobol ,
201+ stopping_criteria = sem_stopping_criteria , # Optional: use a stopping criteria based on confidence bound
159202)
160203
161204
@@ -172,7 +215,9 @@ def sobol_generator_run(_: Experiment) -> GeneratorRun:
172215 exp_sobol , warm_up_runs , metrics = true_loc_scale_function_estimates
173216)
174217fig_trial_warm_up .show ()
175- fig_last_trial = plot_gp_fits_2d_surface_from_experiment (exp_sobol , n_iter , metrics = true_loc_scale_function_estimates )
218+ fig_last_trial = plot_gp_fits_2d_surface_from_experiment (
219+ exp_sobol , last_itr_sobol , metrics = true_loc_scale_function_estimates
220+ )
176221fig_last_trial .show ()
177222
178223# %%
@@ -340,12 +385,13 @@ def look_ahead_generator_run(experiment: Experiment) -> GeneratorRun:
340385# This needs to be instantiated outside of the loop so the internal state of the generator persists.
341386sobol = Models .SOBOL (search_space = exp_look_ahead .search_space , seed = 5 )
342387
343- run_trials (
388+ last_itr_look_ahead = run_trials (
344389 experiment = exp_look_ahead ,
345390 warm_up_generator = create_sobol_generator (sobol ),
346391 doe_generator = look_ahead_generator_run ,
347392 warm_up_runs = warm_up_runs ,
348- doe_runs = n_iter ,
393+ doe_runs = n_iter_doe ,
394+ stopping_criteria = sem_stopping_criteria , # Optional: use a stopping criteria based on confidence bound
349395)
350396
351397# %%
@@ -355,7 +401,7 @@ def look_ahead_generator_run(experiment: Experiment) -> GeneratorRun:
355401)
356402fig_trial_warm_up .show ()
357403fig_last_trial = plot_gp_fits_2d_surface_from_experiment (
358- exp_look_ahead , n_iter , metrics = true_loc_scale_function_estimates
404+ exp_look_ahead , last_itr_look_ahead , metrics = true_loc_scale_function_estimates
359405)
360406fig_last_trial .show ()
361407
@@ -371,13 +417,6 @@ def look_ahead_generator_run(experiment: Experiment) -> GeneratorRun:
371417# %%
372418# For a more sophisticated stopping criteria experiment, see doe_dev.py as of 2025-06-10.
373419_ , ax = plt .subplots ()
374- _ = ax .axhline (
375- large_dataset_mean + 1.96 * large_dataset_var ** 0.5 ,
376- c = "sandybrown" ,
377- label = f"Stopping criteria\n ({ large_dataset_points } Sobol points)" ,
378- )
379- _ = ax .axhline (large_dataset_mean - 1.96 * large_dataset_var ** 0.5 , c = "sandybrown" )
380- _ = ax .axhline (large_dataset_mean , c = "sandybrown" , linestyle = "--" , label = "Sobol mean" )
381420ax = plot_qoi_estimates_from_experiment (exp_sobol , ax = ax , name = "Sobol" )
382421ax = plot_qoi_estimates_from_experiment (exp_look_ahead , ax = ax , color = "green" , name = "look ahead" )
383422_ = ax .axhline (brute_force_qoi , c = "black" , label = "brute_force_value" )
0 commit comments