diff --git a/ax/modelbridge/transforms/relativize.py b/ax/modelbridge/transforms/relativize.py index 808a2d8e3fc..8b9a2b66b9b 100644 --- a/ax/modelbridge/transforms/relativize.py +++ b/ax/modelbridge/transforms/relativize.py @@ -11,7 +11,7 @@ from abc import ABC, abstractmethod from math import sqrt -from typing import Callable, Optional, TYPE_CHECKING +from typing import Callable, Optional, Tuple, TYPE_CHECKING, Union import numpy as np from ax.core.observation import Observation, ObservationData, ObservationFeatures @@ -47,8 +47,6 @@ class BaseRelativize(Transform, ABC): appropriate transform/untransform differently. """ - MISSING_STATUS_QUO_ERROR = "Cannot relativize data without status quo data" - def __init__( self, search_space: Optional[SearchSpace] = None, @@ -56,7 +54,8 @@ def __init__( modelbridge: Optional[modelbridge_module.base.ModelBridge] = None, config: Optional[TConfig] = None, ) -> None: - assert observations is not None, "Relativize requires observations" + cls_name = self.__class__.__name__ + assert observations is not None, f"{cls_name} requires observations" super().__init__( search_space=search_space, observations=observations, @@ -65,9 +64,18 @@ def __init__( ) # self.modelbridge should NOT be modified self.modelbridge: ModelBridge = not_none( - modelbridge, "Relativize transform requires a modelbridge" + modelbridge, f"{cls_name} transform requires a modelbridge" ) + self.status_quo_data_by_trial: dict[int, ObservationData] = not_none( + self.modelbridge.status_quo_data_by_trial, + f"{cls_name} requires status quo data.", + ) + # use latest index of latest observed trial by default + # to handle pending trials, which may not have a trial_index + # if TrialAsTask was not used to generate the trial. + self.default_trial_idx: int = max(self.status_quo_data_by_trial.keys()) + @property @abstractmethod def control_as_constant(self) -> bool: @@ -158,42 +166,36 @@ def untransform_observations( observations=observations, rel_op=unrelativize ) - def _rel_op_on_observations( + def _get_relative_data_from_obs( self, - observations: list[Observation], + obs: Observation, rel_op: Callable[..., tuple[np.ndarray, np.ndarray]], - ) -> list[Observation]: - - sq_data_by_trial: dict[int, ObservationData] = not_none( - self.modelbridge.status_quo_data_by_trial, self.MISSING_STATUS_QUO_ERROR + ) -> ObservationData: + idx = ( + int(obs.features.trial_index) + if obs.features.trial_index is not None + else self.default_trial_idx ) - - # use latest index of latest observed trial by default - # to handle pending trials, which may not have a trial_index - # if TrialAsTask was not used to generate the trial. - default_trial_idx: int = max(sq_data_by_trial.keys()) - - def _get_relative_data_from_obs( - obs: Observation, - rel_op: Callable[..., tuple[np.ndarray, np.ndarray]], - ) -> ObservationData: - idx = ( - int(obs.features.trial_index) - if obs.features.trial_index is not None - else default_trial_idx - ) - if idx not in sq_data_by_trial: - raise ValueError(self.MISSING_STATUS_QUO_ERROR) - return self._get_relative_data( - data=obs.data, - status_quo_data=sq_data_by_trial[idx], - rel_op=rel_op, + if idx not in self.status_quo_data_by_trial: + raise ValueError( + f"{self.__class__.__name__} requires status quo data for trial " + f"index {idx}." ) + return self._get_relative_data( + data=obs.data, + status_quo_data=self.status_quo_data_by_trial[idx], + rel_op=rel_op, + ) + def _rel_op_on_observations( + self, + observations: list[Observation], + rel_op: Callable[..., tuple[np.ndarray, np.ndarray]], + ) -> list[Observation]: return [ Observation( features=obs.features, - data=_get_relative_data_from_obs(obs, rel_op), + data=self._get_relative_data_from_obs(obs, rel_op), arm_name=obs.arm_name, ) for obs in observations @@ -225,37 +227,59 @@ def _get_relative_data( covariance=np.zeros((L, L)), ) for i, metric in enumerate(data.metric_names): - try: - j = next( - k for k in range(L) if status_quo_data.metric_names[k] == metric - ) - except (IndexError, StopIteration): - raise ValueError( - "Relativization cannot be performed because " - "ObservationData for status quo is missing metrics" - ) - + j = get_metric_index(data=status_quo_data, metric_name=metric) means_t = data.means[i] sems_t = sqrt(data.covariance[i][i]) mean_c = status_quo_data.means[j] sem_c = sqrt(status_quo_data.covariance[j][j]) - # if the is the status quo - if means_t == mean_c and sems_t == sem_c: - means_rel, sems_rel = 0, 0 - else: - means_rel, sems_rel = rel_op( - means_t=means_t, - sems_t=sems_t, - mean_c=mean_c, - sem_c=sem_c, - as_percent=True, - control_as_constant=self.control_as_constant, - ) + means_rel, sems_rel = self._get_rel_mean_sem( + means_t=means_t, + sems_t=sems_t, + mean_c=mean_c, + sem_c=sem_c, + metric=metric, + rel_op=rel_op, + ) result.means[i] = means_rel result.covariance[i][i] = sems_rel**2 return result + def _get_rel_mean_sem( + self, + means_t: float, + sems_t: float, + mean_c: float, + sem_c: float, + metric: str, + rel_op: Callable[..., tuple[np.ndarray, np.ndarray]], + ) -> Tuple[Union[float, np.ndarray], Union[float, np.ndarray]]: + """Compute (un)relativized mean and sem for a single metric.""" + # if the is the status quo + if means_t == mean_c and sems_t == sem_c: + return 0, 0 + return rel_op( + means_t=means_t, + sems_t=sems_t, + mean_c=mean_c, + sem_c=sem_c, + as_percent=True, + control_as_constant=self.control_as_constant, + ) + + +def get_metric_index(data: ObservationData, metric_name: str) -> int: + """Get the index of a metric in the ObservationData.""" + try: + return next( + k for k, name in enumerate(data.metric_names) if name == metric_name + ) + except (IndexError, StopIteration): + raise ValueError( + "Relativization cannot be performed because " + "ObservationData for status quo is missing metrics" + ) + class Relativize(BaseRelativize): """ diff --git a/ax/modelbridge/transforms/tests/test_relativize_transform.py b/ax/modelbridge/transforms/tests/test_relativize_transform.py index c11548904e0..778c35544ae 100644 --- a/ax/modelbridge/transforms/tests/test_relativize_transform.py +++ b/ax/modelbridge/transforms/tests/test_relativize_transform.py @@ -6,6 +6,7 @@ # pyre-strict from copy import deepcopy +from typing import List, Tuple, Type from unittest.mock import Mock import numpy as np @@ -44,10 +45,42 @@ class RelativizeDataTest(TestCase): + relativize_classes: List[Type[Transform]] = [ + Relativize, + RelativizeWithConstantControl, + ] + cases: List[Tuple[Type[Transform], List[Tuple[np.ndarray, np.ndarray]]]] = [ + ( + Relativize, + [ + (np.array([0.0, 0.0]), np.array([[0.0, 0.0], [0.0, 0.0]])), + ( + np.array([-22.56, 98.01652893]), + np.array([[604.8, 0.0], [0.0, 512.39669421]]), + ), + (np.array([0.0, 0.0]), np.array([[0.0, 0.0], [0.0, 0.0]])), + (np.array([-51.25, 98.4]), np.array([[812.5, 0.0], [0.0, 480.0]])), + ], + ), + ( + RelativizeWithConstantControl, + [ + (np.array([0.0, 0.0]), np.array([[0.0, 0.0], [0.0, 0.0]])), + ( + np.array([-20.0, 100.0]), + np.array([[400.0, 0.0], [0.0, 115.70247934]]), + ), + (np.array([0.0, 0.0]), np.array([[0.0, 0.0], [0.0, 0.0]])), + (np.array([-50.0, 100.0]), np.array([[750.0, 0.0], [0.0, 160.0]])), + ], + ), + ] + def test_relativize_transform_requires_a_modelbridge(self) -> None: - for relativize_cls in [Relativize, RelativizeWithConstantControl]: + for relativize_cls in self.relativize_classes: with self.assertRaisesRegex( - ValueError, "Relativize transform requires a modelbridge" + ValueError, + f"{relativize_cls.__name__} transform requires a modelbridge", ): relativize_cls( search_space=None, @@ -57,12 +90,12 @@ def test_relativize_transform_requires_a_modelbridge(self) -> None: def test_relativize_transform_requires_a_modelbridge_to_have_status_quo_data( self, ) -> None: - for relativize_cls in [Relativize, RelativizeWithConstantControl]: + for relativize_cls in self.relativize_classes: # modelbridge has no status quo sobol = Models.SOBOL(search_space=get_search_space()) self.assertIsNone(sobol.status_quo) with self.assertRaisesRegex( - ValueError, "Cannot relativize data without status quo data" + ValueError, f"{relativize_cls.__name__} requires status quo data." ): relativize_cls( search_space=None, @@ -110,6 +143,15 @@ def test_relativize_transform_requires_a_modelbridge_to_have_status_quo_data( self.assertEqual( mean_in_data, not_none(modelbridge.status_quo_data_by_trial)[0].means[0] ) + # reset SQ + not_none(exp._status_quo)._parameters["x1"] = 0.0 + modelbridge = ModelBridge( + search_space=exp.search_space, + model=Model(), + transforms=[relativize_cls], + experiment=exp, + data=data, + ) # create a new experiment new_exp = get_branin_experiment( @@ -143,7 +185,7 @@ def test_relativize_transform_requires_a_modelbridge_to_have_status_quo_data( mb_sq = not_none(modelbridge._status_quo) mb_sq.arm_name = None self.assertIsNotNone(modelbridge.status_quo_data_by_trial) - + self.assertEqual(len(modelbridge.status_quo_data_by_trial), 1) # test transform edge cases observations = observations_from_data( experiment=exp, @@ -180,16 +222,33 @@ def _check_transform_observations( ) # Check untransform untsfm_results = tf.untransform_observations(results) - for i, untsfm_obs in enumerate(untsfm_results): - obs = observations[i] + j = 0 + for untsfm_obs in untsfm_results: + obs = observations[j] + # skip status quo for the non-target trial since that + # is removed when transforming observations + if untsfm_obs.arm_name != obs.arm_name: + j += 1 + obs = observations[j] self.assertTrue(np.allclose(untsfm_obs.data.means, obs.data.means)) self.assertTrue( np.allclose(untsfm_obs.data.covariance, obs.data.covariance) ) + j += 1 metric_names = ["foobar", "foobaz"] - arm_names = ["status_quo", "0_0"] + arm_names = ["status_quo", "0_0", "status_quo", "1_0"] obs_data = [ + ObservationData( + metric_names=metric_names, + means=np.array([2.5, 5.5]), + covariance=np.array([[0.2, 0.0], [0.0, 0.3]]), + ), + ObservationData( + metric_names=metric_names, + means=np.array([2.0, 11.0]), + covariance=np.array([[0.25, 0.0], [0.0, 0.35]]), + ), ObservationData( metric_names=metric_names, means=np.array([2.0, 5.0]), @@ -204,32 +263,19 @@ def _check_transform_observations( obs_features = [ ObservationFeatures(parameters={"x": 1}, trial_index=0), ObservationFeatures(parameters={"x": 2}, trial_index=0), + ObservationFeatures(parameters={"x": 1}, trial_index=1), + ObservationFeatures(parameters={"x": 3}, trial_index=1), ] observations = recombine_observations(obs_features, obs_data, arm_names) modelbridge = Mock( status_quo=Mock( - data=obs_data[0], features=obs_features[0], arm_name=arm_names[0] + data=obs_data[2], features=obs_features[2], arm_name=arm_names[2] ), - status_quo_data_by_trial={0: obs_data[0]}, + status_quo_data_by_trial={0: obs_data[0], 1: obs_data[2]}, ) - for relativize_cls, expected_mean_and_covar in [ - ( - Relativize, - [ - (np.array([0.0, 0.0]), np.array([[0.0, 0.0], [0.0, 0.0]])), - (np.array([-51.25, 98.4]), np.array([[812.5, 0.0], [0.0, 480.0]])), - ], - ), - ( - RelativizeWithConstantControl, - [ - (np.array([0.0, 0.0]), np.array([[0.0, 0.0], [0.0, 0.0]])), - (np.array([-50.0, 100.0]), np.array([[750.0, 0.0], [0.0, 160.0]])), - ], - ), - ]: + for relativize_cls, expected_mean_and_covar in self.cases: tf = relativize_cls( search_space=None, observations=observations, @@ -242,25 +288,30 @@ def _check_transform_observations( expected_mean_and_covar=expected_mean_and_covar, ) # transform should still work when trial_index is None - modelbridge = Mock( - status_quo=Mock( - data=obs_data[0], features=obs_features[0], arm_name=arm_names[0] - ), - status_quo_data_by_trial={0: obs_data[1], 1: obs_data[0]}, - ) - tf = relativize_cls( - search_space=None, - observations=observations, - modelbridge=modelbridge, - ) - for obs in observations: - obs.features.trial_index = None - _check_transform_observations( - tf=tf, - observations=observations, - expected_mean_and_covar=expected_mean_and_covar, - ) + if relativize_cls in [RelativizeWithConstantControl, Relativize]: + modelbridge = Mock( + status_quo=Mock( + data=obs_data[2], + features=obs_features[2], + arm_name=arm_names[2], + ), + status_quo_data_by_trial={0: obs_data[0], 1: obs_data[2]}, + ) + tf = relativize_cls( + search_space=None, + observations=observations, + modelbridge=modelbridge, + ) + observations2 = deepcopy(observations) + for obs in observations2: + obs.features.trial_index = None + _check_transform_observations( + tf=tf, + observations=observations2[2:4], + expected_mean_and_covar=expected_mean_and_covar[2:4], + ) + def test_bad_relativize(self) -> None: # Check instantiation and subclassing of BaseRelativize class BadRelativize(BaseRelativize): pass @@ -269,8 +320,8 @@ class BadRelativize(BaseRelativize): with self.assertRaisesRegex(TypeError, "Can't instantiate abstract class"): abstract_cls( search_space=None, - observations=observations, - modelbridge=modelbridge, + observations=None, + modelbridge=None, ) # pyre-fixme[56]: Pyre was not able to infer the type of argument @@ -419,6 +470,7 @@ def setUp(self) -> None: features=ObservationFeatures(parameters=gr.arms[0].parameters) ), ) + self.model.status_quo_data_by_trial = {0: None} def test_transform_optimization_config_without_constraints(self) -> None: for relativize_cls in [Relativize, RelativizeWithConstantControl]: diff --git a/ax/modelbridge/transforms/tests/test_transform_to_new_sq.py b/ax/modelbridge/transforms/tests/test_transform_to_new_sq.py new file mode 100644 index 00000000000..f74ff7e7043 --- /dev/null +++ b/ax/modelbridge/transforms/tests/test_transform_to_new_sq.py @@ -0,0 +1,125 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +# pyre-strict + +from typing import List, Tuple, Type + +import numpy as np +from ax.core.batch_trial import BatchTrial +from ax.modelbridge import ModelBridge +from ax.modelbridge.transforms.base import Transform +from ax.modelbridge.transforms.tests.test_relativize_transform import RelativizeDataTest +from ax.modelbridge.transforms.transform_to_new_sq import TransformToNewSQ +from ax.models.base import Model +from ax.utils.common.testutils import TestCase +from ax.utils.common.typeutils import checked_cast +from ax.utils.testing.core_stubs import ( + get_branin_data_batch, + get_branin_experiment, + get_branin_optimization_config, +) + + +class TransformToNewSQTest(RelativizeDataTest): + # pyre-ignore [15]: `relativize_classes` overrides attribute + # defined in `RelativizeDataTest` inconsistently. Type `List + # [Type[TransformToNewSQ]]` is not a subtype of the + # overridden attribute `List[Type[Transform]]` + relativize_classes = [TransformToNewSQ] + cases: List[Tuple[Type[Transform], List[Tuple[np.ndarray, np.ndarray]]]] = [ + ( + TransformToNewSQ, + [ + ( + np.array([-38.0, 505.0]), + np.array([[1600.0, 0.0], [0.0, 2892.56198347]]), + ), + (np.array([2.0, 5.0]), np.array([[0.1, 0.0], [0.0, 0.2]])), + (np.array([1.0, 10.0]), np.array([[0.3, 0.0], [0.0, 0.4]])), + ], + ) + ] + + # these tests are defined by RelativizeDataTest, but it is irrelevant + # for TransformToNewSQ, so we don't need to run it here. + def test_bad_relativize(self) -> None: + pass + + def test_transform_status_quos_always_zero(self) -> None: + pass + + +class TransformToNewSQSpecificTest(TestCase): + def setUp(self) -> None: + self.exp = get_branin_experiment( + with_batch=True, + with_status_quo=True, + ) + for t in self.exp.trials.values(): + t.mark_running(no_runner_required=True) + self.exp.attach_data( + get_branin_data_batch(batch=checked_cast(BatchTrial, t)) + ) + t.mark_completed() + self.data = self.exp.fetch_data() + + self.modelbridge = ModelBridge( + search_space=self.exp.search_space, + model=Model(), + experiment=self.exp, + data=self.data, + status_quo_name="status_quo", + ) + + def test_modelbridge_without_status_quo(self) -> None: + self.modelbridge._status_quo = None + + with self.assertRaisesRegex( + ValueError, "Status quo must be set on modelbridge for TransformToNewSQ." + ): + TransformToNewSQ( + search_space=None, + observations=[], + modelbridge=self.modelbridge, + ) + + def test_transform_optimization_config(self) -> None: + tf = TransformToNewSQ( + search_space=None, + observations=[], + modelbridge=self.modelbridge, + ) + oc = get_branin_optimization_config() + new_oc = tf.transform_optimization_config(optimization_config=oc) + self.assertIs(new_oc, oc) + + def test_untransform_outcome_constraints(self) -> None: + tf = TransformToNewSQ( + search_space=None, + observations=[], + modelbridge=self.modelbridge, + ) + oc = get_branin_optimization_config() + new_outcome_constraints = tf.untransform_outcome_constraints( + outcome_constraints=oc.outcome_constraints + ) + self.assertIs(new_outcome_constraints, oc.outcome_constraints) + + def test_custom_target_trial(self) -> None: + tf = TransformToNewSQ( + search_space=None, + observations=[], + modelbridge=self.modelbridge, + ) + self.assertEqual(tf.default_trial_idx, 0) + + tf = TransformToNewSQ( + search_space=None, + observations=[], + modelbridge=self.modelbridge, + config={"target_trial_index": 1}, + ) + self.assertEqual(tf.default_trial_idx, 1) diff --git a/ax/modelbridge/transforms/transform_to_new_sq.py b/ax/modelbridge/transforms/transform_to_new_sq.py new file mode 100644 index 00000000000..7d609eb0ebc --- /dev/null +++ b/ax/modelbridge/transforms/transform_to_new_sq.py @@ -0,0 +1,193 @@ +#!/usr/bin/env python3 +# Copyright (c) Meta Platforms, Inc. and affiliates. +# +# This source code is licensed under the MIT license found in the +# LICENSE file in the root directory of this source tree. + +# pyre-strict + +from __future__ import annotations + +from math import sqrt +from typing import Callable, Optional, Tuple, TYPE_CHECKING + +import numpy as np +from ax.core.observation import Observation, ObservationData, ObservationFeatures +from ax.core.optimization_config import OptimizationConfig +from ax.core.outcome_constraint import OutcomeConstraint +from ax.core.search_space import SearchSpace +from ax.modelbridge.transforms.relativize import BaseRelativize, get_metric_index +from ax.models.types import TConfig +from ax.utils.common.typeutils import checked_cast, not_none +from ax.utils.stats.statstools import relativize, unrelativize + +if TYPE_CHECKING: + # import as module to make sphinx-autodoc-typehints happy + from ax import modelbridge as modelbridge_module # noqa F401 + + +class TransformToNewSQ(BaseRelativize): + """Map relative values of one batch to SQ of another. + + Will compute the relative metrics for each arm in each batch, and will then turn + those back into raw metrics but using the status quo values set on the Modelbridge. + + This is useful if batches are comparable on a relative scale, but + have offset in their status quo. This is often approximately true for online + experiments run in separate batches. + + Note that relativization is done using the delta method, so it will not + simply be the ratio of the means.""" + + def __init__( + self, + search_space: Optional[SearchSpace] = None, + observations: Optional[list[Observation]] = None, + modelbridge: Optional[modelbridge_module.base.ModelBridge] = None, + config: Optional[TConfig] = None, + ) -> None: + super().__init__( + search_space=search_space, + observations=observations, + modelbridge=modelbridge, + config=config, + ) + self.status_quo: Observation = not_none( + self.modelbridge.status_quo, + f"Status quo must be set on modelbridge for {self.__class__.__name__}.", + ) + if config is not None: + target_trial_index = config.get("target_trial_index") + if target_trial_index is not None: + self.default_trial_idx: int = checked_cast(int, target_trial_index) + + @property + def control_as_constant(self) -> bool: + """Whether or not the control is treated as a constant in the model.""" + return True + + def transform_optimization_config( + self, + optimization_config: OptimizationConfig, + modelbridge: Optional[modelbridge_module.base.ModelBridge] = None, + fixed_features: Optional[ObservationFeatures] = None, + ) -> OptimizationConfig: + return optimization_config + + def untransform_outcome_constraints( + self, + outcome_constraints: list[OutcomeConstraint], + fixed_features: Optional[ObservationFeatures] = None, + ) -> list[OutcomeConstraint]: + return outcome_constraints + + def _get_relative_data_from_obs( + self, + obs: Observation, + rel_op: Callable[..., tuple[np.ndarray, np.ndarray]], + ) -> ObservationData: + idx = ( + int(obs.features.trial_index) + if obs.features.trial_index is not None + else self.default_trial_idx + ) + if idx == self.default_trial_idx: + # don't transform data from target batch + return obs.data + return super()._get_relative_data_from_obs( + obs=obs, + rel_op=rel_op, + ) + + def _rel_op_on_observations( + self, + observations: list[Observation], + rel_op: Callable[..., tuple[np.ndarray, np.ndarray]], + ) -> list[Observation]: + rel_observations = super()._rel_op_on_observations( + observations=observations, + rel_op=rel_op, + ) + return [ + obs + for obs in rel_observations + # drop SQ observations + if ( + obs.arm_name != self.status_quo.arm_name + or obs.features.trial_index == self.default_trial_idx + ) + ] + + def _get_relative_data( + self, + data: ObservationData, + status_quo_data: ObservationData, + rel_op: Callable[..., tuple[np.ndarray, np.ndarray]], + ) -> ObservationData: + r""" + Transform or untransform `data` based on `status_quo_data` based on `rel_op`. + + Args: + data: ObservationData object to relativize + status_quo_data: The status quo data associated with the specific trial + that `data` belongs to. + rel_op: relativize or unrelativize operator. + control_as_constant: if treating the control metric as constant + + Returns: + (un)transformed ObservationData + """ + L = len(data.metric_names) + result = ObservationData( + metric_names=data.metric_names, + # zeros are just to create the shape so values can be set by index + means=np.zeros(L), + covariance=np.zeros((L, L)), + ) + for i, metric in enumerate(data.metric_names): + j = get_metric_index(data=status_quo_data, metric_name=metric) + means_t = data.means[i] + sems_t = sqrt(data.covariance[i][i]) + mean_c = status_quo_data.means[j] + sem_c = sqrt(status_quo_data.covariance[j][j]) + means_rel, sems_rel = self._get_rel_mean_sem( + means_t=means_t, + sems_t=sems_t, + mean_c=mean_c, + sem_c=sem_c, + metric=metric, + rel_op=rel_op, + ) + result.means[i] = means_rel + result.covariance[i][i] = sems_rel**2 + return result + + def _get_rel_mean_sem( + self, + means_t: float, + sems_t: float, + mean_c: float, + sem_c: float, + metric: str, + rel_op: Callable[..., tuple[np.ndarray, np.ndarray]], + ) -> Tuple[float, float]: + """Compute (un)transformed mean and sem for a single metric.""" + target_status_quo_data = self.status_quo_data_by_trial[self.default_trial_idx] + j = get_metric_index(data=target_status_quo_data, metric_name=metric) + target_mean_c = target_status_quo_data.means[j] + abs_target_mean_c = np.abs(target_mean_c) + if rel_op == unrelativize: + means_t = (means_t - target_mean_c) / abs_target_mean_c + sems_t = sems_t / abs_target_mean_c + means_rel, sems_rel = rel_op( + means_t=means_t, + sems_t=sems_t, + mean_c=mean_c, + sem_c=sem_c, + as_percent=True, + control_as_constant=self.control_as_constant, + ) + if rel_op == relativize: + means_rel = means_rel * abs_target_mean_c + target_mean_c + sems_rel = sems_rel * abs_target_mean_c + return means_rel, sems_rel diff --git a/sphinx/source/modelbridge.rst b/sphinx/source/modelbridge.rst index d825f4f3989..0ec5e640366 100644 --- a/sphinx/source/modelbridge.rst +++ b/sphinx/source/modelbridge.rst @@ -384,6 +384,14 @@ Transforms :undoc-members: :show-inheritance: +`ax.modelbridge.transforms.transform\_to\_new\_sq` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. automodule:: ax.modelbridge.transforms.transform_to_new_sq + :members: + :undoc-members: + :show-inheritance: + `ax.modelbridge.transforms.trial\_as\_task` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~