From 3c51a8fd946982a611158602917b22410501d98e Mon Sep 17 00:00:00 2001 From: Jason Chow Date: Mon, 14 Oct 2024 11:11:09 -0700 Subject: [PATCH] Implement new parameter setting config style (#393) Summary: To support more granular parameter settings in the future, we need to change the config style to be able to separately handle settings for each parameter. Currently the only setting for each parameter are the lower and upper bounds, but we also implement parameter types here (which currently does nothing). This is technically not a breaking change. Even though we have changed all documentation to match the new style, it is possible to define parameter bounds the old way. This will throw a warning however as this causes all parameter-specific bounds to be ignored. Old tests in test_config.py are also modified to match the new style unless specifically testing for these changes. Other tests not in test_config.py are left unchanged (this was the case for the previous major config change where experiment was a section). Reviewed By: crasanders Differential Revision: D63679916 --- aepsych/config.py | 48 +- configs/ax_beta_regression_example.ini | 57 -- configs/ax_example.ini | 61 -- configs/ax_ordinal_exploration_example.ini | 70 -- configs/multi_outcome_example.ini | 43 - configs/nonmonotonic_optimization_example.ini | 13 +- configs/ordinal_exploration_example.ini | 12 +- configs/pairwise_al_example.ini | 13 +- configs/pairwise_opt_example.ini | 13 +- configs/parameter_settings_example.ini | 23 + configs/regression_example.ini | 12 +- configs/single_lse_example.ini | 13 +- docs/configs.md | 64 +- examples/Interactive_AEPsych.ipynb | 931 +++++++++--------- .../aepsych_config.ini | 32 +- .../data_collection_analysis_tutorial.ipynb | 52 +- tests/test_config.py | 341 ++++++- 17 files changed, 1018 insertions(+), 780 deletions(-) delete mode 100644 configs/ax_beta_regression_example.ini delete mode 100644 configs/ax_example.ini delete mode 100644 configs/ax_ordinal_exploration_example.ini delete mode 100644 configs/multi_outcome_example.ini create mode 100644 configs/parameter_settings_example.ini diff --git a/aepsych/config.py b/aepsych/config.py index a6d04af3a..a7a5b69bb 100644 --- a/aepsych/config.py +++ b/aepsych/config.py @@ -22,7 +22,6 @@ _T = TypeVar("_T") - class Config(configparser.ConfigParser): # names in these packages can be referred to by string name @@ -157,6 +156,26 @@ def update( if config_str is not None: self.read_string(config_str) + # Warn if ub/lb is defined in common section + if "ub" in self["common"] and "lb" in self["common"]: + warnings.warn( + "ub and lb have been defined in common section, ignoring parameter specific blocks, be very careful!" + ) + elif "parnames" in self["common"]: # it's possible to pass no parnames + par_names = self.getlist("common", "parnames", element_type=str, fallback = []) + lb = [None] * len(par_names) + ub = [None] * len(par_names) + for i, par_name in enumerate(par_names): + # Validate the parameter-specific block + self._check_param_settings(par_name) + + lb[i] = self[par_name]["lower_bound"] + ub[i] = self[par_name]["upper_bound"] + + self["common"]["lb"] = f"[{', '.join(lb)}]" + self["common"]["ub"] = f"[{', '.join(ub)}]" + + # Deprecation warning for "experiment" section if "experiment" in self: for i in self["experiment"]: @@ -191,6 +210,33 @@ def _str_to_obj(self, v: str, fallback_type: _T = str, warn: bool = True) -> obj warnings.warn(f'No known object "{v}"!') return fallback_type(v) + def _check_param_settings(self, param_name: str) -> None: + """Check parameter-specific blocks have the correct settings, raises a ValueError if not. + + Args: + param_name (str): Parameter block to check. + """ + # Check if the config block exists at all + if param_name not in self: + raise ValueError(f"Parameter {param_name} is missing its own config block.") + + param_block = self[param_name] + + # Checking if param_type is set + if "par_type" not in param_block: + raise ValueError(f"Parameter {param_name} is missing the param_type setting.") + + # Each parameter type has a different set of required settings + if param_block['par_type'] == "continuous": + # Check if bounds exist + if "lower_bound" not in param_block: + raise ValueError(f"Parameter {param_name} is missing the lower_bound setting.") + if "upper_bound" not in param_block: + raise ValueError(f"Parameter {param_name} is missing the upper_bound setting.") + else: + raise ValueError(f"Parameter {param_name} has an unsupported parameter type {param_block['par_type']}.") + + def __repr__(self): return f"Config at {hex(id(self))}: \n {str(self)}" diff --git a/configs/ax_beta_regression_example.ini b/configs/ax_beta_regression_example.ini deleted file mode 100644 index 9b1d3d6bc..000000000 --- a/configs/ax_beta_regression_example.ini +++ /dev/null @@ -1,57 +0,0 @@ -# The common section includes parameters and other info used by multiple parts of the server. -[common] -use_ax = True # Required to enable the new parameter features. - -stimuli_per_trial = 1 # The number of stimuli shown in each trial; currently the Ax backend only supports 1 -outcome_types = [percentage] # The type of response given by the participant; can be [binary] or [continuous]. - # Multiple outcomes will be supported in a future update. - -parnames = [par1, par2, par3] # Names of continuous parameters. -lb = [-1, -1, -1] # Lower bounds of the continuous parameters, in the same order as above. -ub = [1, 1, 1] # Upper bounds of the continuous parameter, in the same order as above. -par_constraints = [par1 >= par2] # Linear constrains placed on the continuous parameters - # Parameters with log_scale = True cannot be included here. - # Having lots of constraints may make trial generation slow. - -choice_parnames = [par4, par5] # Names of discrete parameters; the possible values of each are specified below. - -fixed_parnames = [par6, par7] # Names of fixed parameters, the values of which are specified below. These parameters - # always have the same value and are not modeled. - -strategy_names = [init_strat, opt_strat] # The strategies that will be used, corresponding to the named sections below - -# Configuration for the initialization strategy, which we use to gather initial points -# before we start doing model-based acquisition. -[init_strat] -generator = SobolGenerator # Start trial generation with sobol samples. -min_total_tells = 10 # Number of data points required to complete this strategy. For a real experiment, you will want - # 5-10 initialization trials per parameter. -[SobolGenerator] -seed = 1 # Random seed for the Sobol generator. If not specified, a random seed will be used. -scramble = True # Whether to scramble the Sobol sequence. If not specified, scrambling will be used. - -# Configuration for the optimization strategy, our model-based acquisition strategy. -[opt_strat] -generator = OptimizeAcqfGenerator # after sobol, do model-based active-learning -min_total_tells = 12 # Finish the experiment after 2 total data points have been collected under opt_strat. Depending on how noisy - # your problem is, you may need several dozen points per parameter to get an accurate model. -acqf = qNoisyExpectedImprovement # The acquisition function to be used with the model. We recommend - # qNoisyExpectedImprovement for optimization problems. -model = BetaRegressionGP - -[par4] -choices = [a, b] # Possible values for the discrete parameter, par4. By default, no ordering is assumed. -is_ordered = False # Indicates that the choices for par4 are not ordered. - -[par5] -choices = [low, med, high] # Possible values for the discrete parameter, par5. -is_ordered = True # Indicates that the choices for par5 are ordered such that low < med < high. - -[par3] -value_type = int - -[par6] -value = 0 # Value of the fixed parameter, par6. Can be a float or string. - -[par7] -value = placeholder # Value of the fixed parameter, par7. Can be a float or string. diff --git a/configs/ax_example.ini b/configs/ax_example.ini deleted file mode 100644 index a39bb3a26..000000000 --- a/configs/ax_example.ini +++ /dev/null @@ -1,61 +0,0 @@ -# The common section includes parameters and other info used by multiple parts of the server. -[common] -use_ax = True # Required to enable the new parameter features. - -random_seed = 123 # The random seed used for reproducibility. Delete this line if you would like the experiment to be - # fully randomized each time it is run. - -stimuli_per_trial = 1 # The number of stimuli shown in each trial; currently the Ax backend only supports 1 -outcome_types = [continuous] # The type of response given by the participant; can be [binary] or [continuous]. - # Multiple outcomes will be supported in a future update. - -parnames = [par1, par2, par3] # Names of continuous parameters. -lb = [0, 0, 1] # Lower bounds of the continuous parameters, in the same order as above. -ub = [3, 3, 4] # Upper bounds of the continuous parameter, in the same order as above. -par_constraints = [par1 >= par2] # Linear constrains placed on the continuous parameters - # Parameters with log_scale = True cannot be included here. - # Having lots of constraints may make trial generation slow. - -choice_parnames = [par4, par5] # Names of discrete parameters; the possible values of each are specified below. - -fixed_parnames = [par6, par7] # Names of fixed parameters, the values of which are specified below. These parameters - # always have the same value and are not modeled. - -strategy_names = [init_strat, opt_strat] # The strategies that will be used, corresponding to the named sections below - -# Configuration for the initialization strategy, which we use to gather initial points -# before we start doing model-based acquisition. -[init_strat] -generator = SobolGenerator # Start trial generation with sobol samples. -min_total_tells = 2 # Number of data points required to complete this strategy. For a real experiment, you will want - # 5-10 initialization trials per parameter. - -# Configuration for the optimization strategy, our model-based acquisition strategy. -[opt_strat] -generator = OptimizeAcqfGenerator # after sobol, do model-based active-learning -min_total_tells = 3 # Finish the experiment after 3 total data points have been collected. Depending on how noisy - # your problem is, you may need several dozen points per parameter to get an accurate model. -acqf = qNoisyExpectedImprovement # The acquisition function to be used with the model. We recommend - # qNoisyExpectedImprovement for optimization problems. -model = ContinuousRegressionGP # Basic model for continuous outcomes. - -[par3] -log_scale = True # Indicates that par4 should be searched in log space. This is useful when percentage increases are - # more important than absolute increases, i.e., the difference from 1 to 2 is greater than 10 to 11. -value_type = int # Specifies that par1 can only take integer values. - -[par4] -choices = [a, b] # Possible values for the discrete parameter, par4. By default, no ordering is assumed. - -[par5] -choices = [low, med, high] # Possible values for the discrete parameter, par5. -is_ordered = True # Indicates that the choices for par5 are ordered such that low < med < high. - -[par6] -value = 123 # Value of the fixed parameter, par6. Can be a float or string. - -[par7] -value = placeholder # Value of the fixed parameter, par7. Can be a float or string. - -[OptimizeAcqfGenerator] -max_gen_time = 0.1 diff --git a/configs/ax_ordinal_exploration_example.ini b/configs/ax_ordinal_exploration_example.ini deleted file mode 100644 index 505f3638b..000000000 --- a/configs/ax_ordinal_exploration_example.ini +++ /dev/null @@ -1,70 +0,0 @@ -### Example config for ordinal (likert) data -# Assuming you are learning a latent value from k-point scores, the -# only things that need to be changed for a -# typical experiment are: -# 1. parnames, lb and ub under [common], and optionally target. -# 2. min_asks under init_strat -# 3. n_levels under OrdinalLikelihood - -## The common section includes global server parameters and parameters -[common] -use_ax = True # Required to enable the new parameter features. -parnames = [par1, par2, par3] # Names of continuous parameters. -lb = [0, 0, 1] # Lower bounds of the continuous parameters, in the same order as above. -ub = [3, 3, 4] # Upper bounds of the continuous parameter, in the same order as above. -par_constraints = [par1 >= par2] # Linear constrains placed on the continuous parameters - # Parameters with log_scale = True cannot be included here. - # Having lots of constraints may make trial generation slow. - -choice_parnames = [par4, par5] # Names of discrete parameters; the possible values of each are specified below. - -fixed_parnames = [par6, par7] # Names of fixed parameters, the values of which are specified below. These parameters - # always have the same value and are not modeled. - -stimuli_per_trial = 1 # the number of stimuli shown in each trial; 1 for single, or 2 for pairwise experiments -outcome_types = [ordinal] -strategy_names = [init_strat, opt_strat] # The strategies that will be used, corresponding to the named sections below - -# Configuration for the initialization strategy, which we use to gather initial points -# before we start doing model-based acquisition -[init_strat] -min_total_tells = 2 # number of sobol trials to run -generator = SobolGenerator # The generator class used to generate new parameter values - -[opt_strat] -generator = OptimizeAcqfGenerator # after sobol, do model-based active-learning -min_total_tells = 3 # Finish the experiment after 3 total data points have been collected. Depending on how noisy - # your problem is, you may need several dozen points per parameter to get an accurate model. -acqf = qNoisyExpectedImprovement # The acquisition function to be used with the model. We recommend -model = OrdinalGP - -## OrdinalGP model settings. -[OrdinalGP] -# Number of inducing points for approximate inference. 100 is fine for 2d and overkill for 1d; -# for larger dimensions, scale this up. -inducing_size = 100 -# ordinal_mean_covar_factory has better defaults for the ordinal setting than the default factory, -mean_covar_factory = ordinal_mean_covar_factory -likelihood = OrdinalLikelihood - -[OrdinalLikelihood] -n_levels = 5 - - -[par3] -log_scale = True # Indicates that par4 should be searched in log space. This is useful when percentage increases are - # more important than absolute increases, i.e., the difference from 1 to 2 is greater than 10 to 11. -value_type = int # Specifies that par1 can only take integer values. - -[par4] -choices = [a, b] # Possible values for the discrete parameter, par4. By default, no ordering is assumed. - -[par5] -choices = [low, med, high] # Possible values for the discrete parameter, par5. -is_ordered = True # Indicates that the choices for par5 are ordered such that low < med < high. - -[par6] -value = 123 # Value of the fixed parameter, par6. Can be a float or string. - -[par7] -value = placeholder # Value of the fixed parameter, par7. Can be a float or string. diff --git a/configs/multi_outcome_example.ini b/configs/multi_outcome_example.ini deleted file mode 100644 index f99fc1dd7..000000000 --- a/configs/multi_outcome_example.ini +++ /dev/null @@ -1,43 +0,0 @@ -# The common section includes parameters and other info used by multiple parts of the server. -[common] -use_ax = True # Required to enable multiple outcomes. - -stimuli_per_trial = 1 # The number of stimuli shown in each trial; currently the Ax backend only supports 1 -outcome_types = [continuous, continuous] # The type of response given by the participant. - # Currently only 'continous' is supported for multiple outcomes. -outcome_names = [out1, out2] # Names of the outcomes, in the same order as above. If these are not explicitly specified, - # they will default to outcome_1, outcome_2, etc. - -parnames = [x1, x2] # Names of continuous parameters. -lb = [0, 0] # Lower bounds of the continuous parameters, in the same order as above. -ub = [1, 1] # Upper bounds of the continuous parameter, in the same order as above. - -strategy_names = [init_strat, opt_strat] # The strategies that will be used, corresponding to the named sections below. - -# Settings for the first outcome, out1 -[out1] -minimize = False # Set to True to minimize this outcome or False to maximize it. Defaults to False. -threshold = -18 # The worst possible value for this outcome that would be acceptable. Defaults to None, in which case - # heuristics will be used to infer a value (but explicitly setting this is preferable when possible). - -# Settings for the first outcome, out2 -[out2] -minimize = False # Set to True to minimize this outcome or False to maximize it. Defaults to False. -threshold = -6 # The worst possible value for this outcome that would be acceptable. Defaults to None, in which case - # heuristics will be used to infer a value (but explicitly setting this is preferable when possible). - -# Configuration for the initialization strategy, which we use to gather initial points -# before we start doing model-based acquisition. -[init_strat] -generator = SobolGenerator # Start trial generation with sobol samples. -min_total_tells = 2 # Number of data points required to complete this strategy. For a real experiment, you will want - # 5-10 initialization trials per parameter. - -# Configuration for the optimization strategy, our model-based acquisition strategy. -[opt_strat] -generator = OptimizeAcqfGenerator # After sobol, do model-based active-learning for multiple outcomes. -min_total_tells = 3 # Finish the experiment after 3 total data points have been collected. Depending on how noisy - # your problem is, you may need several dozen points per parameter to get an accurate model. -acqf = qNoisyExpectedHypervolumeImprovement # The acquisition function to be used with the model. We recommend - # qNoisyExpectedHypervolumeImprovement for multi-outcome optimization. -model = ContinuousRegressionGP # Basic model for continuous outcomes. diff --git a/configs/nonmonotonic_optimization_example.ini b/configs/nonmonotonic_optimization_example.ini index 72a53304a..8a12564c3 100644 --- a/configs/nonmonotonic_optimization_example.ini +++ b/configs/nonmonotonic_optimization_example.ini @@ -11,12 +11,21 @@ ## reused in multiple other classes [common] parnames = [par1, par2] # names of the parameters -lb = [0, 0] # lower bounds of the parameters, in the same order as above -ub = [1, 1] # upper bounds of parameter, in the same order as above stimuli_per_trial = 1 # the number of stimuli shown in each trial; 1 for single, or 2 for pairwise experiments outcome_types = [binary] # the type of response given by the participant; can be [binary] or [continuous] for single strategy_names = [init_strat, opt_strat] # The strategies that will be used, corresponding to the named sections below +# Parameter settings, blocks based on parameter names in [common] +[par1] +par_type = continuous +lower_bound = 0 # lower bound +upper_bound = 1 # upper bound + +[par2] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + # Configuration for the initialization strategy, which we use to gather initial points # before we start doing model-based acquisition [init_strat] diff --git a/configs/ordinal_exploration_example.ini b/configs/ordinal_exploration_example.ini index 0a5270c62..080305faa 100644 --- a/configs/ordinal_exploration_example.ini +++ b/configs/ordinal_exploration_example.ini @@ -10,12 +10,20 @@ ## reused in multiple other classes [common] parnames = [par1, par2] # names of the parameters -lb = [-1, -1] # lower bounds of the parameters, in the same order as above -ub = [1, 1] # upper bounds of parameter, in the same order as above stimuli_per_trial = 1 # the number of stimuli shown in each trial; 1 for single, or 2 for pairwise experiments outcome_types = [ordinal] strategy_names = [init_strat] # The strategies that will be used, corresponding to the named sections below +[par1] +par_type = continuous +lower_bound = -1 +upper_bound = 1 + +[par2] +par_type = continuous +lower_bound = -1 +upper_bound = 1 + # Configuration for the initialization strategy, which we use to gather initial points # before we start doing model-based acquisition [init_strat] diff --git a/configs/pairwise_al_example.ini b/configs/pairwise_al_example.ini index e6f4f7150..5c12670ac 100644 --- a/configs/pairwise_al_example.ini +++ b/configs/pairwise_al_example.ini @@ -9,12 +9,21 @@ ## reused in multiple other classes [common] parnames = [par1, par2] # names of the parameters -lb = [0, 0] # lower bounds of the parameters, in the same order as above -ub = [1, 1] # upper bounds of parameter, in the same order as above stimuli_per_trial = 2 # the number of stimuli shown in each trial; 1 for single, or 2 for pairwise experiments outcome_types = [binary] # the type of response given by the participant; can only be [binary] for pairwise for now strategy_names = [init_strat, opt_strat] # The strategies that will be used, corresponding to the named sections below +# Parameter settings, blocks based on parameter names in [common] +[par1] +par_type = continuous +lower_bound = 0 # lower bound +upper_bound = 1 # upper bound + +[par2] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + # Configuration for the initialization strategy, which we use to gather initial points # before we start doing model-based acquisition [init_strat] diff --git a/configs/pairwise_opt_example.ini b/configs/pairwise_opt_example.ini index 8fb15aa74..eed3bc968 100644 --- a/configs/pairwise_opt_example.ini +++ b/configs/pairwise_opt_example.ini @@ -9,12 +9,21 @@ ## reused in multiple other classes [common] parnames = [par1, par2] # names of the parameters -lb = [0, 0] # lower bounds of the parameters, in the same order as above -ub = [1, 1] # upper bounds of parameter, in the same order as above stimuli_per_trial = 2 # the number of stimuli shown in each trial; 1 for single, or 2 for pairwise experiments outcome_types = [binary] # the type of response given by the participant; can only be [binary] for pairwise for now strategy_names = [init_strat, opt_strat] # The strategies that will be used, corresponding to the named sections below +# Parameter settings, blocks based on parameter names in [common] +[par1] +par_type = continuous +lower_bound = 0 # lower bound +upper_bound = 1 # upper bound + +[par2] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + # Configuration for the initialization strategy, which we use to gather initial points # before we start doing model-based acquisition [init_strat] diff --git a/configs/parameter_settings_example.ini b/configs/parameter_settings_example.ini new file mode 100644 index 000000000..162126f6e --- /dev/null +++ b/configs/parameter_settings_example.ini @@ -0,0 +1,23 @@ +[common] +parnames = [contPar] # names of the parameters +stimuli_per_trial = 1 +outcome_types = [binary] +target = 0.75 +strategy_names = [init_strat, opt_strat] + +[contPar] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + +# Strategy blocks below +[init_strat] +min_total_tells = 10 +generator = SobolGenerator + +[opt_strat] +min_total_tells = 20 +refit_every = 5 +generator = OptimizeAcqfGenerator +acqf = MCLevelSetEstimation +model = GPClassificationModel diff --git a/configs/regression_example.ini b/configs/regression_example.ini index 211a441b0..24bfa43c2 100644 --- a/configs/regression_example.ini +++ b/configs/regression_example.ini @@ -11,13 +11,21 @@ ## reused in multiple other classes [common] parnames = [par1, par2] # names of the parameters -lb = [0, 0] # lower bounds of the parameters, in the same order as above -ub = [10, 10] # upper bounds of parameter, in the same order as above stimuli_per_trial = 1 # the number of stimuli shown in each trial; 1 for single, or 2 for pairwise experiments outcome_types = [continuous] # the type of response given by the participant; can be [binary] or [continuous] strategy_names = [init_strat, opt_strat] # The strategies that will be used, corresponding to the named sections below pregen_asks = True # The server will automatically generate new trial parameters in the background, saving time. +[par1] +par_type = continuous +lower_bound = 0 # lower bound +upper_bound = 10 # lower bound + +[par2] +par_type = continuous +lower_bound = 0 +upper_bound = 10 + # Configuration for the initialization strategy, which we use to gather initial points # before we start doing model-based acquisition [init_strat] diff --git a/configs/single_lse_example.ini b/configs/single_lse_example.ini index 63ab8677e..68a46f1ae 100644 --- a/configs/single_lse_example.ini +++ b/configs/single_lse_example.ini @@ -10,12 +10,21 @@ ## reused in multiple other classes [common] parnames = [par1, par2] # names of the parameters -lb = [0, 0] # lower bounds of the parameters, in the same order as above -ub = [1, 1] # upper bounds of parameter, in the same order as above stimuli_per_trial = 1 # the number of stimuli shown in each trial; 1 for single, or 2 for pairwise experiments outcome_types = [binary] # the type of response given by the participant; can be [binary] or [continuous] strategy_names = [init_strat, opt_strat] # The strategies that will be used, corresponding to the named sections below +# Parameter settings, blocks based on parameter names in [common] +[par1] +par_type = continuous +lower_bound = 0 # lower bound +upper_bound = 1 # upper bound + +[par2] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + # Configuration for the initialization strategy, which we use to gather initial points # before we start doing model-based acquisition [init_strat] diff --git a/docs/configs.md b/docs/configs.md index 146958fa5..a1e233a29 100644 --- a/docs/configs.md +++ b/docs/configs.md @@ -27,8 +27,6 @@ To be more concrete, we will break down [one of the example config files](https: ``` [common] parnames = [par1, par2] # names of the parameters -lb = [0, 0] # lower bounds of the parameters, in the same order as above -ub = [1, 1] # upper bounds of parameter, in the same order as above outcome_type = single_probit # we show a single stimulus and receive a binary outcome e strategy_names = [init_strat, opt_strat] # the names we give to our strategies ``` @@ -37,14 +35,31 @@ The first section in the file is the `common` section. All AEPsych config files **`parnames`**: This is a list of parameter names. This example uses the generic names par1 and par2, but you can name your parameters whatever you would like. Note that this list should be the same length as lb and ub. -**`lb`**: This is a list of numbers specifying the lower bounds of each of your parameters. - -**`ub`**: This is a list of numbers specifying the upper bounds of each of your parameters. - **`outcome_type`**: This is the type of outcome you will receive on each trial of your experiment. `single_probit` should be used for experiments where participants are shown a single stimulus and asked to make a binary choice, such as whether they detected the stimulus or not. `single_continuous` should be used for experiments where participants are shown a single stimulus and asked to provide a continous rating, such as how bright or loud the stimulus is. **`strategy_names`**: This is a list of the data-collection strategies you plan on using in your experiment. Each named strategy in this list will receive its own section later in the config where we can specify the settings we want. This example follows a typical AEPsych paradigm where we first sample points in a quasi-random way to initialize our model, then we search for the optimal points using the model. Therefore we specify two strategies: `init_strat` (the initialization strategy) and `opt_strat` (the optimization strategy). Note that we chose these names to be mnemonic, but we could have named them whatever we wanted. +

Parameter specific settings

+``` +[par1] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + +[par2] +par_type = continuous +lower_bound = 0 +upper_bound = 1 +``` + +Each parameter will have its own section based on the name defined in the `common` section. + +**`par_type`**: This is the type of parameter it is, for now, this should just always be continuous. + +**`lower_bound`**: This is the lower bound of this parameter. + +**`upper_bound`**: This is the upper bound of this parameter. +

init_strat

``` [init_strat] @@ -129,11 +144,19 @@ AEPsych configs allow you mix-and-match as many strategies as you would like, an ``` [common] parnames = [par1, par2] -lb = [0, 0] -ub = [1, 1] outcome_type = single_probit strategy_names = [sobol_strat, explore_strat, opt_strat] +[par1] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + +[par2] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + [sobol_strat] min_asks = 5 generator = SobolGenerator @@ -162,11 +185,30 @@ Some generators, such as `SobolGenerator` and `RandomGenerator`, do not use mode ``` [common] -parnames = [par1, par2, par3, par4, par5, par6, par7, par8, par9, par10, par11, par12] -lb = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] -ub = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] +parnames = [par1, par2, par3, par4] outcome_type = single_probit strategy_names = [sobol_strat] + +[par1] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + +[par2] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + +[par3] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + +[par4] +par_type = continuous +lower_bound = 0 +upper_bound = 1 + [sobol_strat] min_asks = 20 refit_every = 20 diff --git a/examples/Interactive_AEPsych.ipynb b/examples/Interactive_AEPsych.ipynb index 8fddd9ab7..557dd4a91 100644 --- a/examples/Interactive_AEPsych.ipynb +++ b/examples/Interactive_AEPsych.ipynb @@ -1,464 +1,473 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": { - "originalKey": "8dba0c9a-6265-49f5-a1e8-fffd4f862b89" - }, - "source": [ - "# What is this?\n", - "This notebook is an interactive interface for AEPsych, a Python package for adaptive experimetation in psychophysics and related domains. AEPsych utilizes active learning to efficiently explore parameter spaces, allowing experimenters to find just-noticeable-differences (or other quantities of interest) in far fewer trials than traditional methods. This notebook will allow you to use AEPsych without having to write any code." - ] - }, - { - "cell_type": "markdown", - "metadata": { - "originalKey": "2010f083-ea65-4c7f-a66e-41fadc3bcee0" - }, - "source": [ - "# Instructions\n", - "1. Run the codeblock below. You will see a set of widgets appear.\n", - "2. If you are resuming a previous session, you can use the \"Resume Session\" button to upload a saved .pkl file and resume with all of your settings and data intact.\n", - "3. If you are starting from scratch, use the widgets to change AEPsych's settings. Here is an explanation of the settings:\n", - "\n", - " **Strategy**: There are three strategies for exploring the parameter space:\n", - " \n", - " *Threshold Finding*: AEPsych will try to find the set of parameter values at which the outcome probability equals some target value.\n", - "\n", - " *Exploration*: AEPsych will try to model the outcome at every point in the parameter space.\n", - "\n", - " *Optimization*: AEPsych will try to find the parameter values that maximize the probability of an outcome of 1.\n", - "\n", - " **Threshold**: Sets the target value for the *Threshold Finding* strategy. It is ignored by the other strategies.\n", - "\n", - " **Initialization Trials**: Sets the number of initialization trials before the model-based strategy begins. Parameter values during these trials are generated quasi-randomly. After the model has been initialized, it will begin to choose parameter values according to the strategy you have picked. \n", - " \n", - " **Outcome Labels**: These determine the labels of your outcomes. Currently AEPsych only supports binary outcomes, so one outcome will be coded as a 0 in the data, and the other outcome will be coded as a 1. Pay attention to how you label your outcomes! The *Optimization* strategy and the *Monotonic* parameter settings depend on which outcome is labeled as a 1.\n", - " \n", - " **Parameters**: These settings control the parameter space that AEPsych will explore. Use the \"Add Parameter\" and \"Remove Parameter\" buttons to add or remove parameters to the experiment. For each parameter you can specify its name, bounds, and whether or not it should be monotonically increasing with the probability of an outcome of 1 (in other words, you can specify that increasing this parameter never decreases the probability of an outcome of 1). Currently AEPsych only supports continuous parameters.\n", - "\n", - "\n", - "4. Click the \"Start AEPsych\". A new set of widgets will appear.\n", - "5. You will see the set of parameters AEPsych recommends you try. To see a different set of parameters, click \"Next Parameters\". It may take a few seconds for the parameters to appear.\n", - "6. After testing the parameters, enter the outcome, and click \"Update Model\" to update the model and see the next set of recommended parameters. You can also enter data at any time with any parameter values; you are not restricted to only using the parameters that AEPsych recommends. \n", - "7. You can also upload data from files using the \"Upload Data\" button. The data should be stored in .csv files according to the following template: \n", - "\n", - "```\n", - "parametername1,parametername2,outcome\n", - "1.1,0.4,1\n", - "0.25,1,0\n", - "```\n", - "\n", - "8. After you enter data, a table containing each set of parameters and their outcome will appear. You can download this data by clicking the \"aepsych_data.csv\" link.\n", - "9. After the AEPsych model has been initialized, a plot of the model's posterior will appear to the right of the data table. Currently plotting only works for 1 or 2-dimensional problems.\n", - "10. To save your work, you can download the \"aepsych_server.pkl\" link at the top and upload it again later.\n", - "11. If you ever need to start over, simply rerun the code block.\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": { - "originalKey": "6db98caa-05fa-483b-8970-8c524e4b237d", - "scrolled": true - }, - "outputs": [], - "source": [ - "import io\n", - "import warnings\n", - "\n", - "import dill\n", - "import ipywidgets as widgets\n", - "import matplotlib.pyplot as plt\n", - "import pandas as pd\n", - "from aepsych.acquisition.monotonic_rejection import MonotonicMCLSE\n", - "from aepsych.plotting import plot_strat\n", - "from aepsych.server import AEPsychServer\n", - "from IPython.display import FileLink, clear_output, display\n", - "\n", - "plt.rcParams[\"figure.figsize\"] = (10, 10)\n", - "warnings.filterwarnings(\"ignore\")\n", - "\n", - "\n", - "server = AEPsychServer()\n", - "par_precision = 3\n", - "dim = 0\n", - "inducing_scale = 50\n", - "acq_dict = {\n", - " \"Exploration\": \"MonotonicMCPosteriorVariance\",\n", - " \"Optimization\": \"qNoisyExpectedImprovement\",\n", - " \"Threshold Finding\": \"MonotonicMCLSE\",\n", - "}\n", - "style = {\"description_width\": \"initial\"}\n", - "csv_file_name = \"aepsych_data.csv\"\n", - "strat_file_name = \"aepsych_server.pkl\"\n", - "\n", - "\n", - "def add_param(b):\n", - " global dim\n", - " dim += 1\n", - " hb = widgets.HBox(\n", - " [\n", - " widgets.Text(f\"par{dim}\", description=\"Name\", style=style),\n", - " widgets.FloatText(\n", - " 0.0, description=\"Lower Bound:\", step=10 ** -par_precision, style=style\n", - " ),\n", - " widgets.FloatText(\n", - " 1.0, description=\"Upper Bound:\", step=10 ** -par_precision, style=style\n", - " ),\n", - " widgets.Checkbox(value=False, description=\"Monotonic\"),\n", - " ]\n", - " )\n", - " params_boxes.children = tuple(list(params_boxes.children) + [hb])\n", - " pars = [child.children[0].value for child in params_boxes.children]\n", - " lbs = [child.children[1].value for child in params_boxes.children]\n", - " ubs = [child.children[2].value for child in params_boxes.children]\n", - "\n", - "\n", - "def rem_param(b):\n", - " global dim\n", - " if dim > 1:\n", - " dim -= 1\n", - " params_boxes.children = tuple(list(params_boxes.children[:-1]))\n", - "\n", - "\n", - "def start_server(b):\n", - " config = make_config()\n", - " server.configure(config_str=config)\n", - " server.one_outcome = one_outcome.value\n", - " server.zero_outcome = zero_outcome.value\n", - "\n", - " with data_output:\n", - " clear_output()\n", - "\n", - " with plot_output:\n", - " clear_output()\n", - " \n", - " tell_boxes.children = [\n", - " widgets.BoundedFloatText(\n", - " lb, description=par, min=lb, max=ub, step=10 ** -par_precision, style=style\n", - " )\n", - " for par, lb, ub in zip(server.parnames, server.strat.lb, server.strat.ub)\n", - " ]\n", - " outcome_box.options = [('', None), (zero_outcome.value, 0), (one_outcome.value, 1)]\n", - " clear_output()\n", - " display(server_download, params_cont, plot_data_cont)\n", - " get_next(None)\n", - "\n", - "\n", - "def resume_server(change):\n", - " global server\n", - " for name, csv in server_uploader.value.items():\n", - " with io.BytesIO(csv[\"content\"]) as f:\n", - " server = dill.load(f)\n", - " # When the server is pickled, it deletes these attributes.\n", - " # This is an ugly hack around that.\n", - " server.socket = None\n", - " server.db = None\n", - "\n", - " tell_boxes.children = [\n", - " widgets.BoundedFloatText(\n", - " lb,\n", - " description=par,\n", - " min=lb,\n", - " max=ub,\n", - " step=10 ** -par_precision,\n", - " style=style,\n", - " )\n", - " for par, lb, ub in zip(\n", - " server.parnames, server.strat.lb, server.strat.ub\n", - " )\n", - " ]\n", - " zero_outcome.value = server.zero_outcome\n", - " one_outcome.value = server.one_outcome\n", - " outcome_box.options = [('', None), (zero_outcome.value, 0), (one_outcome.value, 1)]\n", - " \n", - " clear_output()\n", - " display(server_download, params_cont, plot_data_cont)\n", - " display_data()\n", - " display_plot()\n", - " get_next(None)\n", - "\n", - " server_uploader.value.clear()\n", - "\n", - "\n", - "def make_config():\n", - " dim = len(params_boxes.children)\n", - " pars = [child.children[0].value for child in params_boxes.children]\n", - " parnames = f\"[{','.join(par for par in pars)}]\"\n", - " lbs = [child.children[1].value for child in params_boxes.children]\n", - " ubs = [child.children[2].value for child in params_boxes.children]\n", - " monotonic = [\n", - " i for i, child in enumerate(params_boxes.children) if child.children[3]\n", - " ]\n", - " target = threshold_box.value\n", - " n_sobol = n_sobol_box.value\n", - " acq = acq_dict[strategy_btns.value]\n", - " model = \"GPClassificationModel\" if acq == \"qNoisyExpectedImprovement\" else \"MonotonicRejectionGP\"\n", - " generator = \"OptimizeAcqfGenerator\" if acq == \"qNoisyExpectedImprovement\" else \"MonotonicRejectionGenerator\"\n", - "\n", - " config = f\"\"\"\n", - " [common]\n", - " parnames = {parnames}\n", - " lb = {lbs}\n", - " ub = {ubs}\n", - " outcome_type = single_probit\n", - " target = {target}\n", - " strategy_names = [init_strat, opt_strat]\n", - "\n", - " [init_strat]\n", - " n_trials = {n_sobol}\n", - " generator = SobolGenerator\n", - "\n", - " [opt_strat]\n", - " n_trials = -1\n", - " refit_every = 1\n", - " generator = {generator}\n", - "\n", - " [experiment]\n", - " acqf = {acq}\n", - " model = {model}\n", - " \n", - " [SobolGenerator]\n", - " n_points = {n_sobol}\n", - " \n", - " [GPClassificationModel]\n", - " inducing_size = {inducing_scale*dim} #TODO: find a better way to scale this\n", - "\n", - " [MonotonicRejectionGP]\n", - " inducing_size = {inducing_scale*dim} #TODO: find a better way to scale this\n", - " mean_covar_factory = monotonic_mean_covar_factory\n", - " monotonic_idxs = {monotonic}\n", - " \"\"\"\n", - " return config\n", - "\n", - "\n", - "def tell_model(b):\n", - " if outcome_box.value is not None:\n", - " with upload_output:\n", - " clear_output()\n", - " params = {child.description: child.value for child in tell_boxes.children}\n", - " outcome = outcome_box.value\n", - " server.tell(outcome, params)\n", - " for child in tell_boxes.children:\n", - " child.value = child.min\n", - " outcome_box.value = None\n", - " get_next(None)\n", - " display_data()\n", - " display_plot()\n", - " else:\n", - " with upload_output:\n", - " clear_output()\n", - " print(\"Select an outcome for this set of parameters!\")\n", - "\n", - "\n", - "def get_next(b):\n", - " tell_btn.disabled = True\n", - " ask_btn.disabled = True\n", - " uploader.disabled = True\n", - " outcome_box.disabled = True\n", - " for child in tell_boxes.children:\n", - " child.disabled = True\n", - "\n", - " if server.strat.x is None and server.strat._count >= n_sobol_box.value:\n", - " n_sobol_box.value = 1\n", - " config = make_config()\n", - " server.configure(config_str=config)\n", - " next_pars = server.ask()\n", - "\n", - " else:\n", - " next_pars = server.ask()\n", - "\n", - " for child, value in zip(tell_boxes.children, next_pars.values()):\n", - " child.value = round(value[0], par_precision)\n", - "\n", - " tell_btn.disabled = False\n", - " ask_btn.disabled = False\n", - " uploader.disabled = False\n", - " outcome_box.disabled = False\n", - " for child in tell_boxes.children:\n", - " child.disabled = False\n", - " write_server()\n", - "\n", - "\n", - "def write_server():\n", - " server_download.disabled = True\n", - " with open(strat_file_name, \"wb\") as f:\n", - " dill.dump(server, f)\n", - " server_download.disabled = False\n", - "\n", - "\n", - "def display_data():\n", - " if server.strat.x is not None:\n", - " data = {par: server.strat.x[:, i] for i, par in enumerate(server.parnames)}\n", - " data[\"outcome\"] = server.strat.y\n", - " data = pd.DataFrame(data)\n", - " data.to_csv(csv_file_name, index=False)\n", - " with data_output:\n", - " clear_output()\n", - " display(FileLink(csv_file_name), data)\n", - "\n", - "\n", - "def display_plot():\n", - " with plot_output:\n", - " clear_output()\n", - " if server.strat.dim <= 2:\n", - " if server.strat._strat_idx > 0:\n", - " xlabel = server.parnames[0]\n", - " ylabel = server.parnames[1] if server.strat.dim == 2 else None\n", - " yes_label = one_outcome.value\n", - " no_label = zero_outcome.value\n", - " acqf = server.strat._strat.generator.acqf\n", - " thresh = (\n", - " threshold_box.value\n", - " if acqf == MonotonicMCLSE\n", - " else None\n", - " )\n", - " plot_strat(\n", - " server.strat, xlabel=xlabel, ylabel=ylabel, target_level=thresh,\n", - " yes_label=yes_label, no_label=no_label\n", - " )\n", - " else:\n", - " print(\n", - " \"\\n\\n\\n\\n\\n Initializing model. Collect more data to plot posterior.\"\n", - " )\n", - " else:\n", - " print(\"Plotting currently only works for <=2D\")\n", - "\n", - "\n", - "def mass_tell(change):\n", - " for name, csv in uploader.value.items():\n", - " with io.BytesIO(csv[\"content\"]) as f:\n", - " try:\n", - " data = pd.read_csv(f)\n", - " for i, row in data.iterrows():\n", - " server.tell(\n", - " row[\"outcome\"], {par: row[par] for par in server.parnames}\n", - " )\n", - " idx = server.strat._strat_idx\n", - " server.strat.strat_list[idx]._count += 1\n", - " with upload_output:\n", - " clear_output()\n", - " get_next(None)\n", - " display_data()\n", - " display_plot()\n", - " except:\n", - " with upload_output:\n", - " clear_output()\n", - " print(\"Data is improperly formatted!\")\n", - " uploader.value.clear()\n", - " write_server()\n", - "\n", - "\n", - "server_uploader = widgets.FileUpload(\n", - " description=\"Resume Session\", accept=\".pkl\", multiple=False, style=style\n", - ")\n", - "server_uploader.observe(resume_server, names=\"_counter\")\n", - "\n", - "outcome_label = widgets.Label(value='Outcome Labels:')\n", - "zero_outcome = widgets.Text(\"No Trial\", description=\"0: \", style=style)\n", - "one_outcome = widgets.Text(\"Yes Trial\", description=\"1: \", style=style)\n", - "outcomes_labels = widgets.VBox([outcome_label, zero_outcome, one_outcome])\n", - "\n", - "params_label = widgets.Label(value=\"Parameters:\")\n", - "params_boxes = widgets.VBox([])\n", - "add_param(None)\n", - "\n", - "add_param_btn = widgets.Button(description=\"Add Parameter\")\n", - "add_param_btn.on_click(add_param)\n", - "\n", - "rem_param_btn = widgets.Button(description=\"Remove Parameter\")\n", - "rem_param_btn.on_click(rem_param)\n", - "\n", - "btns = widgets.HBox([add_param_btn, rem_param_btn])\n", - "\n", - "strategy_btns = widgets.RadioButtons(\n", - " options=[\"Threshold Finding\", \"Exploration\", \"Optimization\"],\n", - " value=\"Threshold Finding\",\n", - " description=\"Strategy:\",\n", - ")\n", - "\n", - "threshold_box = widgets.BoundedFloatText(\n", - " value=0.75, min=0, max=1.0, step=0.05, description=\"Threshold:\"\n", - ")\n", - "\n", - "n_sobol_box = widgets.BoundedIntText(\n", - " value=10, min=0, description=\"Initialization Trials:\", style=style\n", - ")\n", - "\n", - "start_server_btn = widgets.Button(description=\"Start AEPsych\")\n", - "start_server_btn.on_click(start_server)\n", - "\n", - "strat_settings = widgets.HBox([strategy_btns, threshold_box, n_sobol_box])\n", - "\n", - "config = make_config()\n", - "server.configure(config_str=config)\n", - "\n", - "tell_boxes = widgets.VBox()\n", - "outcome_box = widgets.Dropdown(\n", - " options=[('No Trial', 0), ('Yes Trial', 1), ('', None)],\n", - " value=None,\n", - " description='Outcome:',\n", - ")\n", - "\n", - "ask_btn = widgets.Button(description=\"Next Parameters\")\n", - "ask_btn.on_click(get_next)\n", - "\n", - "tell_btn = widgets.Button(description=\"Update Model\")\n", - "tell_btn.on_click(tell_model)\n", - "\n", - "uploader = widgets.FileUpload(description=\"Upload Data\", accept=\".csv\", multiple=False)\n", - "uploader.observe(mass_tell, names=\"_counter\")\n", - "\n", - "server_download = FileLink(strat_file_name)\n", - "\n", - "upload_output = widgets.Output()\n", - "\n", - "ask_tell_cont = widgets.HBox([ask_btn, tell_btn, uploader, upload_output])\n", - "\n", - "params_cont = widgets.VBox([ask_tell_cont, widgets.HBox([tell_boxes, outcome_box])])\n", - "\n", - "data_output = widgets.Output()\n", - "plot_output = widgets.Output()\n", - "plot_data_cont = widgets.HBox([data_output, plot_output])\n", - "\n", - "server_btns = widgets.HBox([start_server_btn, server_uploader])\n", - "\n", - "display(\n", - " server_btns, strat_settings, outcomes_labels, btns, params_boxes,\n", - ")\n" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "originalKey": "8dba0c9a-6265-49f5-a1e8-fffd4f862b89" + }, + "source": [ + "# What is this?\n", + "This notebook is an interactive interface for AEPsych, a Python package for adaptive experimetation in psychophysics and related domains. AEPsych utilizes active learning to efficiently explore parameter spaces, allowing experimenters to find just-noticeable-differences (or other quantities of interest) in far fewer trials than traditional methods. This notebook will allow you to use AEPsych without having to write any code." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "originalKey": "2010f083-ea65-4c7f-a66e-41fadc3bcee0" + }, + "source": [ + "# Instructions\n", + "1. Run the codeblock below. You will see a set of widgets appear.\n", + "2. If you are resuming a previous session, you can use the \"Resume Session\" button to upload a saved .pkl file and resume with all of your settings and data intact.\n", + "3. If you are starting from scratch, use the widgets to change AEPsych's settings. Here is an explanation of the settings:\n", + "\n", + " **Strategy**: There are three strategies for exploring the parameter space:\n", + " \n", + " *Threshold Finding*: AEPsych will try to find the set of parameter values at which the outcome probability equals some target value.\n", + "\n", + " *Exploration*: AEPsych will try to model the outcome at every point in the parameter space.\n", + "\n", + " *Optimization*: AEPsych will try to find the parameter values that maximize the probability of an outcome of 1.\n", + "\n", + " **Threshold**: Sets the target value for the *Threshold Finding* strategy. It is ignored by the other strategies.\n", + "\n", + " **Initialization Trials**: Sets the number of initialization trials before the model-based strategy begins. Parameter values during these trials are generated quasi-randomly. After the model has been initialized, it will begin to choose parameter values according to the strategy you have picked. \n", + " \n", + " **Outcome Labels**: These determine the labels of your outcomes. Currently AEPsych only supports binary outcomes, so one outcome will be coded as a 0 in the data, and the other outcome will be coded as a 1. Pay attention to how you label your outcomes! The *Optimization* strategy and the *Monotonic* parameter settings depend on which outcome is labeled as a 1.\n", + " \n", + " **Parameters**: These settings control the parameter space that AEPsych will explore. Use the \"Add Parameter\" and \"Remove Parameter\" buttons to add or remove parameters to the experiment. For each parameter you can specify its name, bounds, and whether or not it should be monotonically increasing with the probability of an outcome of 1 (in other words, you can specify that increasing this parameter never decreases the probability of an outcome of 1). Currently AEPsych only supports continuous parameters.\n", + "\n", + "\n", + "4. Click the \"Start AEPsych\". A new set of widgets will appear.\n", + "5. You will see the set of parameters AEPsych recommends you try. To see a different set of parameters, click \"Next Parameters\". It may take a few seconds for the parameters to appear.\n", + "6. After testing the parameters, enter the outcome, and click \"Update Model\" to update the model and see the next set of recommended parameters. You can also enter data at any time with any parameter values; you are not restricted to only using the parameters that AEPsych recommends. \n", + "7. You can also upload data from files using the \"Upload Data\" button. The data should be stored in .csv files according to the following template: \n", + "\n", + "```\n", + "parametername1,parametername2,outcome\n", + "1.1,0.4,1\n", + "0.25,1,0\n", + "```\n", + "\n", + "8. After you enter data, a table containing each set of parameters and their outcome will appear. You can download this data by clicking the \"aepsych_data.csv\" link.\n", + "9. After the AEPsych model has been initialized, a plot of the model's posterior will appear to the right of the data table. Currently plotting only works for 1 or 2-dimensional problems.\n", + "10. To save your work, you can download the \"aepsych_server.pkl\" link at the top and upload it again later.\n", + "11. If you ever need to start over, simply rerun the code block.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "originalKey": "6db98caa-05fa-483b-8970-8c524e4b237d", + "scrolled": true + }, + "outputs": [], + "source": [ + "import io\n", + "import warnings\n", + "\n", + "import dill\n", + "import ipywidgets as widgets\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from aepsych.acquisition.monotonic_rejection import MonotonicMCLSE\n", + "from aepsych.plotting import plot_strat\n", + "from aepsych.server import AEPsychServer\n", + "from IPython.display import FileLink, clear_output, display\n", + "\n", + "plt.rcParams[\"figure.figsize\"] = (10, 10)\n", + "warnings.filterwarnings(\"ignore\")\n", + "\n", + "\n", + "server = AEPsychServer()\n", + "par_precision = 3\n", + "dim = 0\n", + "inducing_scale = 50\n", + "acq_dict = {\n", + " \"Exploration\": \"MonotonicMCPosteriorVariance\",\n", + " \"Optimization\": \"qNoisyExpectedImprovement\",\n", + " \"Threshold Finding\": \"MonotonicMCLSE\",\n", + "}\n", + "style = {\"description_width\": \"initial\"}\n", + "csv_file_name = \"aepsych_data.csv\"\n", + "strat_file_name = \"aepsych_server.pkl\"\n", + "\n", + "\n", + "def add_param(b):\n", + " global dim\n", + " dim += 1\n", + " hb = widgets.HBox(\n", + " [\n", + " widgets.Text(f\"par{dim}\", description=\"Name\", style=style),\n", + " widgets.FloatText(\n", + " 0.0, description=\"Lower Bound:\", step=10 ** -par_precision, style=style\n", + " ),\n", + " widgets.FloatText(\n", + " 1.0, description=\"Upper Bound:\", step=10 ** -par_precision, style=style\n", + " ),\n", + " widgets.Checkbox(value=False, description=\"Monotonic\"),\n", + " ]\n", + " )\n", + " params_boxes.children = tuple(list(params_boxes.children) + [hb])\n", + " pars = [child.children[0].value for child in params_boxes.children]\n", + " lbs = [child.children[1].value for child in params_boxes.children]\n", + " ubs = [child.children[2].value for child in params_boxes.children]\n", + "\n", + "\n", + "def rem_param(b):\n", + " global dim\n", + " if dim > 1:\n", + " dim -= 1\n", + " params_boxes.children = tuple(list(params_boxes.children[:-1]))\n", + "\n", + "\n", + "def start_server(b):\n", + " config = make_config()\n", + " server.configure(config_str=config)\n", + " server.one_outcome = one_outcome.value\n", + " server.zero_outcome = zero_outcome.value\n", + "\n", + " with data_output:\n", + " clear_output()\n", + "\n", + " with plot_output:\n", + " clear_output()\n", + "\n", + " tell_boxes.children = [\n", + " widgets.BoundedFloatText(\n", + " lb, description=par, min=lb, max=ub, step=10 ** -par_precision, style=style\n", + " )\n", + " for par, lb, ub in zip(server.parnames, server.strat.lb, server.strat.ub)\n", + " ]\n", + " outcome_box.options = [('', None), (zero_outcome.value, 0), (one_outcome.value, 1)]\n", + " clear_output()\n", + " display(server_download, params_cont, plot_data_cont)\n", + " get_next(None)\n", + "\n", + "\n", + "def resume_server(change):\n", + " global server\n", + " for name, csv in server_uploader.value.items():\n", + " with io.BytesIO(csv[\"content\"]) as f:\n", + " server = dill.load(f)\n", + " # When the server is pickled, it deletes these attributes.\n", + " # This is an ugly hack around that.\n", + " server.socket = None\n", + " server.db = None\n", + "\n", + " tell_boxes.children = [\n", + " widgets.BoundedFloatText(\n", + " lb,\n", + " description=par,\n", + " min=lb,\n", + " max=ub,\n", + " step=10 ** -par_precision,\n", + " style=style,\n", + " )\n", + " for par, lb, ub in zip(\n", + " server.parnames, server.strat.lb, server.strat.ub\n", + " )\n", + " ]\n", + " zero_outcome.value = server.zero_outcome\n", + " one_outcome.value = server.one_outcome\n", + " outcome_box.options = [('', None), (zero_outcome.value, 0), (one_outcome.value, 1)]\n", + "\n", + " clear_output()\n", + " display(server_download, params_cont, plot_data_cont)\n", + " display_data()\n", + " display_plot()\n", + " get_next(None)\n", + "\n", + " server_uploader.value.clear()\n", + "\n", + "\n", + "def make_config():\n", + " dim = len(params_boxes.children)\n", + " pars = [child.children[0].value for child in params_boxes.children]\n", + " parnames = f\"[{','.join(par for par in pars)}]\"\n", + " lbs = [child.children[1].value for child in params_boxes.children]\n", + " ubs = [child.children[2].value for child in params_boxes.children]\n", + " monotonic = [\n", + " i for i, child in enumerate(params_boxes.children) if child.children[3]\n", + " ]\n", + " target = threshold_box.value\n", + " n_sobol = n_sobol_box.value\n", + " acq = acq_dict[strategy_btns.value]\n", + " model = \"GPClassificationModel\" if acq == \"qNoisyExpectedImprovement\" else \"MonotonicRejectionGP\"\n", + " generator = \"OptimizeAcqfGenerator\" if acq == \"qNoisyExpectedImprovement\" else \"MonotonicRejectionGenerator\"\n", + "\n", + " config = f\"\"\"\n", + " [common]\n", + " parnames = {parnames}\n", + " outcome_type = single_probit\n", + " target = {target}\n", + " strategy_names = [init_strat, opt_strat]\n", + "\n", + " [init_strat]\n", + " n_trials = {n_sobol}\n", + " generator = SobolGenerator\n", + "\n", + " [opt_strat]\n", + " n_trials = -1\n", + " refit_every = 1\n", + " generator = {generator}\n", + "\n", + " [experiment]\n", + " acqf = {acq}\n", + " model = {model}\n", + "\n", + " [SobolGenerator]\n", + " n_points = {n_sobol}\n", + "\n", + " [GPClassificationModel]\n", + " inducing_size = {inducing_scale*dim} #TODO: find a better way to scale this\n", + "\n", + " [MonotonicRejectionGP]\n", + " inducing_size = {inducing_scale*dim} #TODO: find a better way to scale this\n", + " mean_covar_factory = monotonic_mean_covar_factory\n", + " monotonic_idxs = {monotonic}\n", + " \"\"\"\n", + "\n", + " for par_name, lb, ub in zip(parnames, lbs, ubs):\n", + " config += f\"\"\"\n", + "\n", + " [{par_name}]\n", + " par_type = continuous\n", + " lower_bound = {lb}\n", + " upper_bound = {ub}\n", + " \"\"\"\n", + "\n", + " return config\n", + "\n", + "\n", + "def tell_model(b):\n", + " if outcome_box.value is not None:\n", + " with upload_output:\n", + " clear_output()\n", + " params = {child.description: child.value for child in tell_boxes.children}\n", + " outcome = outcome_box.value\n", + " server.tell(outcome, params)\n", + " for child in tell_boxes.children:\n", + " child.value = child.min\n", + " outcome_box.value = None\n", + " get_next(None)\n", + " display_data()\n", + " display_plot()\n", + " else:\n", + " with upload_output:\n", + " clear_output()\n", + " print(\"Select an outcome for this set of parameters!\")\n", + "\n", + "\n", + "def get_next(b):\n", + " tell_btn.disabled = True\n", + " ask_btn.disabled = True\n", + " uploader.disabled = True\n", + " outcome_box.disabled = True\n", + " for child in tell_boxes.children:\n", + " child.disabled = True\n", + "\n", + " if server.strat.x is None and server.strat._count >= n_sobol_box.value:\n", + " n_sobol_box.value = 1\n", + " config = make_config()\n", + " server.configure(config_str=config)\n", + " next_pars = server.ask()\n", + "\n", + " else:\n", + " next_pars = server.ask()\n", + "\n", + " for child, value in zip(tell_boxes.children, next_pars.values()):\n", + " child.value = round(value[0], par_precision)\n", + "\n", + " tell_btn.disabled = False\n", + " ask_btn.disabled = False\n", + " uploader.disabled = False\n", + " outcome_box.disabled = False\n", + " for child in tell_boxes.children:\n", + " child.disabled = False\n", + " write_server()\n", + "\n", + "\n", + "def write_server():\n", + " server_download.disabled = True\n", + " with open(strat_file_name, \"wb\") as f:\n", + " dill.dump(server, f)\n", + " server_download.disabled = False\n", + "\n", + "\n", + "def display_data():\n", + " if server.strat.x is not None:\n", + " data = {par: server.strat.x[:, i] for i, par in enumerate(server.parnames)}\n", + " data[\"outcome\"] = server.strat.y\n", + " data = pd.DataFrame(data)\n", + " data.to_csv(csv_file_name, index=False)\n", + " with data_output:\n", + " clear_output()\n", + " display(FileLink(csv_file_name), data)\n", + "\n", + "\n", + "def display_plot():\n", + " with plot_output:\n", + " clear_output()\n", + " if server.strat.dim <= 2:\n", + " if server.strat._strat_idx > 0:\n", + " xlabel = server.parnames[0]\n", + " ylabel = server.parnames[1] if server.strat.dim == 2 else None\n", + " yes_label = one_outcome.value\n", + " no_label = zero_outcome.value\n", + " acqf = server.strat._strat.generator.acqf\n", + " thresh = (\n", + " threshold_box.value\n", + " if acqf == MonotonicMCLSE\n", + " else None\n", + " )\n", + " plot_strat(\n", + " server.strat, xlabel=xlabel, ylabel=ylabel, target_level=thresh,\n", + " yes_label=yes_label, no_label=no_label\n", + " )\n", + " else:\n", + " print(\n", + " \"\\n\\n\\n\\n\\n Initializing model. Collect more data to plot posterior.\"\n", + " )\n", + " else:\n", + " print(\"Plotting currently only works for <=2D\")\n", + "\n", + "\n", + "def mass_tell(change):\n", + " for name, csv in uploader.value.items():\n", + " with io.BytesIO(csv[\"content\"]) as f:\n", + " try:\n", + " data = pd.read_csv(f)\n", + " for i, row in data.iterrows():\n", + " server.tell(\n", + " row[\"outcome\"], {par: row[par] for par in server.parnames}\n", + " )\n", + " idx = server.strat._strat_idx\n", + " server.strat.strat_list[idx]._count += 1\n", + " with upload_output:\n", + " clear_output()\n", + " get_next(None)\n", + " display_data()\n", + " display_plot()\n", + " except:\n", + " with upload_output:\n", + " clear_output()\n", + " print(\"Data is improperly formatted!\")\n", + " uploader.value.clear()\n", + " write_server()\n", + "\n", + "\n", + "server_uploader = widgets.FileUpload(\n", + " description=\"Resume Session\", accept=\".pkl\", multiple=False, style=style\n", + ")\n", + "server_uploader.observe(resume_server, names=\"_counter\")\n", + "\n", + "outcome_label = widgets.Label(value='Outcome Labels:')\n", + "zero_outcome = widgets.Text(\"No Trial\", description=\"0: \", style=style)\n", + "one_outcome = widgets.Text(\"Yes Trial\", description=\"1: \", style=style)\n", + "outcomes_labels = widgets.VBox([outcome_label, zero_outcome, one_outcome])\n", + "\n", + "params_label = widgets.Label(value=\"Parameters:\")\n", + "params_boxes = widgets.VBox([])\n", + "add_param(None)\n", + "\n", + "add_param_btn = widgets.Button(description=\"Add Parameter\")\n", + "add_param_btn.on_click(add_param)\n", + "\n", + "rem_param_btn = widgets.Button(description=\"Remove Parameter\")\n", + "rem_param_btn.on_click(rem_param)\n", + "\n", + "btns = widgets.HBox([add_param_btn, rem_param_btn])\n", + "\n", + "strategy_btns = widgets.RadioButtons(\n", + " options=[\"Threshold Finding\", \"Exploration\", \"Optimization\"],\n", + " value=\"Threshold Finding\",\n", + " description=\"Strategy:\",\n", + ")\n", + "\n", + "threshold_box = widgets.BoundedFloatText(\n", + " value=0.75, min=0, max=1.0, step=0.05, description=\"Threshold:\"\n", + ")\n", + "\n", + "n_sobol_box = widgets.BoundedIntText(\n", + " value=10, min=0, description=\"Initialization Trials:\", style=style\n", + ")\n", + "\n", + "start_server_btn = widgets.Button(description=\"Start AEPsych\")\n", + "start_server_btn.on_click(start_server)\n", + "\n", + "strat_settings = widgets.HBox([strategy_btns, threshold_box, n_sobol_box])\n", + "\n", + "config = make_config()\n", + "server.configure(config_str=config)\n", + "\n", + "tell_boxes = widgets.VBox()\n", + "outcome_box = widgets.Dropdown(\n", + " options=[('No Trial', 0), ('Yes Trial', 1), ('', None)],\n", + " value=None,\n", + " description='Outcome:',\n", + ")\n", + "\n", + "ask_btn = widgets.Button(description=\"Next Parameters\")\n", + "ask_btn.on_click(get_next)\n", + "\n", + "tell_btn = widgets.Button(description=\"Update Model\")\n", + "tell_btn.on_click(tell_model)\n", + "\n", + "uploader = widgets.FileUpload(description=\"Upload Data\", accept=\".csv\", multiple=False)\n", + "uploader.observe(mass_tell, names=\"_counter\")\n", + "\n", + "server_download = FileLink(strat_file_name)\n", + "\n", + "upload_output = widgets.Output()\n", + "\n", + "ask_tell_cont = widgets.HBox([ask_btn, tell_btn, uploader, upload_output])\n", + "\n", + "params_cont = widgets.VBox([ask_tell_cont, widgets.HBox([tell_boxes, outcome_box])])\n", + "\n", + "data_output = widgets.Output()\n", + "plot_output = widgets.Output()\n", + "plot_data_cont = widgets.HBox([data_output, plot_output])\n", + "\n", + "server_btns = widgets.HBox([start_server_btn, server_uploader])\n", + "\n", + "display(\n", + " server_btns, strat_settings, outcomes_labels, btns, params_boxes,\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "fileHeader": "", + "fileUid": "ce5e3209-53c9-4fb4-b241-87731f9efa1a", + "isAdHoc": false, + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.7" + }, + "last_base_url": "https://2475.od.fbinfra.net:443/", + "last_kernel_id": "0c9f6ace-9f8d-4d28-8c5f-0a19560b1e10", + "last_server_session_id": "a7b97464-bf11-482a-8771-84e593302ec1" } - ], - "metadata": { - "kernelspec": { - "display_name": "Python 3 (ipykernel)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.9.7" - }, - "last_base_url": "https://2475.od.fbinfra.net:443/", - "last_kernel_id": "0c9f6ace-9f8d-4d28-8c5f-0a19560b1e10", - "last_server_session_id": "a7b97464-bf11-482a-8771-84e593302ec1" - }, - "nbformat": 4, - "nbformat_minor": 2 } diff --git a/examples/contrast_discrimination_psychopy/aepsych_config.ini b/examples/contrast_discrimination_psychopy/aepsych_config.ini index 69e24657e..61698de62 100644 --- a/examples/contrast_discrimination_psychopy/aepsych_config.ini +++ b/examples/contrast_discrimination_psychopy/aepsych_config.ini @@ -7,12 +7,40 @@ # size: 1 to 10 # eccentricity: 0 to 10 parnames = [pedestal, contrast, temporal_frequency, spatial_frequency, size, eccentricity] -lb = [-1.5, -1.5, 0, 0.5, 1, 0] -ub = [0, 0, 20, 7, 10, 10] outcome_type = single_probit strategy_names = [init_strat, opt_strat] # The strategies that will be used, corresponding to the named sections below acqf = GlobalMI +[pedestal] +par_type = continuous +lower_bound = -1.5 +upper_bound = 0 + +[contrast] +par_type = continuous +lower_bound = -1.5 +upper_bound = 0 + +[temporal_frequency] +par_type = continuous +lower_bound = 0 +upper_bound = 20 + +[spatial_frequency] +par_type = continuous +lower_bound = 0.5 +upper_bound = 7 + +[size] +par_type = continuous +lower_bound = 1 +upper_bound = 10 + +[eccentricity] +par_type = continuous +lower_bound = 0 +upper_bound = 10 + [GPClassificationModel] inducing_size = 200 mean_covar_factory=default_mean_covar_factory diff --git a/examples/data_collection_analysis_tutorial.ipynb b/examples/data_collection_analysis_tutorial.ipynb index d5bcb2284..b751c51e1 100644 --- a/examples/data_collection_analysis_tutorial.ipynb +++ b/examples/data_collection_analysis_tutorial.ipynb @@ -723,32 +723,29 @@ "2022-02-25 14:47:53,575 [INFO ] Fit done, time=1.198071002960205\n" ] } - ], - "source": [ - "strat.model.fit(strat.x, strat.y)" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "We can now plot the posterior of the fitted model:" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "metadata": {}, - "outputs": [ - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA/QAAAJJCAYAAAAEBnx9AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjQuMywgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/MnkTPAAAACXBIWXMAAAsTAAALEwEAmpwYAACsiElEQVR4nOzdeXhddYH/8fe5+5qbPWnTfYOutKUtlF1ksyKLoiDIOsqoKAyjKOroKD9U3B1RB/ERcZRdRUARQRFZBEuhpbRQmi5p0zbNvtx9Oef8/kgbCLdt2nKT3CSf1/P0gd7P2XKb3JPP2b6Gbds2IiIiIiIiIjKiOIZ7A0RERERERETk0KnQi4iIiIiIiIxAKvQiIiIiIiIiI5AKvYiIiIiIiMgIpEIvIiIiIiIiMgKp0IuIiIiIiIiMQK7h3oBDVVlZyZQpU4Z7M0RERERERA5JQ0MDbW1tw70ZMoqMuEI/ZcoUVq1aNdybISIiIiIickiWLFky3Jsgo4wuuRcREREREREZgVToRUREREREREYgFXoRERERERGREUiFXkRERERERGQEUqEXERERERERGYFU6EVERERERERGIBV6ERERERERkRFIhV5ERERERERkBFKhFxERERERERmBVOhFRERERERERiAVehEREREREZERSIVeREREREREZARSoRcREREREREZgVToRUREREREREYgFXoRERERERGREUiFXkRERERERGQEUqEXERERERERGYFU6EVERERERERGIBV6ERERERERkRFo0Ar9VVddRXV1NfPmzdtnbts21157LTNmzGDBggW8/PLLg7UpIiIiIiIiIqPOoBX6K664gscee2y/+Z///Gfq6+upr6/n9ttv5xOf+MRgbYqIiIiIiIjIqOMarAWfdNJJNDQ07Dd/6KGHuOyyyzAMg2OPPZauri6ampoYN27cYG2SiAwy27b7/Xeg1w5n2SIiIiIHw+12YxjGcG+GyKAatEI/kJ07dzJx4sS+v0+YMIGdO3fus9Dffvvt3H777QC0trYO2TaKjBaWZWGaZt6fbDZLOmuSyeXIZLKkszlMy8a2bXK5HOl0Go/Xi2EY7O3Ttm1jY2PmcqRSqd7c4dyT9U5jGGDmTFKpJD5/AIfDiW0AGLBnmpyZJZVI4AsEcLrc/TfYBtPMkUrE8QWCOJ35H1X98rfPD5i57NDm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paGhgdtuu43vfe973HzzzXzxi1/E4Ri8i8DXrFnDqlWr8oZGP5BcLsf48eP57W9/e0jzuPbx/hajAd/t008/nUcffZS2tjba29v585//zNlnn81Pf/pTPvnJTw7FNha9sToO/Qv1uwBYNr264Ms3Uz3U6XJ7ERERERljoqkMVmQC2WBtvz8JVyntSRs7MuGQ8q74gZ+e//rrr3PMMccQCARwuVycfPLJ/P73vz/gPG63m0QiQSKRwO12s3nzZhobGznllFP2O8+UKVP43Oc+x/z581m2bBmbNm0CoKGhgVNPPZUFCxbw7ne/m+3btwPwwAMPMG/ePI466ihOOukkMpkMX/nKV7jvvvtYuHAh9913H/F4nKuuuoply5axaNEiHnroIQDuvPNOzjnnHE499VTe/e5309DQwLx584DeZxZceeWVzJ8/n0WLFvH3v/99n/O8VUNDA0ceeSRXXHEFs2bN4pJLLuGvf/0rxx9/PDNnzmTlypUArFy5kuXLl7No0SKOO+443njjDQDWr1/PsmXLWLhwIQsWLKC+vp54PM573/tejjrqKObNm8d99913wPd8fwYs9C+88AJnnnlm39/POOMMnn/+eY499ljS6fRhrXS0K4ayPRT5utYcM6pClAU8hzX//vJ0Mo7PShEOh/MyEREREZHRbF9n3t/p79cHMm/ePJ555hna29tJJBI8+uijNDY29uU//vGPWbBgAVdddRWdnb3DVX/hC1/gsssu45vf/Caf+tSn+NKXvsTNN9884LoikQivvvoqn/rUp/iP//gPAD796U9z+eWXs3btWi655BKuvfZaAG666Sb+8pe/8Morr/Dwww/j8Xi46aabuPDCC1mzZg0XXnghX//61zn11FNZuXIlf//737nhhhuIx+MAvPzyy/z2t7/lH//4R79t+MlPfoJhGLz66qvcc889XH755X0PndvfPACbNm3iM5/5DBs2bGDDhg3cfffdPPvss3z3u9/lG9/4BgBHHnkkzzzzDKtXr+amm27ii1/8IgC33XYb1113Xd8VBhMmTOCxxx5j/PjxvPLKK6xbt46zzjrrUP7Z+gxY6MeNG8e3vvUttm3bxrZt2/j2t79NTU0NpmkO6uUUI1WxlO3BzmPOEnZ2p1kypeyw5j9Q3t28gxmT6/T9JSIiIiJj3ju+8jWXO+DyZ8+ezec//3nOOOMMzjrrLBYuXIjT6QTgE5/4BJs3b2bNmjWMGzeOz3zmMwAsXLiQF154gb///e9s2bKFcePGYds2F154IR/5yEdobm7e57o+/OEP9/33+eefB+D555/n4osvBuDSSy/l2WefBeD444/niiuu4Oc//zmmae5zeY8//ji33HILCxcu5JRTTiGVSvWd4T/99NMpLy/Pm+fZZ5/lIx/5CNBbwCdPnszGjRsPOA/A1KlTmT9/Pg6Hg7lz5/Lud78bwzCYP38+DQ0NAHR3d/PBD36QefPmcf3117N+/XoAli9fzje+8Y2+Xu33+5k/fz5PPPEEn//853nmmWeIRA7v2WEDNqa7776bHTt2cN5553Heeeexfft27r77bkzT5P777z+slY5WxVK2hyJf09R75OvoyWWHNf+B8rJImOqKff8giYiIiIiMFYX4/TqTPvAl9wD/9m//xksvvcTTTz9NWVkZs2bNAqCmpgan04nD4eBjH/tY36Xle9m2zc0338yXv/xlvva1r/Htb3+bj33sY/zoRz/a53re+sT2gZ7eftttt3HzzTfT2NjI0UcfTXt7e940tm3zu9/9jjVr1rBmzRq2b9/O7NmzAQgGgwN+3W93oHm8Xm/f/zscjr6/OxwOcnsOmnz5y1/mXe96F+vWreORRx7pO/N/8cUX8/DDD+P3+1mxYgVPPvkks2bN4uWXX2b+/Pn813/9FzfddNMhby8cRKGvrKzk1ltvZfXq1axevZof//jHVFVV4fF4+h6UIMVVtociX9XQyczqNy+3L9Ty/eFSKkI+AoFA3jQiIiIiImNFoX6/9hzEJfgtLS0AbN++nd///vd9Z8ybmpr6pnnwwQf77kPf6//+7/9YsWIF5eXlJBIJHA4HDoeDRCKxz/XsvU/8vvvuY/ny5QAcd9xx3HvvvQDcddddnHjiiQBs3ryZY445hptuuomqqioaGxsJh8NEo9G+5Z155pnceuut2LYNwOrVqwf8Wk888UTuuusuADZu3Mj27ds54oj9PzTwUHR3d1NXVwf03pO/15YtW5g2bRrXXnst5557LmvXrmXXrl0EAgE+8pGPcMMNN/Dyyy8f1joHfHTfxo0b+e53v0tDQ0PfkQeAJ5988rBWOBqZuSyxaPGU7cHOd3Ul2dmV5MNLJxZ8+VY2xcSaco3FKSIiIiJjViF/v3bmugZc3wc+8AHa29txu9385Cc/obS0FIDPfe5zrFmzBsMwmDJlCj/72c/65kkkEtx55508/vjjAPznf/4nK1aswOPxcPfdd+9zPZ2dnSxYsACv18s999wDwK233sqVV17Jd77zHaqqqvjlL38JwA033EB9fT22bfPud7+bo446ikmTJvVdYv+FL3yBL3/5y/zHf/wHCxYswLIspk6dyh//+McDfq2f/OQn+cQnPsH8+fNxuVzceeed/c6+vxOf+9znuPzyy7n55pt573vf2/f6/fffz69//Wvcbje1tbV88Ytf5MUXX+SGG27A4XDgdrv53//938Nap2HvPZyxH0cddRQf//jHOfroo/vupQCGbbi6JUuWsGrVqmFZ976kUikee3YV3oqJRVG2hyJ/5JVdPPzKLr5zwQKCTqtgy3d5vDhiLZyw8IiC/VCJiIiIiBSLfXWZ119/ve8ycYDVr75Oc3vHIQ1Nd6B8oGHrhsqUKVNYtWoVlZWVw70pI87bv0feasAz9C6Xi0984hMF36jRZKyNQ79qWyczqkMFLfNur49sOkldxK8yLyIiIiJj1qL5+y5uIvsy4D3073vf+/jpT39KU1MTHR0dfX/kTWNpHPq9l9svnhAu+PLtTIIJGnteRERERGTUaWho0Nn5QTDgGfpf/epXAHznO9/pe80wDLZs2TJ4WzXCFWsZL0S+alsnBjArnCVUVlu4Mm9ZBB05SkpK8qYXERERERGRfAMW+q1btw7FdowaxVzGC5G/uLWdqWVuxtcWrswDZFIJplRGcO3jPiARERERERHJt9/29OSTT3Lqqafy+9//fp/5+9///kHbqJFquMv2YOfbW7po6klz4eJxBV++y0xRWzkpbx4RERERERHZt/0W+n/84x+ceuqpPPLII3mZYRgq9G8z3GV7KPLn65swgKXTqgq6fMvMEXCYBIPBvPlERERERERk3/Zb6L/2ta8B8JWvfIWpU6f2y3QZfn9jZRz6da05ZtaEKA14Crr8RHcHsyaV4XAM+IxGEREREREpMMMw+M///E++973vAfDd736XWCzGV7/61YOa/6mnnsLj8XDcccftM3/44Yd57bXXuPHGG/e7jDvvvJNVq1bx4x//+JC3fywbsEF94AMfyHvtggsuGJSNGamS8diwl+3BznscYZp60iyZXF7w5ZPsoqZKT7wUERERERkOXq+X3//+97S1tR3W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uYYuJpT5mVoZPKz5D5QHSysJuB26f15EREREpACeekqX20uv/Rb6rVu3Mm/ePAB++ctfcvrpp/PII4/wr3/9izvuuGPINnAkGOnj0O+KmWxrT3DSzKq+hx8WcvkOh4PSkB+n05k3nYiIiIiIiBye/RZ6t/vNkvq3v/2NFStWABAOh3E4Bnw4/phWTGX9YPKn69twOw2OnVY+KMvPZVJUlob3/WaJiIiIiIjIYdnvPfQTJ07k1ltvZcKECbz88sucddZZACSTyYMaV3CsKrayPlCeypr8a2s7SyaXE/C4BmX9LnKUhIJ504qIiIiIiMjh2++p9l/84hesX7+eO++8k/vuu4/S0lIAXnjhBa688sqh2r4RpdjK+sHkqxo6SWUtTppVOSjLt20bHzn8fv8+3jERERERERluTqeThQsX9v255ZZb9jvtH/7wB1577bUBl3nXXXf1W6bD4WDNmjUAnHLKKRxxxBF9WUtLCwC33nor8+bNY8WKFWQyGQCeffZZrr/++n2uo6uri5/+9Kd9f3/qqac4++yzD/bLPmhXXHEFv/3tbw96+oaGhr7b19/ulFNOKegw7Ps9Q19dXc1tt92W9/q73vUu3vWudxVsA0aLYizrB5M/Xd/KuIiPySWuQVm+mctSEfDich3UgAoiIiIiIjLE/H5/X9keyB/+8AfOPvts5syZc8DpLrnkEi655BIAXn31Vc477zwWLlzYl991110sWbKk3zx33XUXa9eu5Rvf+AZ/+ctfOPvss/l//+//cc899+xzHXsL/Sc/+cmD2va9TNMcNc/30s3wBWDmskVZ1gfKGzsTbGmLc/zUUuJdbYOy/t775zVcnYiIiIjISHPjjTcyZ84cFixYwGc/+1n++c9/8vDDD3PDDTewcOFCNm/efFDLueeee7jooosGnM62bbLZLIlEArfbzW9+8xve85737Hfo9BtvvJHNmzezcOFCbrjhBgBisRgXXHABRx55JJdccgm2bQMwZcoUPv/5z7N48WIeeOABHn/8cZYvX87ixYv54Ac/SCwW2+fXvNfTTz/Ncccdx7Rp0/rO1tu2zQ033MC8efOYP38+9913X942JpNJLrroImbPns35559PMpk8qPfsYOm0aQGM1HHon9nYhsthMK/UJFRWMyjrJ5MgEq7Of11ERERERIpCMpnsd/b8C1/4AqeddhoPPvggGzZswDAMurq6KC0t5ZxzzuHss8/mggsuOOjl33fffTz00EP9XrvyyitxOp184AMf4L/+678wDINPfepTHHvsscydO5fjjz+ec889l7/85S/7Xe4tt9zCunXr+q4ueOqpp1i9ejXr169n/PjxHH/88Tz33HOccMIJAFRUVPDyyy/T1tbG+9//fv76178SDAb51re+xfe//32uueaavK95r6amJp599lk2bNjAOeecwwUXXMDvf/971qxZwyuvvEJbWxtLly7lpJNO6reN//u//0sgEOD1119n7dq1LF68+KDft4Ox3zP0n//85wF44IEHCrrC0WgkjkOfyVk8v6WN+dVeamoGp8xn0ynsVJRAIJCXiYiIiIhIcdh7yf3ePxdeeCGRSASfz8e//du/8fvf//6wf6f/17/+RSAQ6HdP+V133cWrr77KM888wzPPPMOvf/1rAC699FJWr17Nb37zG37wgx9w7bXX8uc//5kLLriA66+/HsuyBlzfsmXLmDBhAg6Hg4ULF9LQ0NCXXXjhhUDvc+Fee+01jj/+eBYuXMivfvUrtm3bdsCv+bzzzsPhcDBnzhyam5uB3vv7P/zhD+N0OqmpqeHkk0/mxRdf7Lc9Tz/9NB/5yEcAWLBgAQsWLDis93F/9lvoH330UWzb5pvf/GZBVzgajcRx6FdubiaZtThldu2grb+nrYmq8tJ+QyCKiIiIiEjxc7lcrFy5kgsuuIA//vGPfaOeHap7772XD3/4w/1eq6urA3qHRL/44otZuXJlv3zXrl2sXLmS8847j+9973t9D2n/29/+NuD6vF5v3/87nU5yuVzf34PB3pG3bNvm9NNP7zuA8dprr/GLX/zigF/zW5e79zL+YrDfQn/WWWdRVlbG2rVrKSkpIRwO9/uv7N9wl/WDyf/xRgvVIQ9zJlQM2vq9gTA1FaV5uYiIiIiIFLdYLEZ3dzcrVqzgBz/4Aa+88grQW8Kj0ehBLcOyLO6///5+98/ncjna2toAyGaz/PGPf8x7IvyXv/xlbrrpJqD3dgDDMHA4HCQSiX7THcq2vNWxxx7Lc889x6ZNmwCIx+Ns3Lhxv1/z/px44oncd999mKZJa2srTz/9NMuWLes3zUknncTdd98NwLp161i7du0hb++B7Pce+u985zt85zvf4dxzz82730H2rxjK+kD55sYmtnZluWDxBAzDGLT129kEZSV6IJ6IiIiISDF7+z30Z511Ftdddx3nnnsuqVQK27b5/ve/D8BFF13Exz72MX70ox/x29/+lieeeAKAj3/843nLffrpp5k4cSLTpk3rey2dTnPmmWeSzWYxTZPTTjuNj33sY3356tWrAfruNb/44ouZP38+EydO5HOf+1y/5VdUVHD88cczb9483vOe9/De9773oL7eqqoq7rzzTj784Q+TTqcBuPnmmwmHw/v8mvfn/PPP5/nnn+eoo47CMAy+/e1vU1tb2+8y/0984hNceeWVzJ49m9mzZ3P00Ucf1DYeLMM+iOsFmpub++4FOOaYY6iqqiroRhyKJUuWFHTcvncqlUrx7KubITg447gPRv5YQ46nNnXwnQ8soMTvPuT5DzZ3xFs5fsHMfpeniIiIiIiMVfvqMq+//jqzZ88epi2SkeBA3yMDDlv3wAMPsGzZMh544AHuv/9+li1b1veYfnlTsZT1AS+DL6ngX9u6WTSxdFDLvGXm8LsMPB5P/pslIiIiIiIi79iAw9bdfPPNvPjii1RX9w491traymmnnXZIwxSMdmYuSyw6/GX9YPKXd8WJpXOcOLNyUNefzaSpqgjnXdIvIiIiIiIihTHgGXrLsvrKPPTep3AwwwWMJcl4rCjK+sHkz9S3URnyMHtcyaCu386lKdf98yIiIiIiIoNmwEJ/1llnceaZZ3LnnXdy55138t73vpcVK1YMxbaNGCNlHPrmnhQbdkc5cWYVDsMY1PX7DVPjz4uIiIiIFMLfD3Io8YOd7m2SySQnn3wypmke1PTHHXccAA0NDX1PcAe48847+dSnPjXg/E8++SSLFy9m3rx5XH755X1Dyz311FNEIhEWLlzIwoUL+55039raygknnMC8efP4wx/+0Lecc889l127dh3sl3nIurq6+OlPf3pY865YsYKurq7CbtA+DFjov/Od7/Dv//7vrF27lrVr13L11VfzrW99a9A3bCQZKePQP1PfhsOA46dXDOr6LcvEbeT0MDwRERERkUL4xy2Fne5t7rjjDt7//vfjdDoPavp//vOfQH6hPxiWZXH55Zdz7733sm7dOiZPnsyvfvWrvvzEE0/sGx/+K1/5CgD33HMPH//4x1m5ciU//OEPAXjkkUdYtGgR48ePP6T1H4rDKfS2bWNZFo8++iilpaWHNM/hGLDQA7z//e/n+9//Pt///vc5//zzD2tFY0kxlvmcafHc5jaOmlBK0GkN6vpT0R5KQ37dPy8iIiIiMgLcddddnHvuuQBcc801PPzww0DvsGxXXXUV0Fv6v/SlLwEQCvXeWnvjjTfyzDPPsHDhQn7wgx8AsGvXLs466yxmzpyZN8wcQHt7Ox6Ph1mzZgFw+umn87vf/e6A2+d2u0kkEqTTaZxOJ7lcjh/+8If7XP5eV1xxBR//+MdZsmQJs2bN4o9//CPQO0rZlVdeyfz581m0aBF///vfAVi/fj3Lli1j4cKFLFiwgPr6em688UY2b97MwoULueGGG4DeE95Lly5lwYIF/Pd//zfQe2DjiCOO4LLLLmPevHk0NjYyZcoU2traAPj+97/PvHnzmDdvXt8BiX3NczgGfCieHJpiLPMAa3Z0EU3lOG5KZPDX39HM+Jlz898cEREREREpKplMhi1btjBlyhSg9wz5M888wznnnMPOnTtpamoC4JlnnuGiiy7qN+8tt9zCd7/73b6yfOedd7JmzRpWr16N1+vliCOO4NOf/jQTJ07sm6eyspJcLseqVatYsmQJv/3tb/uV2b3juo8fP57vfve7zJ07l4svvpiLL76Y22+/nW9961v89Kc/5dJLLx3wFt+GhgZWrlzJ5s2bede73sWmTZv4yU9+gmEYvPrqq2zYsIEzzjiDjRs3ctttt3HddddxySWXkMlkME2TW265hXXr1rFmzRoAHn/8cerr61m5ciW2bXPOOefw9NNPM2nSJOrr6/nVr37Fscce228bXnrpJX75y1/yr3/9C9u2OeaYYzj55JMpKyvb7zyHQoW+gIq1zAM8s7GNsoCbSb4kobLqQV1/WWmJ7p8XERERETlUf74Rdr+67+yX7z24Zbx9utr58J79X4rf1tbW79LwE088kR/+8Ie89tprzJkzh87OTpqamnj++ef50Y9+NODq3/3udxOJRACYM2cO27Zt61foDcPg3nvv5frrryedTnPGGWf0Xeq/ePFitm3bRigU4tFHH+W8886jvr6eSCTCn/70JwA6Ozu55ZZbePDBB/nYxz5GZ2cnn/nMZ1i+fHnetnzoQx/C4XAwc+ZMpk2bxoYNG3j22Wf59Kc/DcCRRx7J5MmT2bhxI8uXL+frX/86O3bs4P3vfz8zZ87MW97jjz/O448/zqJFiwCIxWLU19czadIkJk+evM9i/uyzz3L++ecTDAaB3qvf9x4w2d88h2LAQv/II4/w3ve+F4fjoK7OH7OKucy3RtOsb+rh9OlBSsoHt8wHSysIWVF8vvxpRERERETkMG17dlAW6/f7SaVSfX+vq6ujq6uLxx57jJNOOomOjg7uv/9+QqEQ4XB4wOW99Tlaey+Pf7vly5fzzDPPAL0leePGjQCUlJT0TbNixQo++clP0tbWRmXlm0Nu/7//9//40pe+xD333MMJJ5zABRdcwPvf/37+8pe/5K3n7bcAH+iW4IsvvphjjjmGP/3pT6xYsYKf/exnTJs2rd80tm3zhS98gX//93/v93pDQ0NfYT8UhzPP2w1Y6O+77z7+4z/+gw984ANcddVVHHnkke94paNNsY9D/483dmMAp8ypG/T1g01ZSUgHgEREREREDtX+zqR/NQJf7R54/q9G4Mo/HdIqy8rKME2TVCrVd1Lu2GOP5Yc//CFPPvkk7e3tXHDBBVxwwQV584bDYaLR6CGtD6ClpYXq6mrS6TTf+ta3+u7N3717NzU1NRiGwcqVK7Esi4qKir756uvr2bFjB6eccgqvvPIKPp8PwzBIJpP7XM8DDzzA5ZdfztatW9myZQtHHHEEJ554InfddRennnoqGzduZPv27RxxxBFs2bKFadOmce2117J9+3bWrl3LUUcd1e/rO/PMM/nyl7/MJZdcQigUYufOnbjd+Q9If6sTTzyRK664ghtvvBHbtnnwwQf59a9/fcjv2f4M2Lp+85vfsHr1aqZPn84VV1zB8uXLuf322w/rH260KuZx6FPJJP/c3M7ccWFqykoOef5Dzc1smqqIxp8XERERERkpzjjjDJ599s0rAE488URyuRwzZsxg8eLFdHR0cOKJJ+bNt2DBApxOJ0cddVTfQ/EOxne+8x1mz57NggULeN/73sepp54KwG9/+1vmzZvHUUcdxbXXXsu9997b76z6l770Jb7+9a8D8OEPf5j//d//ZenSpVx33XX7XM+kSZNYtmwZ73nPe7jtttvw+Xx88pOfxLIs5s+fz4UXXsidd96J1+vl/vvvZ968eSxcuJB169Zx2WWXUVFRwfHHH8+8efO44YYbOOOMM7j44otZvnw58+fP54ILLhiwFy9evJgrrriCZcuWccwxx/DRj36075L9QjBs27YPZsL29nZ+/etf88Mf/pDZs2ezadMmrr322r77D4bKkiVLWLVq1ZCu80BSqRT/WL0BZ2RcXjbcZT6bTvHCG438ak03n3rXDBZOLB309VuxNo6bM7nvyZciIiIiItJrX13m9ddfZ/bs2Qee8VDO0B/MdG/z8ssv84Mf/KCgZ46H2xVXXMHZZ5+9zysLRpoDfY8MeIb+oYce4vzzz+eUU04hm82ycuVK/vznP/PKK6/wve99r+AbOxIV8zj0LzWblPrdzK+LDPr6bdvGa5j4/f686UVEREREpDgtXryYd73rXZimOdybIodowHvof//733P99ddz0kkn9Xs9EAjwi1/8YtA2bCQrljKf8UZYv7uZ984bh9NhHPL8h5qb2QyRoL/vKZUiIiIiIlIAJ99Y2On2Ye9486PFnXfeOdybMCQGPENfW1ubV+Y///nPA71DEkh/xVLmQ2VV/Gt7DGw4YWblYc1/qHkuk6KqVJfai4iIiIgU1Lu+UNjpZNQYsNA/8cQTea/9+c9/HpSNGemKqcw73V6erW9jzrgSKkPeIVm/kU1QElahFxERERGRoTVlyhTa2toOa95TTjml79kGp512Gp2dnYXctEG130L/v//7v8yfP58NGzawYMGCvj9Tp05lwYIFQ7mNI0IxlXm318e6Xd10JDKcNKtqSNafSSUhHdf98yIiIiIiMqQKee//pZdeyk9/+tOCLW+w7bfQX3zxxTzyyCOce+65PPLII31/XnrpJX7zm98M5TYWPTOXLaoyD/B0fRthn4ujJkSGZP09bU1UV5bjcg34WAYRERERESkSDQ0NHHnkkVxxxRXMmjWLSy65hL/+9a8cf/zxzJw5k5UrVwKwcuVKli9fzqJFizjuuON44403AFi/fj3Lli1j4cKFLFiwgPr6euLxOO9973s56qijmDdvHvfdd1/eek855RSuv/56lixZwuzZs3nxxRd5//vfz8yZM/mv//qvvunOO+88jj76aObOncvtt9/e93ooFOIzn/kMRx11FM8//3zf68lkkve85z38/Oc/Jx6Pc9VVV7Fs2TIWLVrEQw891DfNRRddxOzZszn//PP7jWN/zjnncM899xT2TR5E+21fhmEwZcoUfvKTn+RlHR0dlJeXD+qGjSTJeIxQxcSiKfNdiQxrd3Rxxpxa7FxmSNbvC4apqYjk5SIiIiIiUtw2bdrEAw88wB133MHSpUu5++67efbZZ3n4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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" + ], + "metadata": { + "fileHeader": "", + "fileUid": "8cc0a476-f94e-4750-90bd-8af3dc902024", + "interpreter": { + "hash": "445720f8fdcbba65d997174e9b6315f32a9c0fb7d8d99a631746a7b63e54ff16" + }, + "isAdHoc": false, + "kernelspec": { + "display_name": "Python 3.9.7 64-bit", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.7" } ], "source": [ @@ -791,7 +788,4 @@ "pygments_lexer": "ipython3", "version": "3.9.7" } - }, - "nbformat": 4, - "nbformat_minor": 5 } diff --git a/tests/test_config.py b/tests/test_config.py index c7fbb2dea..f2df367d8 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -40,8 +40,6 @@ class ConfigTestCase(unittest.TestCase): def test_single_probit_config(self): config_str = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [binary] parnames = [par1, par2] @@ -49,6 +47,16 @@ def test_single_probit_config(self): model = GPClassificationModel acqf = MCLevelSetEstimation + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] generator = SobolGenerator min_asks = 10 @@ -210,13 +218,21 @@ class DummyMod: def test_multiple_models_and_strats(self): config_str = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [binary] parnames = [par1, par2] strategy_names = [init_strat, opt_strat1, opt_strat2] + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] generator = SobolGenerator min_asks = 1 @@ -268,14 +284,22 @@ def test_experiment_deprecation(self): def test_to_string(self): in_str = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [binary] parnames = [par1, par2] strategy_names = [init_strat, opt_strat] model = GPClassificationModel acqf = LevelSetEstimation + lb = [0, 0] + ub = [1, 1] + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 [init_strat] generator = SobolGenerator min_asks = 10 @@ -360,13 +384,22 @@ def test_conversion(self): def test_warn_about_refit(self): config_str = """ [common] - lb = [0, 0] - ub = [1, 1] + parnames = [par1, par2] stimuli_per_trial = 1 outcome_types = [binary] strategy_names = [init_strat] model = GPClassificationModel + [par1] + par_type = continuous + lower_bound = 0 # lower bound + upper_bound = 1 # upper bound + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] generator = SobolGenerator min_asks = 10 @@ -381,8 +414,6 @@ def test_warn_about_refit(self): def test_pairwise_probit_config(self): config_str = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 2 outcome_types = [binary] parnames = [par1, par2] @@ -390,6 +421,16 @@ def test_pairwise_probit_config(self): acqf = PairwiseMCPosteriorVariance model = PairwiseProbitModel + [par1] + par_type = continuous + lower_bound = 0 # lower bound + upper_bound = 1 # upper bound + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] min_asks = 10 generator = SobolGenerator @@ -568,14 +609,22 @@ def test_pairwise_opt_config(self): def test_jsonify(self): sample_configstr = """ [common] - lb = [0, 0] - ub = [1, 1] outcome_type = pairwise_probit parnames = [par1, par2] strategy_names = [init_strat, opt_strat] acqf = PairwiseMCPosteriorVariance model = PairwiseProbitModel + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] min_asks = 10 generator = PairwiseSobolGenerator @@ -607,13 +656,23 @@ def test_jsonify(self): configedjson = temporaryconfig.jsonifyAll() referencejsonstr = """{ "common": { - "lb": "[0, 0]", - "ub": "[1, 1]", "outcome_type": "pairwise_probit", "parnames": "[par1, par2]", "strategy_names": "[init_strat, opt_strat]", "acqf": "PairwiseMCPosteriorVariance", - "model": "PairwiseProbitModel" + "model": "PairwiseProbitModel", + "lb": "[0, 0]", + "ub": "[1, 1]" + }, + "par1": { + "par_type": "continuous", + "lower_bound": "0", + "upper_bound": "1" + }, + "par2": { + "par_type": "continuous", + "lower_bound": "0", + "upper_bound": "1" }, "init_strat": { "min_asks": "10", @@ -646,13 +705,21 @@ def test_jsonify(self): def test_stimuli_compatibility(self): config_str1 = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [binary] parnames = [par1, par2] strategy_names = [init_strat] + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] generator = SobolGenerator model = GPClassificationModel @@ -662,13 +729,21 @@ def test_stimuli_compatibility(self): config_str2 = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [binary] parnames = [par1, par2] strategy_names = [init_strat] + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] generator = SobolGenerator model = GPClassificationModel @@ -678,13 +753,21 @@ def test_stimuli_compatibility(self): config_str3 = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [binary] parnames = [par1, par2] strategy_names = [init_strat] + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] generator = SobolGenerator model = PairwiseProbitModel @@ -706,13 +789,21 @@ def test_stimuli_compatibility(self): def test_outcome_compatibility(self): config_str1 = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [binary] parnames = [par1, par2] strategy_names = [init_strat] + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] generator = SobolGenerator model = GPClassificationModel @@ -722,13 +813,21 @@ def test_outcome_compatibility(self): config_str2 = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [continuous] parnames = [par1, par2] strategy_names = [init_strat] + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] generator = SobolGenerator model = GPClassificationModel @@ -738,13 +837,21 @@ def test_outcome_compatibility(self): config_str3 = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [binary] parnames = [par1, par2] strategy_names = [init_strat] + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] generator = SobolGenerator model = GPRegressionModel @@ -766,13 +873,21 @@ def test_outcome_compatibility(self): def test_strat_names(self): good_str = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [binary] parnames = [par1, par2] strategy_names = [init_strat, opt_strat] + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] generator = SobolGenerator model = GPClassificationModel @@ -784,13 +899,21 @@ def test_strat_names(self): bad_str = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [binary] parnames = [par1, par2] strategy_names = [init_strat, init_strat] + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] generator = SobolGenerator model = GPClassificationModel @@ -809,8 +932,6 @@ def test_strat_names(self): def test_semip_config(self): config_str = """ [common] - lb = [0, 0] - ub = [1, 1] stimuli_per_trial = 1 outcome_types = [binary] parnames = [par1, par2] @@ -818,6 +939,16 @@ def test_semip_config(self): acqf = MCLevelSetEstimation model = HadamardSemiPModel + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + [init_strat] min_asks = 10 generator = SobolGenerator @@ -861,6 +992,150 @@ def test_semip_config(self): self.assertTrue(isinstance(model.likelihood, BernoulliObjectiveLikelihood)) self.assertTrue(isinstance(model.likelihood.objective, FloorGumbelObjective)) + def test_derived_bounds(self): + config_str = """ + [common] + parnames = [par1, par2] + stimuli_per_trial = 1 + outcome_types = [binary] + target = 0.75 + strategy_names = [init_strat, opt_strat] + + [par1] + par_type = continuous + lower_bound = 0 + upper_bound = 1 + + [par2] + par_type = continuous + lower_bound = -10 + upper_bound = 10 + + [init_strat] + min_total_tells = 10 + generator = SobolGenerator + + [opt_strat] + min_total_tells = 20 + refit_every = 5 + generator = OptimizeAcqfGenerator + acqf = MCLevelSetEstimation + model = GPClassificationModel + """ + + config = Config() + config.update(config_str=config_str) + + strat = SequentialStrategy.from_config(config) + opt_strat = strat.strat_list[1] + model = opt_strat.model + + self.assertTrue(torch.all(model.lb == torch.Tensor([0, -10]))) + self.assertTrue(torch.all(model.ub == torch.Tensor([1, 10]))) + + def test_ignore_specific_bounds(self): + config_str = """ + [common] + parnames = [par1, par2] + lb = [0, 0] + ub = [1, 1] + stimuli_per_trial = 1 + outcome_types = [binary] + target = 0.75 + strategy_names = [init_strat, opt_strat] + + [par1] + par_type = continuous + lower_bound = 1 + upper_bound = 100 + + [par2] + par_type = continuous + lower_bound = -5 + upper_bound = 1 + + [init_strat] + min_total_tells = 10 + generator = SobolGenerator + + [opt_strat] + min_total_tells = 20 + refit_every = 5 + generator = OptimizeAcqfGenerator + acqf = MCLevelSetEstimation + model = GPClassificationModel + """ + + config = Config() + config.update(config_str=config_str) + + strat = SequentialStrategy.from_config(config) + opt_strat = strat.strat_list[1] + model = opt_strat.model + + self.assertTrue(torch.all(model.lb == torch.Tensor([0, 0]))) + self.assertTrue(torch.all(model.ub == torch.Tensor([1, 1]))) + + def test_parameter_setting_block_validation(self): + config_str = """ + [common] + parnames = [par1, par2] + """ + config = Config() + + with self.assertRaises(ValueError): + config.update(config_str=config_str) + + def test_invalid_parameter_type(self): + config_str = """ + [common] + parnames = [par1] + + [par1] + par_type = invalid_type + """ + config = Config() + with self.assertRaises(ValueError): + config.update(config_str=config_str) + + def test_continuous_parameter_lb_validation(self): + config_str = """ + [common] + parnames = [par1, par2] + + [par1] + par_type = continuous + lower_bound = 1 + upper_bound = 100 + + [par2] + par_type = continuous + upper_bound = 1 + """ + config = Config() + with self.assertRaises(ValueError): + config.update(config_str=config_str) + + def test_continuous_parameter_ub_validation(self): + config_str = """ + [common] + parnames = [par1, par2] + + [par1] + par_type = continuous + lower_bound = 1 + upper_bound = 100 + + [par2] + par_type = continuous + lower_bound = 0 + """ + config = Config() + with self.assertRaises(ValueError): + config.update(config_str=config_str) + + + if __name__ == "__main__": unittest.main()