55
66# pyre-strict
77
8+ import warnings
89from collections .abc import Iterable , Mapping , Sequence
910from dataclasses import dataclass , field
10- from math import sqrt
1111from typing import Any
1212
1313import numpy as np
1616from ax .benchmark .benchmark_step_runtime_function import TBenchmarkStepRuntimeFunction
1717from ax .benchmark .benchmark_test_function import BenchmarkTestFunction
1818from ax .benchmark .benchmark_trial_metadata import BenchmarkTrialMetadata
19+ from ax .benchmark .noise import GaussianNoise , Noise
1920from ax .core .base_trial import BaseTrial , TrialStatus
2021from ax .core .batch_trial import BatchTrial
2122from ax .core .runner import Runner
2425from ax .runners .simulated_backend import SimulatedBackendRunner
2526from ax .utils .common .serialization import TClassDecoderRegistry , TDecoderRegistry
2627from ax .utils .testing .backend_simulator import BackendSimulator , BackendSimulatorOptions
27- from pyre_extensions import assert_is_instance
2828
2929
3030def _dict_of_arrays_to_df (
@@ -70,54 +70,6 @@ def _dict_of_arrays_to_df(
7070 return df
7171
7272
73- def _add_noise (
74- df : pd .DataFrame ,
75- noise_stds : Mapping [str , float ],
76- arm_weights : Mapping [str , float ] | None ,
77- ) -> pd .DataFrame :
78- """
79- For each ``Y_true`` in ``df``, with metric name ``metric_name`` and
80- arm name ``arm_name``, add noise with standard deviation
81- ``noise_stds[metric_name] / sqrt_nlzd_arm_weights[arm_name]``,
82- where ``sqrt_nlzd_arm_weights = sqrt(arm_weights[arm_name] /
83- sum(arm_weights.values())])``.
84-
85- Args:
86- df: A DataFrame with columns including
87- ["metric_name", "arm_name", "Y_true"].
88- noise_stds: A mapping from metric name to what the standard
89- deviation would be if one arm received the entire
90- sample budget.
91- arm_weights: Either ``None`` if there is only one ``Arm``, or a
92- mapping from ``Arm`` name to the arm's allocation. Using arm
93- weights will increase noise levels, since each ``Arm`` is
94- assumed to receive a fraction of the total sample budget.
95-
96- Returns:
97- The original ``df``, now with additional columns ["mean", "sem"].
98- """
99- noiseless = all (v == 0 for v in noise_stds .values ())
100- if not noiseless :
101- noise_std_ser = df ["metric_name" ].map (noise_stds )
102- if arm_weights is not None :
103- nlzd_arm_weights_sqrt = {
104- arm_name : sqrt (weight / sum (arm_weights .values ()))
105- for arm_name , weight in arm_weights .items ()
106- }
107- arm_weights_ser = df ["arm_name" ].map (nlzd_arm_weights_sqrt )
108- df ["sem" ] = noise_std_ser / arm_weights_ser
109-
110- else :
111- df ["sem" ] = noise_std_ser
112-
113- df ["mean" ] = df ["Y_true" ] + np .random .normal (loc = 0 , scale = df ["sem" ])
114-
115- else :
116- df ["sem" ] = 0.0
117- df ["mean" ] = df ["Y_true" ]
118- return df
119-
120-
12173def get_total_runtime (
12274 trial : BaseTrial ,
12375 step_runtime_function : TBenchmarkStepRuntimeFunction | None ,
@@ -140,7 +92,7 @@ class BenchmarkRunner(Runner):
14092 A Runner that produces both observed and ground-truth values.
14193
14294 Observed values equal ground-truth values plus noise, with the noise added
143- according to the standard deviations returned by `get_noise_stds()` .
95+ according to the `Noise` object provided .
14496
14597 This runner does require that every benchmark has a ground truth, which
14698 won't necessarily be true for real-world problems. Such problems fall into
@@ -162,8 +114,10 @@ class BenchmarkRunner(Runner):
162114 Args:
163115 test_function: A ``BenchmarkTestFunction`` from which to generate
164116 deterministic data before adding noise.
165- noise_std: The standard deviation of the noise added to the data. Can be
166- a list or dict to be per-metric.
117+ noise: A ``Noise`` object that determines how noise is added to the
118+ ground-truth evaluations. Defaults to noiseless
119+ (``GaussianNoise(noise_std=0.0)``).
120+ noise_std: Deprecated. Use ``noise`` instead.
167121 step_runtime_function: A function that takes in parameters
168122 (in ``TParameterization`` format) and returns the runtime of a step.
169123 max_concurrency: The maximum number of trials that can be running at a
@@ -176,24 +130,30 @@ class BenchmarkRunner(Runner):
176130 """
177131
178132 test_function : BenchmarkTestFunction
179- noise_std : float | Sequence [float ] | Mapping [str , float ] = 0.0
133+ noise : Noise = field (default_factory = GaussianNoise )
134+ noise_std : float | Sequence [float ] | Mapping [str , float ] | None = None
180135 step_runtime_function : TBenchmarkStepRuntimeFunction | None = None
181136 max_concurrency : int = 1
182137 force_use_simulated_backend : bool = False
183138 simulated_backend_runner : SimulatedBackendRunner | None = field (init = False )
184139
185140 def __post_init__ (self ) -> None :
186- # Check for conflicting noise configuration
187- has_custom_noise = self .test_function .add_custom_noise is not None
188-
189- # This works for both lists and dicts, and the user specifies anything
190- # other than 0.0 as noise_std alongside a custom noise, we error out.
191- if has_custom_noise and (self .noise_std != 0.0 ):
192- raise ValueError (
193- "Cannot specify both `add_custom_noise` on the test function and "
194- "a `noise_std`. Either use `add_custom_noise` for custom "
195- "noise behavior or `noise_std` for default noise behavior."
141+ # Handle backward compatibility for noise_std parameter
142+ if self .noise_std is not None :
143+ warnings .warn (
144+ "noise_std is deprecated. Use noise=GaussianNoise(noise_std=...) "
145+ "instead." ,
146+ DeprecationWarning ,
147+ stacklevel = 2 ,
196148 )
149+ # Check if noise was also explicitly set (not default)
150+ if not isinstance (self .noise , GaussianNoise ) or self .noise .noise_std != 0.0 :
151+ raise ValueError (
152+ "Cannot specify both 'noise_std' and a non-default 'noise'. "
153+ "Use only 'noise=GaussianNoise(noise_std=...)' instead."
154+ )
155+ # Convert noise_std to GaussianNoise
156+ object .__setattr__ (self , "noise" , GaussianNoise (noise_std = self .noise_std ))
197157
198158 use_simulated_backend = (
199159 (self .max_concurrency > 1 )
@@ -239,22 +199,6 @@ def get_Y_true(self, params: Mapping[str, TParamValue]) -> npt.NDArray:
239199 return result [:, None ]
240200 return result
241201
242- def get_noise_stds (self ) -> dict [str , float ]:
243- noise_std = self .noise_std
244- if isinstance (noise_std , float | int ):
245- return {name : float (noise_std ) for name in self .outcome_names }
246- elif isinstance (noise_std , dict ):
247- if not set (noise_std .keys ()) == set (self .outcome_names ):
248- raise ValueError (
249- "Noise std must have keys equal to outcome names if given as "
250- "a dict."
251- )
252- return noise_std
253- # list of floats
254- return dict (
255- zip (self .outcome_names , assert_is_instance (noise_std , list ), strict = True )
256- )
257-
258202 def run (self , trial : BaseTrial ) -> dict [str , BenchmarkTrialMetadata ]:
259203 """Run the trial by evaluating its parameterization(s).
260204
@@ -293,15 +237,13 @@ def run(self, trial: BaseTrial) -> dict[str, BenchmarkTrialMetadata]:
293237 if isinstance (trial , BatchTrial )
294238 else None
295239 )
296- # Check for custom noise function, otherwise use default noise behavior
297- if self .test_function .add_custom_noise is not None :
298- df = self .test_function .add_custom_noise (
299- df , trial , self .get_noise_stds (), arm_weights
300- )
301- else :
302- df = _add_noise (
303- df = df , noise_stds = self .get_noise_stds (), arm_weights = arm_weights
304- )
240+ # Use the Noise object to add noise to the ground-truth evaluations
241+ df = self .noise .add_noise (
242+ df = df ,
243+ trial = trial ,
244+ outcome_names = self .outcome_names ,
245+ arm_weights = arm_weights ,
246+ )
305247 df ["trial_index" ] = trial .index
306248 df .drop (columns = ["Y_true" ], inplace = True )
307249 df ["metric_signature" ] = df ["metric_name" ]
0 commit comments