77
88from collections .abc import Iterable , Mapping , Sequence
99from dataclasses import dataclass , field
10- from math import sqrt
1110from typing import Any
1211
1312import numpy as np
1615from ax .benchmark .benchmark_step_runtime_function import TBenchmarkStepRuntimeFunction
1716from ax .benchmark .benchmark_test_function import BenchmarkTestFunction
1817from ax .benchmark .benchmark_trial_metadata import BenchmarkTrialMetadata
18+ from ax .benchmark .noise import GaussianNoise , Noise
1919from ax .core .base_trial import BaseTrial , TrialStatus
2020from ax .core .batch_trial import BatchTrial
2121from ax .core .runner import Runner
2424from ax .runners .simulated_backend import SimulatedBackendRunner
2525from ax .utils .common .serialization import TClassDecoderRegistry , TDecoderRegistry
2626from ax .utils .testing .backend_simulator import BackendSimulator , BackendSimulatorOptions
27- from pyre_extensions import assert_is_instance
2827
2928
3029def _dict_of_arrays_to_df (
@@ -70,54 +69,6 @@ def _dict_of_arrays_to_df(
7069 return df
7170
7271
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-
12172def get_total_runtime (
12273 trial : BaseTrial ,
12374 step_runtime_function : TBenchmarkStepRuntimeFunction | None ,
@@ -140,7 +91,7 @@ class BenchmarkRunner(Runner):
14091 A Runner that produces both observed and ground-truth values.
14192
14293 Observed values equal ground-truth values plus noise, with the noise added
143- according to the standard deviations returned by `get_noise_stds()` .
94+ according to the `Noise` object provided .
14495
14596 This runner does require that every benchmark has a ground truth, which
14697 won't necessarily be true for real-world problems. Such problems fall into
@@ -162,8 +113,9 @@ class BenchmarkRunner(Runner):
162113 Args:
163114 test_function: A ``BenchmarkTestFunction`` from which to generate
164115 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.
116+ noise: A ``Noise`` object that determines how noise is added to the
117+ ground-truth evaluations. Defaults to noiseless
118+ (``GaussianNoise(noise_std=0.0)``).
167119 step_runtime_function: A function that takes in parameters
168120 (in ``TParameterization`` format) and returns the runtime of a step.
169121 max_concurrency: The maximum number of trials that can be running at a
@@ -176,25 +128,13 @@ class BenchmarkRunner(Runner):
176128 """
177129
178130 test_function : BenchmarkTestFunction
179- noise_std : float | Sequence [ float ] | Mapping [ str , float ] = 0.0
131+ noise : Noise = field ( default_factory = GaussianNoise )
180132 step_runtime_function : TBenchmarkStepRuntimeFunction | None = None
181133 max_concurrency : int = 1
182134 force_use_simulated_backend : bool = False
183135 simulated_backend_runner : SimulatedBackendRunner | None = field (init = False )
184136
185137 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."
196- )
197-
198138 use_simulated_backend = (
199139 (self .max_concurrency > 1 )
200140 or (self .step_runtime_function is not None )
@@ -239,22 +179,6 @@ def get_Y_true(self, params: Mapping[str, TParamValue]) -> npt.NDArray:
239179 return result [:, None ]
240180 return result
241181
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-
258182 def run (self , trial : BaseTrial ) -> dict [str , BenchmarkTrialMetadata ]:
259183 """Run the trial by evaluating its parameterization(s).
260184
@@ -293,15 +217,13 @@ def run(self, trial: BaseTrial) -> dict[str, BenchmarkTrialMetadata]:
293217 if isinstance (trial , BatchTrial )
294218 else None
295219 )
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- )
220+ # Use the Noise object to add noise to the ground-truth evaluations
221+ df = self .noise .add_noise (
222+ df = df ,
223+ trial = trial ,
224+ outcome_names = self .outcome_names ,
225+ arm_weights = arm_weights ,
226+ )
305227 df ["trial_index" ] = trial .index
306228 df .drop (columns = ["Y_true" ], inplace = True )
307229 df ["metric_signature" ] = df ["metric_name" ]
0 commit comments