diff --git a/docs/notebooks/ask_tell_optimization.pct.py b/docs/notebooks/ask_tell_optimization.pct.py index c4a255286f..0bfd1b39aa 100644 --- a/docs/notebooks/ask_tell_optimization.pct.py +++ b/docs/notebooks/ask_tell_optimization.pct.py @@ -12,7 +12,10 @@ import matplotlib.pyplot as plt import gpflow -from trieste.ask_tell_optimization import AskTellOptimizer +from trieste.ask_tell_optimization import ( + AskTellOptimizer, + AskTellOptimizerNoTraining, +) from trieste.bayesian_optimizer import Record from trieste.data import Dataset from trieste.models.gpflow.models import GaussianProcessRegression @@ -96,7 +99,7 @@ def plot_ask_tell_regret(ask_tell_result): # %% [markdown] # ## Model selection: using only Ask part # -# We now turn to a slightly more complex use case. Let's suppose we want to switch between two models depending on some criteria dynamically during the optimization loop, e.g. we want to be able to train a model outside of Trieste. In this case we can only use Ask part of the Ask-Tell interface. +# We now turn to a slightly more complex use case. Let's suppose we want to switch between two models depending on some criteria dynamically during the optimization loop, e.g. we want to be able to train a model outside of Trieste. In this case we can only use Ask part of the Ask-Tell interface. For this it is recommended to use the `AskTellOptimizerNoTraining` class, which performs no training during the Tell stage and can therefore be used with any probabilistic model, including ones which aren't trainable. # %% model1 = build_model( @@ -118,8 +121,8 @@ def plot_ask_tell_regret(ask_tell_result): model = model2 print("Asking for new point to observe") - ask_tell = AskTellOptimizer(search_space, dataset, model) - new_point = ask_tell.ask() + ask_only = AskTellOptimizerNoTraining(search_space, dataset, model) + new_point = ask_only.ask() new_data_point = observer(new_point) dataset = dataset + new_data_point @@ -131,7 +134,7 @@ def plot_ask_tell_regret(ask_tell_result): model2.update(dataset) model2.optimize(dataset) -plot_ask_tell_regret(ask_tell.to_result()) +plot_ask_tell_regret(ask_only.to_result()) # %% [markdown] @@ -151,7 +154,7 @@ def plot_ask_tell_regret(ask_tell_result): new_config = ask_tell.ask() print("Saving Trieste state to re-use later") - state: Record[None] = ask_tell.to_record() + state: Record[None, GaussianProcessRegression] = ask_tell.to_record() saved_state = pickle.dumps(state) print(f"In the lab running the experiment #{step}.") diff --git a/docs/notebooks/expected_improvement.pct.py b/docs/notebooks/expected_improvement.pct.py index 51d9788d18..1ff034ff7f 100644 --- a/docs/notebooks/expected_improvement.pct.py +++ b/docs/notebooks/expected_improvement.pct.py @@ -292,7 +292,7 @@ def build_model(data): saved_result = trieste.bayesian_optimizer.OptimizationResult.from_path( # type: ignore "results_path" ) -saved_result.try_get_final_model().model # type: ignore +saved_result.try_get_final_model().model # %% [markdown] # The second approach is to save the model using the tensorflow SavedModel format. This requires explicitly exporting the methods to be saved and results in a portable model than can be safely loaded and evaluated, but which can no longer be used in subsequent BO steps. diff --git a/docs/notebooks/failure_ego.pct.py b/docs/notebooks/failure_ego.pct.py index 5fb6b4ca59..03546db85a 100644 --- a/docs/notebooks/failure_ego.pct.py +++ b/docs/notebooks/failure_ego.pct.py @@ -238,7 +238,7 @@ def acquisition(at): # %% fig, ax = plot_gp_2d( - result.models[FAILURE].model, # type: ignore + result.models[FAILURE].model, search_space.lower, search_space.upper, grid_density=20, diff --git a/tests/integration/test_active_learning.py b/tests/integration/test_active_learning.py index 291cb5c107..80b7b56bec 100644 --- a/tests/integration/test_active_learning.py +++ b/tests/integration/test_active_learning.py @@ -107,7 +107,7 @@ def test_optimizer_learns_scaled_branin_function( .optimize(num_steps, initial_data, model, acquisition_rule) .try_get_final_model() ) - final_predicted_means, _ = final_model.model.predict_f(test_query_points) # type: ignore + final_predicted_means, _ = final_model.model.predict_f(test_query_points) final_accuracy = tf.reduce_max(tf.abs(final_predicted_means - test_data.observations)) assert initial_accuracy > final_accuracy @@ -357,7 +357,7 @@ def ilink(f: TensorType) -> TensorType: .optimize(num_steps, initial_data, model, rule) .try_get_final_model() ) - final_predicted_means, _ = ilink(final_model.model.predict_f(test_query_points)) # type: ignore + final_predicted_means, _ = ilink(final_model.model.predict_f(test_query_points)) final_error = tf.reduce_mean(tf.abs(final_predicted_means - test_data.observations)) assert initial_error > final_error diff --git a/tests/integration/test_ask_tell_optimization.py b/tests/integration/test_ask_tell_optimization.py index 6e8fc22c4e..8f90314518 100644 --- a/tests/integration/test_ask_tell_optimization.py +++ b/tests/integration/test_ask_tell_optimization.py @@ -207,7 +207,8 @@ def _test_ask_tell_optimization_finds_minima( if reload_state: state: Record[ - None | State[TensorType, AsynchronousRuleState | BatchTrustRegionBox.State] + None | State[TensorType, AsynchronousRuleState | BatchTrustRegionBox.State], + GaussianProcessRegression, ] = ask_tell.to_record() written_state = pickle.dumps(state) @@ -228,7 +229,8 @@ def _test_ask_tell_optimization_finds_minima( ask_tell.tell(new_data_point) result: OptimizationResult[ - None | State[TensorType, AsynchronousRuleState | BatchTrustRegionBox.State] + None | State[TensorType, AsynchronousRuleState | BatchTrustRegionBox.State], + GaussianProcessRegression, ] = ask_tell.to_result() dataset = result.try_get_final_dataset() diff --git a/tests/integration/test_bayesian_optimization.py b/tests/integration/test_bayesian_optimization.py index 1819e843e4..1673a66fd6 100644 --- a/tests/integration/test_bayesian_optimization.py +++ b/tests/integration/test_bayesian_optimization.py @@ -701,9 +701,9 @@ def _test_optimizer_finds_minimum( # check history saved ok assert len(result.history) <= (num_steps or 2) assert len(result.loaded_history) == len(result.history) - loaded_result: OptimizationResult[None] = OptimizationResult.from_path( - Path(tmpdirname) / "history" - ) + loaded_result: OptimizationResult[ + None, TrainableProbabilisticModel + ] = OptimizationResult.from_path(Path(tmpdirname) / "history") assert loaded_result.final_result.is_ok assert len(loaded_result.history) == len(result.history) diff --git a/tests/unit/test_ask_tell_optimization.py b/tests/unit/test_ask_tell_optimization.py index 8d1056661d..ce1f19193f 100644 --- a/tests/unit/test_ask_tell_optimization.py +++ b/tests/unit/test_ask_tell_optimization.py @@ -13,7 +13,7 @@ # limitations under the License. from __future__ import annotations -from typing import Mapping, Optional +from typing import Mapping, Optional, Type, Union import numpy.testing as npt import pytest @@ -28,7 +28,7 @@ ) from trieste.acquisition.rule import AcquisitionRule, LocalDatasetsAcquisitionRule from trieste.acquisition.utils import copy_to_local_models -from trieste.ask_tell_optimization import AskTellOptimizer +from trieste.ask_tell_optimization import AskTellOptimizer, AskTellOptimizerNoTraining from trieste.bayesian_optimizer import OptimizationResult, Record from trieste.data import Dataset from trieste.models.interfaces import ProbabilisticModel, TrainableProbabilisticModel @@ -76,156 +76,188 @@ def model() -> TrainableProbabilisticModel: return LinearWithUnitVariance() +# most of the tests below should be run for both AskTellOptimizer and AskTellOptimizerNoTraining +OPTIMIZERS = [AskTellOptimizer, AskTellOptimizerNoTraining] +OptimizerType = Union[ + Type[AskTellOptimizer[Box, TrainableProbabilisticModel]], + Type[AskTellOptimizerNoTraining[Box, TrainableProbabilisticModel]], +] + + +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_suggests_new_point( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: - ask_tell = AskTellOptimizer(search_space, init_dataset, model, acquisition_rule) + ask_tell = optimizer(search_space, init_dataset, model, acquisition_rule) new_point = ask_tell.ask() assert len(new_point) == 1 +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_with_default_acquisition_suggests_new_point( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, + optimizer: OptimizerType, ) -> None: - ask_tell = AskTellOptimizer(search_space, init_dataset, model) + ask_tell = optimizer(search_space, init_dataset, model) new_point = ask_tell.ask() assert len(new_point) == 1 +@pytest.mark.parametrize("optimizer", OPTIMIZERS) @pytest.mark.parametrize("copy", [True, False]) def test_ask_tell_optimizer_returns_complete_state( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, copy: bool, ) -> None: - ask_tell = AskTellOptimizer(search_space, init_dataset, model, acquisition_rule) + ask_tell = optimizer(search_space, init_dataset, model, acquisition_rule) - state_record: Record[None] = ask_tell.to_record(copy=copy) + state_record: Record[None, TrainableProbabilisticModel] = ask_tell.to_record(copy=copy) assert_datasets_allclose(state_record.dataset, init_dataset) assert isinstance(state_record.model, type(model)) assert state_record.acquisition_state is None +@pytest.mark.parametrize("optimizer", OPTIMIZERS) @pytest.mark.parametrize("copy", [True, False]) def test_ask_tell_optimizer_loads_from_state( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, copy: bool, ) -> None: - old_state: Record[None] = Record({OBJECTIVE: init_dataset}, {OBJECTIVE: model}, None) + old_state: Record[None, TrainableProbabilisticModel] = Record( + {OBJECTIVE: init_dataset}, {OBJECTIVE: model}, None + ) - ask_tell = AskTellOptimizer.from_record(old_state, search_space, acquisition_rule) - new_state: Record[None] = ask_tell.to_record(copy=copy) + ask_tell = optimizer.from_record(old_state, search_space, acquisition_rule) + new_state: Record[None, TrainableProbabilisticModel] = ask_tell.to_record(copy=copy) assert_datasets_allclose(old_state.dataset, new_state.dataset) assert isinstance(new_state.model, type(old_state.model)) +@pytest.mark.parametrize("optimizer", OPTIMIZERS) @pytest.mark.parametrize("copy", [True, False]) def test_ask_tell_optimizer_returns_optimization_result( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, copy: bool, ) -> None: - ask_tell = AskTellOptimizer(search_space, init_dataset, model, acquisition_rule) + ask_tell = optimizer(search_space, init_dataset, model, acquisition_rule) - result: OptimizationResult[None] = ask_tell.to_result(copy=copy) + result: OptimizationResult[None, TrainableProbabilisticModel] = ask_tell.to_result(copy=copy) assert_datasets_allclose(result.try_get_final_dataset(), init_dataset) assert isinstance(result.try_get_final_model(), type(model)) +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_updates_state_with_new_data( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: new_data = mk_dataset([[1.0]], [[1.0]]) - ask_tell = AskTellOptimizer(search_space, init_dataset, model, acquisition_rule) + ask_tell = optimizer(search_space, init_dataset, model, acquisition_rule) ask_tell.tell(new_data) - state_record: Record[None] = ask_tell.to_record() + state_record: Record[None, TrainableProbabilisticModel] = ask_tell.to_record() assert_datasets_allclose(state_record.dataset, init_dataset + new_data) +@pytest.mark.parametrize("optimizer", OPTIMIZERS) @pytest.mark.parametrize("copy", [True, False]) def test_ask_tell_optimizer_copies_state( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, copy: bool, ) -> None: new_data = mk_dataset([[1.0]], [[1.0]]) - ask_tell = AskTellOptimizer(search_space, init_dataset, model, acquisition_rule) - state_start: Record[None] = ask_tell.to_record(copy=copy) + ask_tell = optimizer(search_space, init_dataset, model, acquisition_rule) + state_start: Record[None, TrainableProbabilisticModel] = ask_tell.to_record(copy=copy) ask_tell.tell(new_data) - state_end: Record[None] = ask_tell.to_record(copy=copy) + state_end: Record[None, TrainableProbabilisticModel] = ask_tell.to_record(copy=copy) assert_datasets_allclose(state_start.dataset, init_dataset if copy else init_dataset + new_data) assert_datasets_allclose(state_end.dataset, init_dataset + new_data) assert state_start.model is not model if copy else state_start.model is model +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_datasets_property( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: - ask_tell = AskTellOptimizer(search_space, init_dataset, model, acquisition_rule) + ask_tell = optimizer(search_space, init_dataset, model, acquisition_rule) assert_datasets_allclose(ask_tell.datasets[OBJECTIVE], init_dataset) assert_datasets_allclose(ask_tell.dataset, init_dataset) +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_models_property( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: - ask_tell = AskTellOptimizer(search_space, init_dataset, model, acquisition_rule) + ask_tell = optimizer(search_space, init_dataset, model, acquisition_rule) assert ask_tell.models[OBJECTIVE] is model assert ask_tell.model is model +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_models_setter( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: - ask_tell = AskTellOptimizer(search_space, init_dataset, model, acquisition_rule) + ask_tell = optimizer(search_space, init_dataset, model, acquisition_rule) model2 = LinearWithUnitVariance() ask_tell.models = {OBJECTIVE: model2} assert ask_tell.models[OBJECTIVE] is model2 is not model +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_models_setter_errors( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: - ask_tell = AskTellOptimizer(search_space, init_dataset, model, acquisition_rule) + ask_tell = optimizer(search_space, init_dataset, model, acquisition_rule) with pytest.raises(ValueError): ask_tell.models = {} with pytest.raises(ValueError): @@ -234,25 +266,29 @@ def test_ask_tell_optimizer_models_setter_errors( ask_tell.models = {"CONSTRAINT": LinearWithUnitVariance()} +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_model_setter( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: - ask_tell = AskTellOptimizer(search_space, init_dataset, model, acquisition_rule) + ask_tell = optimizer(search_space, init_dataset, model, acquisition_rule) model2 = LinearWithUnitVariance() ask_tell.model = model2 assert ask_tell.models[OBJECTIVE] is model2 is not model +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_model_setter_errors( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: - one_model = AskTellOptimizer(search_space, {"X": init_dataset}, {"X": model}, acquisition_rule) + one_model = optimizer(search_space, {"X": init_dataset}, {"X": model}, acquisition_rule) with pytest.raises(ValueError): one_model.model = model two_models = AskTellOptimizer( @@ -277,7 +313,7 @@ def test_ask_tell_optimizer_trains_model( ) ask_tell.tell(new_data) - state_record: Record[None] = ask_tell.to_record() + state_record: Record[None, TrainableProbabilisticModel] = ask_tell.to_record() assert state_record.model.optimize_count == 1 # type: ignore @@ -293,7 +329,7 @@ def test_ask_tell_optimizer_optimizes_initial_model( ask_tell = AskTellOptimizer( search_space, init_dataset, model, acquisition_rule, fit_model=fit_initial_model ) - state_record: Record[None] = ask_tell.to_record() + state_record: Record[None, TrainableProbabilisticModel] = ask_tell.to_record() if fit_initial_model: assert state_record.model.optimize_count == 1 # type: ignore @@ -307,14 +343,34 @@ def test_ask_tell_optimizer_from_state_does_not_train_model( model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], ) -> None: - old_state: Record[None] = Record({OBJECTIVE: init_dataset}, {OBJECTIVE: model}, None) + old_state: Record[None, TrainableProbabilisticModel] = Record( + {OBJECTIVE: init_dataset}, {OBJECTIVE: model}, None + ) ask_tell = AskTellOptimizer.from_record(old_state, search_space, acquisition_rule) - state_record: Record[None] = ask_tell.to_record() + state_record: Record[None, TrainableProbabilisticModel] = ask_tell.to_record() + + assert state_record.model.optimize_count == 0 # type: ignore + + +def test_ask_tell_optimizer_no_training_does_not_train_model( + search_space: Box, + init_dataset: Dataset, + model: TrainableProbabilisticModel, + acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], +) -> None: + new_data = mk_dataset([[1.0]], [[1.0]]) + ask_tell = AskTellOptimizerNoTraining( + search_space, init_dataset, model, acquisition_rule, fit_model=True + ) + + ask_tell.tell(new_data) + state_record: Record[None, TrainableProbabilisticModel] = ask_tell.to_record() assert state_record.model.optimize_count == 0 # type: ignore +@pytest.mark.parametrize("optimizer", OPTIMIZERS) @pytest.mark.parametrize( "starting_state, expected_state", [(None, 1), (0, 1), (3, 4)], @@ -323,6 +379,7 @@ def test_ask_tell_optimizer_uses_specified_acquisition_state( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, + optimizer: OptimizerType, starting_state: int | None, expected_state: int, ) -> None: @@ -348,84 +405,90 @@ def go(state: int | None) -> tuple[int | None, TensorType]: rule = Rule() - ask_tell = AskTellOptimizer( - search_space, init_dataset, model, rule, acquisition_state=starting_state - ) + ask_tell = optimizer(search_space, init_dataset, model, rule, acquisition_state=starting_state) _ = ask_tell.ask() - state_record: Record[State[int, TensorType]] = ask_tell.to_record() + state_record: Record[State[int, TensorType], TrainableProbabilisticModel] = ask_tell.to_record() # mypy cannot see that this is in fact int assert state_record.acquisition_state == expected_state # type: ignore assert ask_tell.acquisition_state == expected_state +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_does_not_accept_empty_datasets_or_models( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: with pytest.raises(ValueError): - AskTellOptimizer(search_space, {}, model, acquisition_rule) # type: ignore + optimizer(search_space, {}, model, acquisition_rule) # type: ignore with pytest.raises(ValueError): - AskTellOptimizer(search_space, init_dataset, {}, acquisition_rule) # type: ignore + optimizer(search_space, init_dataset, {}, acquisition_rule) # type: ignore +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_validates_keys( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: dataset_with_key_1 = {TAG1: init_dataset} model_with_key_2 = {TAG2: model} with pytest.raises(ValueError): - AskTellOptimizer(search_space, dataset_with_key_1, model_with_key_2, acquisition_rule) + optimizer(search_space, dataset_with_key_1, model_with_key_2, acquisition_rule) +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_tell_validates_keys( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: dataset_with_key_1 = {TAG1: init_dataset} model_with_key_1 = {TAG1: model} new_data_with_key_2 = {TAG2: mk_dataset([[1.0]], [[1.0]])} - ask_tell = AskTellOptimizer( - search_space, dataset_with_key_1, model_with_key_1, acquisition_rule - ) + ask_tell = optimizer(search_space, dataset_with_key_1, model_with_key_1, acquisition_rule) with pytest.raises(KeyError, match=str(TAG2)): ask_tell.tell(new_data_with_key_2) +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_default_acquisition_requires_objective_tag( search_space: Box, init_dataset: Dataset, model: TrainableProbabilisticModel, + optimizer: OptimizerType, ) -> None: wrong_tag: Tag = f"{OBJECTIVE}_WRONG" wrong_datasets = {wrong_tag: init_dataset} wrong_models = {wrong_tag: model} with pytest.raises(ValueError): - AskTellOptimizer(search_space, wrong_datasets, wrong_models) + optimizer(search_space, wrong_datasets, wrong_models) +@pytest.mark.parametrize("optimizer", OPTIMIZERS) def test_ask_tell_optimizer_for_uncopyable_model( search_space: Box, init_dataset: Dataset, acquisition_rule: AcquisitionRule[TensorType, Box, TrainableProbabilisticModel], + optimizer: OptimizerType, ) -> None: class _UncopyableModel(LinearWithUnitVariance): def __deepcopy__(self, memo: dict[int, object]) -> _UncopyableModel: raise MemoryError model = _UncopyableModel() - ask_tell = AskTellOptimizer(search_space, init_dataset, model, acquisition_rule) + ask_tell = optimizer(search_space, init_dataset, model, acquisition_rule) with pytest.raises(NotImplementedError): ask_tell.to_result() @@ -531,3 +594,20 @@ def test_ask_tell_optimizer_creates_correct_datasets_for_rank3_points( points = ask_tell.ask() new_data = observer(points) ask_tell.tell(new_data) + + +def test_ask_tell_optimizer_no_training_with_non_trainable_model( + search_space: Box, + init_dataset: Dataset, + acquisition_rule: AcquisitionRule[TensorType, Box, ProbabilisticModel], +) -> None: + model = GaussianProcess([lambda x: 2 * x], [rbf()]) + new_data = mk_dataset([[1.0]], [[1.0]]) + ask_tell = AskTellOptimizerNoTraining(search_space, init_dataset, model, acquisition_rule) + + new_point = ask_tell.ask() + assert len(new_point) == 1 + + ask_tell.tell(new_data) + state_record: Record[None, ProbabilisticModel] = ask_tell.to_record() + assert_datasets_allclose(state_record.dataset, init_dataset + new_data) diff --git a/tests/unit/test_bayesian_optimizer.py b/tests/unit/test_bayesian_optimizer.py index f6d2596544..54362625ce 100644 --- a/tests/unit/test_bayesian_optimizer.py +++ b/tests/unit/test_bayesian_optimizer.py @@ -74,7 +74,7 @@ class _Whoops(Exception): def test_optimization_result_astuple() -> None: - opt_result: OptimizationResult[None] = OptimizationResult( + opt_result: OptimizationResult[None, TrainableProbabilisticModel] = OptimizationResult( Err(_Whoops()), [Record({}, {}, None)] ) final_result, history = opt_result.astuple() @@ -84,7 +84,7 @@ def test_optimization_result_astuple() -> None: def test_optimization_result_try_get_final_datasets_for_successful_optimization() -> None: data = {FOO: empty_dataset([1], [1])} - result: OptimizationResult[None] = OptimizationResult( + result: OptimizationResult[None, TrainableProbabilisticModel] = OptimizationResult( Ok(Record(data, {FOO: _PseudoTrainableQuadratic()}, None)), [] ) assert result.try_get_final_datasets() is data @@ -93,7 +93,7 @@ def test_optimization_result_try_get_final_datasets_for_successful_optimization( def test_optimization_result_status_for_successful_optimization() -> None: data = {FOO: empty_dataset([1], [1])} - result: OptimizationResult[None] = OptimizationResult( + result: OptimizationResult[None, TrainableProbabilisticModel] = OptimizationResult( Ok(Record(data, {FOO: _PseudoTrainableQuadratic()}, None)), [] ) assert result.is_ok @@ -103,27 +103,29 @@ def test_optimization_result_status_for_successful_optimization() -> None: def test_optimization_result_try_get_final_datasets_for_multiple_datasets() -> None: data = {FOO: empty_dataset([1], [1]), BAR: empty_dataset([2], [2])} models = {FOO: _PseudoTrainableQuadratic(), BAR: _PseudoTrainableQuadratic()} - result: OptimizationResult[None] = OptimizationResult(Ok(Record(data, models, None)), []) + result: OptimizationResult[None, TrainableProbabilisticModel] = OptimizationResult( + Ok(Record(data, models, None)), [] + ) assert result.try_get_final_datasets() is data with pytest.raises(ValueError): result.try_get_final_dataset() def test_optimization_result_try_get_final_datasets_for_failed_optimization() -> None: - result: OptimizationResult[object] = OptimizationResult(Err(_Whoops()), []) + result: OptimizationResult[object, ProbabilisticModel] = OptimizationResult(Err(_Whoops()), []) with pytest.raises(_Whoops): result.try_get_final_datasets() def test_optimization_result_status_for_failed_optimization() -> None: - result: OptimizationResult[object] = OptimizationResult(Err(_Whoops()), []) + result: OptimizationResult[object, ProbabilisticModel] = OptimizationResult(Err(_Whoops()), []) assert result.is_err assert not result.is_ok def test_optimization_result_try_get_final_models_for_successful_optimization() -> None: models = {FOO: _PseudoTrainableQuadratic()} - result: OptimizationResult[None] = OptimizationResult( + result: OptimizationResult[None, TrainableProbabilisticModel] = OptimizationResult( Ok(Record({FOO: empty_dataset([1], [1])}, models, None)), [] ) assert result.try_get_final_models() is models @@ -133,21 +135,23 @@ def test_optimization_result_try_get_final_models_for_successful_optimization() def test_optimization_result_try_get_final_models_for_multiple_models() -> None: data = {FOO: empty_dataset([1], [1]), BAR: empty_dataset([2], [2])} models = {FOO: _PseudoTrainableQuadratic(), BAR: _PseudoTrainableQuadratic()} - result: OptimizationResult[None] = OptimizationResult(Ok(Record(data, models, None)), []) + result: OptimizationResult[None, TrainableProbabilisticModel] = OptimizationResult( + Ok(Record(data, models, None)), [] + ) assert result.try_get_final_models() is models with pytest.raises(ValueError): result.try_get_final_model() def test_optimization_result_try_get_final_models_for_failed_optimization() -> None: - result: OptimizationResult[object] = OptimizationResult(Err(_Whoops()), []) + result: OptimizationResult[object, ProbabilisticModel] = OptimizationResult(Err(_Whoops()), []) with pytest.raises(_Whoops): result.try_get_final_models() def test_optimization_result_try_get_optimal_point_for_successful_optimization() -> None: data = {FOO: mk_dataset([[0.25, 0.25], [0.5, 0.4]], [[0.8], [0.7]])} - result: OptimizationResult[None] = OptimizationResult( + result: OptimizationResult[None, TrainableProbabilisticModel] = OptimizationResult( Ok(Record(data, {FOO: _PseudoTrainableQuadratic()}, None)), [] ) x, y, idx = result.try_get_optimal_point() @@ -158,7 +162,7 @@ def test_optimization_result_try_get_optimal_point_for_successful_optimization() def test_optimization_result_try_get_optimal_point_for_multiple_objectives() -> None: data = {FOO: mk_dataset([[0.25], [0.5]], [[0.8, 0.5], [0.7, 0.4]])} - result: OptimizationResult[None] = OptimizationResult( + result: OptimizationResult[None, TrainableProbabilisticModel] = OptimizationResult( Ok(Record(data, {FOO: _PseudoTrainableQuadratic()}, None)), [] ) with pytest.raises(ValueError): @@ -166,19 +170,21 @@ def test_optimization_result_try_get_optimal_point_for_multiple_objectives() -> def test_optimization_result_try_get_optimal_point_for_failed_optimization() -> None: - result: OptimizationResult[object] = OptimizationResult(Err(_Whoops()), []) + result: OptimizationResult[object, ProbabilisticModel] = OptimizationResult(Err(_Whoops()), []) with pytest.raises(_Whoops): result.try_get_optimal_point() def test_optimization_result_from_path() -> None: with tempfile.TemporaryDirectory() as tmpdirname: - opt_result: OptimizationResult[None] = OptimizationResult( + opt_result: OptimizationResult[None, TrainableProbabilisticModel] = OptimizationResult( Err(_Whoops()), [Record({}, {}, None)] * 10 ) opt_result.save(tmpdirname) - result, history = OptimizationResult[None].from_path(tmpdirname).astuple() + result, history = ( + OptimizationResult[None, TrainableProbabilisticModel].from_path(tmpdirname).astuple() + ) assert result.is_err with pytest.raises(_Whoops): result.unwrap() @@ -193,14 +199,16 @@ def test_optimization_result_from_path() -> None: def test_optimization_result_from_path_partial_result() -> None: with tempfile.TemporaryDirectory() as tmpdirname: - opt_result: OptimizationResult[None] = OptimizationResult( + opt_result: OptimizationResult[None, TrainableProbabilisticModel] = OptimizationResult( Err(_Whoops()), [Record({}, {}, None)] * 10 ) opt_result.save(tmpdirname) (Path(tmpdirname) / OptimizationResult.RESULTS_FILENAME).unlink() (Path(tmpdirname) / OptimizationResult.step_filename(9, 10)).unlink() - result, history = OptimizationResult[None].from_path(tmpdirname).astuple() + result, history = ( + OptimizationResult[None, TrainableProbabilisticModel].from_path(tmpdirname).astuple() + ) assert result.is_err with pytest.raises(FileNotFoundError): result.unwrap() @@ -322,7 +330,9 @@ def test_bayesian_optimizer_continue_optimization_raises_for_empty_result() -> N search_space = Box([-1], [1]) optimizer = BayesianOptimizer(lambda x: {FOO: Dataset(x, x)}, search_space) rule = FixedAcquisitionRule([[0.0]]) - opt_result: OptimizationResult[None] = OptimizationResult(Err(_Whoops()), []) + opt_result: OptimizationResult[None, TrainableProbabilisticModel] = OptimizationResult( + Err(_Whoops()), [] + ) with pytest.raises(ValueError): optimizer.continue_optimization(10, opt_result, rule) @@ -353,11 +363,11 @@ def optimize(self, dataset: Dataset) -> None: final_model = final_opt_state.unwrap().model if fit_model == "all": # optimized at start and end of first BO step - assert final_model._optimize_count == 2 # type: ignore + assert final_model._optimize_count == 2 elif fit_model == "all_but_init": # optimized just at end of first BO step - assert final_model._optimize_count == 1 # type: ignore + assert final_model._optimize_count == 1 else: # never optimized - assert final_model._optimize_count == 0 # type: ignore + assert final_model._optimize_count == 0 @pytest.mark.parametrize( diff --git a/trieste/ask_tell_optimization.py b/trieste/ask_tell_optimization.py index 46dd6a8b6a..46053f66b0 100644 --- a/trieste/ask_tell_optimization.py +++ b/trieste/ask_tell_optimization.py @@ -20,8 +20,9 @@ from __future__ import annotations +from abc import ABC, abstractmethod from copy import deepcopy -from typing import Dict, Generic, Mapping, TypeVar, cast, overload +from typing import Dict, Generic, Mapping, Type, TypeVar, cast, overload from .models.utils import optimize_model_and_save_result @@ -47,7 +48,7 @@ write_summary_query_points, ) from .data import Dataset -from .models import TrainableProbabilisticModel +from .models import ProbabilisticModel, TrainableProbabilisticModel from .observer import OBJECTIVE from .space import SearchSpace from .types import State, Tag, TensorType @@ -60,16 +61,19 @@ SearchSpaceType = TypeVar("SearchSpaceType", bound=SearchSpace) """ Type variable bound to :class:`SearchSpace`. """ -TrainableProbabilisticModelType = TypeVar( - "TrainableProbabilisticModelType", bound=TrainableProbabilisticModel, contravariant=True +ProbabilisticModelType = TypeVar( + "ProbabilisticModelType", bound=ProbabilisticModel, contravariant=True ) -""" Contravariant type variable bound to :class:`TrainableProbabilisticModel`. """ +""" Contravariant type variable bound to :class:`ProbabilisticModel`. """ + +AskTellOptimizerType = TypeVar("AskTellOptimizerType") -class AskTellOptimizer(Generic[SearchSpaceType, TrainableProbabilisticModelType]): +class AskTellOptimizerABC(ABC, Generic[SearchSpaceType, ProbabilisticModelType]): """ This class provides Ask/Tell optimization interface. It is designed for those use cases when control of the optimization loop by Trieste is impossible or not desirable. + For the default use case with model training, refer to :class:`AskTellOptimizer`. For more details about the Bayesian Optimization routine, refer to :class:`BayesianOptimizer`. """ @@ -78,7 +82,7 @@ def __init__( self, search_space: SearchSpaceType, datasets: Mapping[Tag, Dataset], - models: Mapping[Tag, TrainableProbabilisticModelType], + models: Mapping[Tag, ProbabilisticModelType], *, fit_model: bool = True, ): @@ -89,10 +93,8 @@ def __init__( self, search_space: SearchSpaceType, datasets: Mapping[Tag, Dataset], - models: Mapping[Tag, TrainableProbabilisticModelType], - acquisition_rule: AcquisitionRule[ - TensorType, SearchSpaceType, TrainableProbabilisticModelType - ], + models: Mapping[Tag, ProbabilisticModelType], + acquisition_rule: AcquisitionRule[TensorType, SearchSpaceType, ProbabilisticModelType], *, fit_model: bool = True, ): @@ -103,9 +105,9 @@ def __init__( self, search_space: SearchSpaceType, datasets: Mapping[Tag, Dataset], - models: Mapping[Tag, TrainableProbabilisticModelType], + models: Mapping[Tag, ProbabilisticModelType], acquisition_rule: AcquisitionRule[ - State[StateType | None, TensorType], SearchSpaceType, TrainableProbabilisticModelType + State[StateType | None, TensorType], SearchSpaceType, ProbabilisticModelType ], acquisition_state: StateType | None, *, @@ -118,7 +120,7 @@ def __init__( self, search_space: SearchSpaceType, datasets: Dataset, - models: TrainableProbabilisticModelType, + models: ProbabilisticModelType, *, fit_model: bool = True, ): @@ -129,10 +131,8 @@ def __init__( self, search_space: SearchSpaceType, datasets: Dataset, - models: TrainableProbabilisticModelType, - acquisition_rule: AcquisitionRule[ - TensorType, SearchSpaceType, TrainableProbabilisticModelType - ], + models: ProbabilisticModelType, + acquisition_rule: AcquisitionRule[TensorType, SearchSpaceType, ProbabilisticModelType], *, fit_model: bool = True, ): @@ -143,9 +143,9 @@ def __init__( self, search_space: SearchSpaceType, datasets: Dataset, - models: TrainableProbabilisticModelType, + models: ProbabilisticModelType, acquisition_rule: AcquisitionRule[ - State[StateType | None, TensorType], SearchSpaceType, TrainableProbabilisticModelType + State[StateType | None, TensorType], SearchSpaceType, ProbabilisticModelType ], acquisition_state: StateType | None = None, *, @@ -157,11 +157,11 @@ def __init__( self, search_space: SearchSpaceType, datasets: Mapping[Tag, Dataset] | Dataset, - models: Mapping[Tag, TrainableProbabilisticModelType] | TrainableProbabilisticModelType, + models: Mapping[Tag, ProbabilisticModelType] | ProbabilisticModelType, acquisition_rule: AcquisitionRule[ TensorType | State[StateType | None, TensorType], SearchSpaceType, - TrainableProbabilisticModelType, + ProbabilisticModelType, ] | None = None, acquisition_state: StateType | None = None, @@ -202,7 +202,7 @@ def __init__( # reassure the type checker that everything is tagged datasets = cast(Dict[Tag, Dataset], datasets) - models = cast(Dict[Tag, TrainableProbabilisticModelType], models) + models = cast(Dict[Tag, ProbabilisticModelType], models) # Get set of dataset and model keys, ignoring any local tag index. That is, only the # global tag part is considered. @@ -228,7 +228,7 @@ def __init__( ) self._acquisition_rule = cast( - AcquisitionRule[TensorType, SearchSpaceType, TrainableProbabilisticModelType], + AcquisitionRule[TensorType, SearchSpaceType, ProbabilisticModelType], EfficientGlobalOptimization(), ) else: @@ -256,8 +256,7 @@ def __init__( tags = [tag, LocalizedTag.from_tag(tag).global_tag] _, dataset = get_value_for_tag(self._filtered_datasets, *tags) assert dataset is not None - model.update(dataset) - optimize_model_and_save_result(model, dataset) + self.update_model(model, dataset) summary_writer = logging.get_tensorboard_writer() if summary_writer: @@ -266,6 +265,13 @@ def __init__( self._datasets, self._models, initial_model_fitting_timer ) + @abstractmethod + def update_model(self, model: ProbabilisticModelType, dataset: Dataset) -> None: + """ + Update the model on the specified dataset, for example by training. + Called during the Tell stage and optionally at initial fitting. + """ + def __repr__(self) -> str: """Print-friendly string representation""" return f"""AskTellOptimizer({self._search_space!r}, {self._datasets!r}, @@ -288,12 +294,12 @@ def dataset(self) -> Dataset: raise ValueError(f"Expected a single dataset, found {len(datasets)}") @property - def models(self) -> Mapping[Tag, TrainableProbabilisticModelType]: + def models(self) -> Mapping[Tag, ProbabilisticModelType]: """The current models.""" return self._models @models.setter - def models(self, models: Mapping[Tag, TrainableProbabilisticModelType]) -> None: + def models(self, models: Mapping[Tag, ProbabilisticModelType]) -> None: """Update the current models.""" if models.keys() != self.models.keys(): raise ValueError( @@ -303,17 +309,17 @@ def models(self, models: Mapping[Tag, TrainableProbabilisticModelType]) -> None: self._models = dict(models) @property - def model(self) -> TrainableProbabilisticModel: + def model(self) -> ProbabilisticModel: """The current model when there is just one model.""" # Ignore local models. - models: Mapping[Tag, TrainableProbabilisticModel] = ignoring_local_tags(self.models) + models: Mapping[Tag, ProbabilisticModel] = ignoring_local_tags(self.models) if len(models) == 1: return next(iter(models.values())) else: raise ValueError(f"Expected a single model, found {len(models)}") @model.setter - def model(self, model: TrainableProbabilisticModelType) -> None: + def model(self, model: ProbabilisticModelType) -> None: """Update the current model, using the OBJECTIVE tag.""" if len(self.models) != 1: raise ValueError(f"Expected a single model, found {len(self.models)}") @@ -331,16 +337,17 @@ def acquisition_state(self) -> StateType | None: @classmethod def from_record( - cls, - record: Record[StateType] | FrozenRecord[StateType], + cls: Type[AskTellOptimizerType], + record: Record[StateType, ProbabilisticModelType] + | FrozenRecord[StateType, ProbabilisticModelType], search_space: SearchSpaceType, acquisition_rule: AcquisitionRule[ TensorType | State[StateType | None, TensorType], SearchSpaceType, - TrainableProbabilisticModelType, + ProbabilisticModelType, ] | None = None, - ) -> AskTellOptimizer[SearchSpaceType, TrainableProbabilisticModelType]: + ) -> AskTellOptimizerType: """Creates new :class:`~AskTellOptimizer` instance from provided optimization state. Model training isn't triggered upon creation of the instance. @@ -356,18 +363,18 @@ def from_record( # so the model was already trained # thus there is no need to train it again - # type ignore below is due to the fact that overloads don't allow - # optional acquisition_rule along with acquisition_state - return cls( + # type ignore below is because this relies on subclasses not overriding __init__ + # ones that do may also need to override this to get it to work + return cls( # type: ignore search_space, record.datasets, - cast(Mapping[Tag, TrainableProbabilisticModelType], record.models), - acquisition_rule=acquisition_rule, # type: ignore + record.models, + acquisition_rule=acquisition_rule, acquisition_state=record.acquisition_state, fit_model=False, ) - def to_record(self, copy: bool = True) -> Record[StateType]: + def to_record(self, copy: bool = True) -> Record[StateType, ProbabilisticModelType]: """Collects the current state of the optimization, which includes datasets, models and acquisition state (if applicable). @@ -391,7 +398,7 @@ def to_record(self, copy: bool = True) -> Record[StateType]: return Record(datasets=datasets_copy, models=models_copy, acquisition_state=state_copy) - def to_result(self, copy: bool = True) -> OptimizationResult[StateType]: + def to_result(self, copy: bool = True) -> OptimizationResult[StateType, ProbabilisticModelType]: """Converts current state of the optimization into a :class:`~trieste.data.OptimizationResult` object. @@ -400,7 +407,7 @@ def to_result(self, copy: bool = True) -> OptimizationResult[StateType]: modify the original state. :return: A :class:`~trieste.data.OptimizationResult` object. """ - record: Record[StateType] = self.to_record(copy=copy) + record: Record[StateType, ProbabilisticModelType] = self.to_record(copy=copy) return OptimizationResult(Ok(record), []) def ask(self) -> TensorType: @@ -470,8 +477,7 @@ def tell(self, new_data: Mapping[Tag, Dataset] | Dataset) -> None: # Always use the matching dataset to the model. If the model is # local, then the dataset should be too by this stage. dataset = self._filtered_datasets[tag] - model.update(dataset) - optimize_model_and_save_result(model, dataset) + self.update_model(model, dataset) summary_writer = logging.get_tensorboard_writer() if summary_writer: @@ -483,3 +489,30 @@ def tell(self, new_data: Mapping[Tag, Dataset] | Dataset) -> None: model_fitting_timer, self._observation_plot_dfs, ) + + +TrainableProbabilisticModelType = TypeVar( + "TrainableProbabilisticModelType", bound=TrainableProbabilisticModel, contravariant=True +) +""" Contravariant type variable bound to :class:`TrainableProbabilisticModel`. """ + + +class AskTellOptimizer(AskTellOptimizerABC[SearchSpaceType, TrainableProbabilisticModelType]): + """ + This class provides Ask/Tell optimization interface with the default model training + using the TrainableProbabilisticModel interface. + """ + + def update_model(self, model: TrainableProbabilisticModelType, dataset: Dataset) -> None: + model.update(dataset) + optimize_model_and_save_result(model, dataset) + + +class AskTellOptimizerNoTraining(AskTellOptimizerABC[SearchSpaceType, ProbabilisticModelType]): + """ + This class provides Ask/Tell optimization interface with no model training performed + during the Tell stage or at initialization. + """ + + def update_model(self, model: ProbabilisticModelType, dataset: Dataset) -> None: + pass diff --git a/trieste/bayesian_optimizer.py b/trieste/bayesian_optimizer.py index a45eeef588..0e489323a6 100644 --- a/trieste/bayesian_optimizer.py +++ b/trieste/bayesian_optimizer.py @@ -61,7 +61,11 @@ ) from .acquisition.utils import with_local_datasets from .data import Dataset -from .models import SupportsCovarianceWithTopFidelity, TrainableProbabilisticModel +from .models import ( + ProbabilisticModel, + SupportsCovarianceWithTopFidelity, + TrainableProbabilisticModel, +) from .objectives.utils import mk_batch_observer from .observer import OBJECTIVE, Observer from .space import SearchSpace @@ -75,6 +79,13 @@ SearchSpaceType = TypeVar("SearchSpaceType", bound=SearchSpace) """ Type variable bound to :class:`SearchSpace`. """ +ProbabilisticModelType = TypeVar( + "ProbabilisticModelType", + bound=ProbabilisticModel, + covariant=True, +) +""" Covariant type variable bound to :class:`ProbabilisticModel`. """ + TrainableProbabilisticModelType = TypeVar( "TrainableProbabilisticModelType", bound=TrainableProbabilisticModel, contravariant=True ) @@ -88,13 +99,13 @@ @dataclass(frozen=True) -class Record(Generic[StateType]): +class Record(Generic[StateType, ProbabilisticModelType]): """Container to record the state of each step of the optimization process.""" datasets: Mapping[Tag, Dataset] """ The known data from the observer. """ - models: Mapping[Tag, TrainableProbabilisticModel] + models: Mapping[Tag, ProbabilisticModelType] """ The models over the :attr:`datasets`. """ acquisition_state: StateType | None @@ -111,16 +122,16 @@ def dataset(self) -> Dataset: raise ValueError(f"Expected a single dataset, found {len(datasets)}") @property - def model(self) -> TrainableProbabilisticModel: + def model(self) -> ProbabilisticModelType: """The model when there is just one dataset.""" # Ignore local models. - models: Mapping[Tag, TrainableProbabilisticModel] = ignoring_local_tags(self.models) + models: Mapping[Tag, ProbabilisticModelType] = ignoring_local_tags(self.models) if len(models) == 1: return next(iter(models.values())) else: raise ValueError(f"Expected a single model, found {len(models)}") - def save(self, path: Path | str) -> FrozenRecord[StateType]: + def save(self, path: Path | str) -> FrozenRecord[StateType, ProbabilisticModelType]: """Save the record to disk. Will overwrite any existing file at the same path.""" Path(path).parent.mkdir(exist_ok=True, parents=True) with open(path, "wb") as f: @@ -129,7 +140,7 @@ def save(self, path: Path | str) -> FrozenRecord[StateType]: @dataclass(frozen=True) -class FrozenRecord(Generic[StateType]): +class FrozenRecord(Generic[StateType, ProbabilisticModelType]): """ A Record container saved on disk. @@ -140,7 +151,7 @@ class FrozenRecord(Generic[StateType]): path: Path """ The path to the pickled Record. """ - def load(self) -> Record[StateType]: + def load(self) -> Record[StateType, ProbabilisticModelType]: """Load the record into memory.""" with open(self.path, "rb") as f: return dill.load(f) @@ -151,7 +162,7 @@ def datasets(self) -> Mapping[Tag, Dataset]: return self.load().datasets @property - def models(self) -> Mapping[Tag, TrainableProbabilisticModel]: + def models(self) -> Mapping[Tag, ProbabilisticModelType]: """The models over the :attr:`datasets`.""" return self.load().models @@ -166,7 +177,7 @@ def dataset(self) -> Dataset: return self.load().dataset @property - def model(self) -> TrainableProbabilisticModel: + def model(self) -> ProbabilisticModelType: """The model when there is just one dataset.""" return self.load().model @@ -174,16 +185,18 @@ def model(self) -> TrainableProbabilisticModel: # this should be a generic NamedTuple, but mypy doesn't support them # https://github.com/python/mypy/issues/685 @dataclass(frozen=True) -class OptimizationResult(Generic[StateType]): +class OptimizationResult(Generic[StateType, ProbabilisticModelType]): """The final result, and the historical data of the optimization process.""" - final_result: Result[Record[StateType]] + final_result: Result[Record[StateType, ProbabilisticModelType]] """ The final result of the optimization process. This contains either a :class:`Record` or an exception. """ - history: list[Record[StateType] | FrozenRecord[StateType]] + history: list[ + Record[StateType, ProbabilisticModelType] | FrozenRecord[StateType, ProbabilisticModelType] + ] r""" The history of the :class:`Record`\ s from each step of the optimization process. These :class:`Record`\ s are created at the *start* of each loop, and as such will never @@ -200,7 +213,13 @@ def step_filename(step: int, num_steps: int) -> str: def astuple( self, - ) -> tuple[Result[Record[StateType]], list[Record[StateType] | FrozenRecord[StateType]]]: + ) -> tuple[ + Result[Record[StateType, ProbabilisticModelType]], + list[ + Record[StateType, ProbabilisticModelType] + | FrozenRecord[StateType, ProbabilisticModelType] + ], + ]: """ **Note:** In contrast to the standard library function :func:`dataclasses.astuple`, this method does *not* deepcopy instance attributes. @@ -264,7 +283,7 @@ def try_get_optimal_point(self) -> tuple[TensorType, TensorType, TensorType]: arg_min_idx = tf.squeeze(tf.argmin(dataset.observations, axis=0)) return dataset.query_points[arg_min_idx], dataset.observations[arg_min_idx], arg_min_idx - def try_get_final_models(self) -> Mapping[Tag, TrainableProbabilisticModel]: + def try_get_final_models(self) -> Mapping[Tag, ProbabilisticModelType]: """ Convenience method to attempt to get the final models. @@ -273,7 +292,7 @@ def try_get_final_models(self) -> Mapping[Tag, TrainableProbabilisticModel]: """ return self.final_result.unwrap().models - def try_get_final_model(self) -> TrainableProbabilisticModel: + def try_get_final_model(self) -> ProbabilisticModelType: """ Convenience method to attempt to get the final model for a single model run. @@ -290,7 +309,7 @@ def try_get_final_model(self) -> TrainableProbabilisticModel: raise ValueError(f"Expected single model, found {len(models)}") @property - def loaded_history(self) -> list[Record[StateType]]: + def loaded_history(self) -> list[Record[StateType, ProbabilisticModelType]]: """The history of the optimization process loaded into memory.""" return [record if isinstance(record, Record) else record.load() for record in self.history] @@ -310,7 +329,9 @@ def save(self, base_path: Path | str) -> None: record.save(record_path) @classmethod - def from_path(cls, base_path: Path | str) -> OptimizationResult[StateType]: + def from_path( + cls, base_path: Path | str + ) -> OptimizationResult[StateType, ProbabilisticModelType]: """Load a previously saved OptimizationResult.""" try: with open(Path(base_path) / cls.RESULTS_FILENAME, "rb") as f: @@ -318,9 +339,10 @@ def from_path(cls, base_path: Path | str) -> OptimizationResult[StateType]: except FileNotFoundError as e: result = Err(e) - history: list[Record[StateType] | FrozenRecord[StateType]] = [ - FrozenRecord(file) for file in sorted(Path(base_path).glob(cls.STEP_GLOB)) - ] + history: list[ + Record[StateType, ProbabilisticModelType] + | FrozenRecord[StateType, ProbabilisticModelType] + ] = [FrozenRecord(file) for file in sorted(Path(base_path).glob(cls.STEP_GLOB))] return cls(result, history) @@ -359,7 +381,7 @@ def optimize( EarlyStopCallback[TrainableProbabilisticModel, object] ] = None, start_step: int = 0, - ) -> OptimizationResult[None]: + ) -> OptimizationResult[None, TrainableProbabilisticModel]: ... @overload @@ -383,7 +405,7 @@ def optimize( # this should really be OptimizationResult[None], but tf.Tensor is untyped so the type # checker can't differentiate between TensorType and State[S | None, TensorType], and # the return types clash. object is close enough to None that object will do. - ) -> OptimizationResult[object]: + ) -> OptimizationResult[object, TrainableProbabilisticModelType]: ... @overload @@ -404,7 +426,7 @@ def optimize( EarlyStopCallback[TrainableProbabilisticModelType, object] ] = None, start_step: int = 0, - ) -> OptimizationResult[object]: + ) -> OptimizationResult[object, TrainableProbabilisticModelType]: ... @overload @@ -426,7 +448,7 @@ def optimize( EarlyStopCallback[TrainableProbabilisticModelType, StateType] ] = None, start_step: int = 0, - ) -> OptimizationResult[StateType]: + ) -> OptimizationResult[StateType, TrainableProbabilisticModelType]: ... @overload @@ -448,7 +470,7 @@ def optimize( EarlyStopCallback[TrainableProbabilisticModelType, StateType] ] = None, start_step: int = 0, - ) -> OptimizationResult[StateType]: + ) -> OptimizationResult[StateType, TrainableProbabilisticModelType]: ... @overload @@ -466,7 +488,7 @@ def optimize( EarlyStopCallback[TrainableProbabilisticModel, object] ] = None, start_step: int = 0, - ) -> OptimizationResult[None]: + ) -> OptimizationResult[None, TrainableProbabilisticModel]: ... @overload @@ -487,7 +509,7 @@ def optimize( EarlyStopCallback[TrainableProbabilisticModelType, object] ] = None, start_step: int = 0, - ) -> OptimizationResult[object]: + ) -> OptimizationResult[object, TrainableProbabilisticModelType]: ... @overload @@ -508,7 +530,7 @@ def optimize( EarlyStopCallback[TrainableProbabilisticModelType, object] ] = None, start_step: int = 0, - ) -> OptimizationResult[object]: + ) -> OptimizationResult[object, TrainableProbabilisticModelType]: ... @overload @@ -530,7 +552,7 @@ def optimize( EarlyStopCallback[TrainableProbabilisticModelType, StateType] ] = None, start_step: int = 0, - ) -> OptimizationResult[StateType]: + ) -> OptimizationResult[StateType, TrainableProbabilisticModelType]: ... @overload @@ -552,7 +574,7 @@ def optimize( EarlyStopCallback[TrainableProbabilisticModelType, StateType] ] = None, start_step: int = 0, - ) -> OptimizationResult[StateType]: + ) -> OptimizationResult[StateType, TrainableProbabilisticModelType]: ... def optimize( @@ -576,7 +598,10 @@ def optimize( EarlyStopCallback[TrainableProbabilisticModelType, StateType] ] = None, start_step: int = 0, - ) -> OptimizationResult[StateType] | OptimizationResult[None]: + ) -> ( + OptimizationResult[StateType, TrainableProbabilisticModelType] + | OptimizationResult[None, TrainableProbabilisticModelType] + ): """ Attempt to find the minimizer of the ``observer`` in the ``search_space`` (both specified at :meth:`__init__`). This is the central implementation of the Bayesian optimization loop. @@ -680,7 +705,10 @@ def optimize( SearchSpaceType, TrainableProbabilisticModelType ]() - history: list[FrozenRecord[StateType] | Record[StateType]] = [] + history: list[ + FrozenRecord[StateType, TrainableProbabilisticModelType] + | Record[StateType, TrainableProbabilisticModelType] + ] = [] query_plot_dfs: dict[int, pd.DataFrame] = {} observation_plot_dfs = observation_plot_init(datasets) @@ -841,10 +869,10 @@ def optimize( def continue_optimization( self, num_steps: int, - optimization_result: OptimizationResult[StateType], + optimization_result: OptimizationResult[StateType, TrainableProbabilisticModelType], *args: Any, **kwargs: Any, - ) -> OptimizationResult[StateType]: + ) -> OptimizationResult[StateType, TrainableProbabilisticModelType]: """ Continue a previous optimization that either failed, was terminated early, or which you simply wish to run for more steps. @@ -861,7 +889,10 @@ def continue_optimization( `optimization_result` (including the `final_result` if that was successful) and any new records. """ - history: list[Record[StateType] | FrozenRecord[StateType]] = [] + history: list[ + Record[StateType, TrainableProbabilisticModelType] + | FrozenRecord[StateType, TrainableProbabilisticModelType] + ] = [] history.extend(optimization_result.history) if optimization_result.final_result.is_ok: history.append(optimization_result.final_result.unwrap()) @@ -910,7 +941,7 @@ def write_summary_init( def write_summary_initial_model_fit( datasets: Mapping[Tag, Dataset], - models: Mapping[Tag, TrainableProbabilisticModel], + models: Mapping[Tag, ProbabilisticModel], model_fitting_timer: Timer, ) -> None: """Write TensorBoard summary for the model fitting to the initial data.""" @@ -961,7 +992,7 @@ def observation_plot_init( def write_summary_observations( datasets: Mapping[Tag, Dataset], - models: Mapping[Tag, TrainableProbabilisticModel], + models: Mapping[Tag, ProbabilisticModel], tagged_output: Mapping[Tag, TensorType], model_fitting_timer: Timer, observation_plot_dfs: MutableMapping[Tag, pd.DataFrame], @@ -1058,7 +1089,7 @@ def write_summary_observations( def write_summary_query_points( datasets: Mapping[Tag, Dataset], - models: Mapping[Tag, TrainableProbabilisticModel], + models: Mapping[Tag, ProbabilisticModel], search_space: SearchSpace, query_points: TensorType, query_point_generation_timer: Timer, diff --git a/trieste/experimental/plotting/plotting.py b/trieste/experimental/plotting/plotting.py index 04bfc52745..521b34f2c0 100644 --- a/trieste/experimental/plotting/plotting.py +++ b/trieste/experimental/plotting/plotting.py @@ -33,6 +33,7 @@ from trieste.acquisition import AcquisitionFunction from trieste.acquisition.multi_objective.dominance import non_dominated from trieste.bayesian_optimizer import FrozenRecord, Record, StateType +from trieste.models import ProbabilisticModel from trieste.observer import OBJECTIVE from trieste.space import TaggedMultiSearchSpace from trieste.types import TensorType @@ -551,7 +552,7 @@ def plot_trust_region_history_2d( obj_func: Callable[[TensorType], TensorType], mins: TensorType, maxs: TensorType, - history: Record[StateType] | FrozenRecord[StateType], + history: Record[StateType, ProbabilisticModel] | FrozenRecord[StateType, ProbabilisticModel], num_query_points: Optional[int] = None, num_init: Optional[int] = None, ) -> tuple[Optional[Figure], Optional[Axes]]: