Source code for alf_tools.optimizer.acquisition_functions.botorch_samplers
# Copyright 2026 InstaDeep Ltd. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Monte Carlo samplers for BoTorch acquisition functions.
This module provides sampler configurations used by BoTorch acquisition functions
to approximate expectations via Monte Carlo sampling.
"""
from typing import Literal
import torch
from botorch.sampling.normal import IIDNormalSampler, SobolQMCNormalSampler
# Type for supported MC sampler types
MCSamplerType = Literal["sobol", "iid"]
[docs]
class BoTorchMCSampler:
"""Wrapper for BoTorch Monte Carlo samplers.
This class provides a unified interface to BoTorch's MC samplers, which are
used by acquisition functions to approximate expectations via Monte Carlo.
Different samplers offer different trade-offs:
- **Sobol QMC**: Quasi-Monte Carlo with better coverage (recommended)
- **IID**: Independent sampling (faster but less efficient)
The sampler is not a search function itself - it's a configuration object
that gets passed to BoTorch acquisition functions to control how they
approximate the acquisition value.
Example:
>>> from alf_tools.optimizer.acquisition_functions import BoTorchMCSampler
>>> from alf_tools.optimizer.acquisition_functions import BoTorchAcquisition
>>>
>>> # Create Sobol QMC sampler
>>> sampler = BoTorchMCSampler(sampler_type="sobol", num_samples=512)
>>>
>>> # Pass to acquisition function
>>> acq_fn = BoTorchAcquisition(
... acquisition_type="qEI",
... sampler=sampler,
... bounds=[[0, 1], [0, 1]]
... )
Args:
sampler_type: Type of sampler to use. Options:
- "sobol": Sobol QMC sampler (better coverage, recommended)
- "iid": Independent sampling (faster)
num_samples: Number of MC samples to draw. More samples = more accurate
but slower. Typical values: 256-512 for Sobol, 1024+ for IID.
seed: Random seed for reproducibility. If None, uses random seed.
Raises:
ValueError: If sampler_type is not 'sobol' or 'iid', or if num_samples
is not positive.
"""
def __init__(
self,
sampler_type: MCSamplerType = "sobol",
num_samples: int = 512,
seed: int | None = None,
):
"""Initialize MC sampler configuration.
Raises:
ValueError: If sampler_type is not valid or num_samples is not positive.
"""
self.sampler_type = sampler_type
self.num_samples = num_samples
self.seed = seed
# Validate inputs
if sampler_type not in ["sobol", "iid"]:
raise ValueError(f"Invalid sampler_type: {sampler_type}. Must be 'sobol' or 'iid'")
if num_samples <= 0:
raise ValueError(f"num_samples must be positive, got {num_samples}")
[docs]
def get_sampler(self):
"""Create and return the configured BoTorch sampler.
Returns:
A BoTorch sampler instance (SobolQMCNormalSampler or IIDNormalSampler).
Raises:
ValueError: If sampler_type is unknown.
Example:
>>> sampler_config = BoTorchMCSampler("sobol", 512)
>>> sampler = sampler_config.get_sampler()
>>> # Use with BoTorch acquisition function
"""
if self.sampler_type == "sobol":
return SobolQMCNormalSampler(
sample_shape=torch.Size([self.num_samples]),
seed=self.seed,
)
elif self.sampler_type == "iid":
return IIDNormalSampler(
sample_shape=torch.Size([self.num_samples]),
seed=self.seed,
)
else:
raise ValueError(f"Unknown sampler type: {self.sampler_type}")
def __repr__(self) -> str:
"""String representation of the sampler configuration.
Returns:
String describing the sampler configuration.
"""
return (
f"BoTorchMCSampler(type={self.sampler_type}, "
f"num_samples={self.num_samples}, seed={self.seed})"
)