Skip to content

BayesianOpt

sgptools.methods.BayesianOpt

Bases: Method

Informative sensor placement / path optimization using Bayesian Optimization over a continuous search space.

A Bayesian optimization loop is run over a flattened vector containing all sensing locations for all robots. At each iteration, the candidate locations are reshaped, optionally transformed (for IPP / FoV modeling), evaluated under a GP-based objective (e.g. mutual information), and penalized by any constraints provided by the Transform.

Refer to the following papers for more details
  • Vivaldini et al., 2019. UAV route planning for active disease classification.
  • Francis et al., 2019. Occupancy map building through Bayesian exploration.

Attributes:

Name Type Description
objective Objective

Objective object encapsulating the GP-based information measure to maximize.

transform Transform | None

Optional transform applied to candidate sensing locations before evaluating the objective.

pbounds Dict[str, Tuple[float, float]]

Dictionary mapping parameter names 'x0', 'x1', ... to their search bounds (lower, upper), as required by bayes_opt.BayesianOptimization.

Source code in sgptools/methods.py
class BayesianOpt(Method):
    """Informative sensor placement / path optimization using Bayesian
    Optimization over a continuous search space.

    A Bayesian optimization loop is run over a flattened vector containing all
    sensing locations for all robots. At each iteration, the candidate
    locations are reshaped, optionally transformed (for IPP / FoV modeling),
    evaluated under a GP-based objective (e.g. mutual information), and
    penalized by any constraints provided by the `Transform`.

    Refer to the following papers for more details:
        - Vivaldini et al., 2019. *UAV route planning for active disease
        classification.*
        - Francis et al., 2019. *Occupancy map building through Bayesian
        exploration.*

    Attributes:
        objective (Objective):
            Objective object encapsulating the GP-based information measure to
            maximize.
        transform (Transform | None):
            Optional transform applied to candidate sensing locations before
            evaluating the objective.
        pbounds (Dict[str, Tuple[float, float]]):
            Dictionary mapping parameter names `'x0', 'x1', ...` to their search
            bounds `(lower, upper)`, as required by `bayes_opt.BayesianOptimization`.
    """

    def __init__(self,
                 num_sensing: int,
                 X_objective: np.ndarray,
                 kernel: gpflow.kernels.Kernel,
                 noise_variance: float,
                 transform: Optional[Transform] = None,
                 num_robots: int = 1,
                 X_candidates: Optional[np.ndarray] = None,
                 num_dim: Optional[int] = None,
                 objective: Union[str, Objective] = 'SLogMI',
                 **kwargs: Any):
        """Initialize a Bayesian optimization-based method.

        Args:
            num_sensing (int):
                Number of sensing locations per robot to optimize.
            X_objective (np.ndarray):
                Array of shape `(n, d)` used to define the underlying objective.
                The bounds of this set are used to define the BO search space.
            kernel (gpflow.kernels.Kernel):
                GPflow kernel used inside the objective.
            noise_variance (float):
                Observation noise variance used inside the objective.
            transform (Transform | None):
                Optional transform applied to the candidate solution before
                evaluating the objective (and constraints). For example, an
                `IPPTransform`.
            num_robots (int):
                Number of robots / agents. Defaults to 1.
            X_candidates (np.ndarray | None):
                Optional discrete candidate set of locations with shape `(c, d)`.
                If provided, the final continuous solution is snapped to the
                nearest candidate locations.
            num_dim (int | None):
                Dimensionality of the sensing locations. If `None`, defaults to
                `X_objective.shape[-1]`.
            objective (str | Objective):
                Objective specification. Either a string key understood by
                `get_objective` (e.g. `'SLogMI'`, `'MI'`) or an already-instantiated
                `Objective` object.
            **kwargs (Any):
                Additional keyword arguments forwarded to the objective constructor
                when `objective` is a string.
        """
        super().__init__(num_sensing, X_objective, kernel, noise_variance,
                         transform, num_robots, X_candidates, num_dim)
        self.transform = transform

        if isinstance(objective, str):
            self.objective = get_objective(objective)(X_objective, kernel,
                                                      noise_variance, **kwargs)
        else:
            self.objective = objective

        # Use the coordinate-wise min/max of X_objective as BO bounds
        pbounds_dims: List[Tuple[float, float]] = []
        for i in range(self.num_dim):
            pbounds_dims.append(
                (np.min(X_objective[:, i]), np.max(X_objective[:, i])))
        self.pbounds: Dict[str, Tuple[float, float]] = {}
        for i in range(self.num_dim * self.num_sensing * self.num_robots):
            self.pbounds[f'x{i}'] = pbounds_dims[i % self.num_dim]

    def update(self, kernel: gpflow.kernels.Kernel,
               noise_variance: float) -> None:
        """Update the kernel and noise variance used by the underlying objective.

        Args:
            kernel (gpflow.kernels.Kernel):
                New GPflow kernel instance.
            noise_variance (float):
                New observation noise variance.
        """
        self.objective.update(kernel, noise_variance)

    def get_hyperparameters(self) -> Tuple[gpflow.kernels.Kernel, float]:
        """Return the current kernel and noise variance used by the objective.

        Returns:
            Tuple[gpflow.kernels.Kernel, float]:
                A deep copy of the kernel and the current noise variance.
        """
        return deepcopy(self.objective.kernel), \
               self.objective.noise_variance

    def optimize(self,
                 max_steps: int = 50,
                 init_points: int = 10,
                 verbose: bool = False,
                 seed: Optional[int] = None,
                 **kwargs: Any) -> np.ndarray:
        """Run Bayesian optimization to obtain informative sensing locations.

        Args:
            max_steps (int):
                Number of Bayesian optimization iterations after the initial random
                exploration. Defaults to 50.
            init_points (int):
                Number of purely random evaluations before BO starts. Defaults to 10.
            verbose (bool):
                If `True`, print progress messages from `BayesianOptimization`.
            seed (int | None):
                Optional random seed to make BO reproducible.
            **kwargs (Any):
                Extra keyword arguments forwarded to `BayesianOptimization`
                (currently unused in this wrapper, but accepted for flexibility).

        Returns:
            np.ndarray:
                Array of shape `(num_robots, num_sensing, num_dim)` containing the
                optimized sensing locations in the original coordinate space.
        """
        verbose = 1 if verbose else 0
        optimizer = BayesianOptimization(f=self._objective,
                                         pbounds=self.pbounds,
                                         verbose=verbose,
                                         random_state=seed,
                                         allow_duplicate_points=True)
        optimizer.maximize(init_points=init_points, n_iter=max_steps)

        sol: List[float] = []
        for i in range(self.num_dim * self.num_sensing * self.num_robots):
            sol.append(optimizer.max['params'][f'x{i}'])

        # Reshape BO solution to (total_points, num_dim)
        sol_np = np.array(sol).reshape(-1, self.num_dim)
        if self.transform is not None:
            # Use the transform for constraints and internal path logic,
            # but disable sensor model expansion (e.g., FoV) when returning
            # waypoint locations.
            sol_np = self.transform.expand(sol_np,
                                           expand_sensor_model=False)

            if not isinstance(sol_np, np.ndarray):
                sol_np = sol_np.numpy()

        # Optionally snap to candidate set
        if self.X_candidates is not None:
            sol_np = cont2disc(sol_np, self.X_candidates)

        sol_np = sol_np.reshape(self.num_robots, -1, self.num_dim)
        return sol_np

    def _objective(self, **kwargs: float) -> float:
        """Objective function passed to `BayesianOptimization`.

        Parameters are expected as a flattened dictionary `{ 'x0': ..., 'x1': ... }`,
        which is reshaped into `(num_sensing * num_robots, num_dim)` to form
        continuous sensing locations. The method:

        1. Reshapes the flattened vector into locations.
        2. Optionally applies the `transform` (including constraints).
        3. Evaluates the GP-based objective.
        4. Adds the constraint penalty returned by the transform.
        5. Returns the scalar objective as a Python float.

        The underlying objective is *maximized*. Transform constraints are
        expected to return non-positive values, so larger violations produce
        more negative penalties.

        Args:
            **kwargs (float):
                Flattened coordinates keyed by `'x0', 'x1', ...`.

        Returns:
            float:
                Objective value to be maximized by `BayesianOptimization`.
        """
        X_list: List[float] = []
        for i in range(len(kwargs)):
            X_list.append(kwargs[f'x{i}'])
        X = np.array(X_list).reshape(-1, self.num_dim)

        constraint_penality: float = 0.0
        if self.transform is not None:
            X_expanded = self.transform.expand(X)
            constraint_penality = self.transform.constraints(X)
            reward = self.objective(X_expanded)  # maximize
        else:
            reward = self.objective(X)  # maximize

        # Transform constraints are typically <= 0; adding them penalizes violations.
        reward += constraint_penality
        return reward.numpy()

__init__(num_sensing, X_objective, kernel, noise_variance, transform=None, num_robots=1, X_candidates=None, num_dim=None, objective='SLogMI', **kwargs)

Initialize a Bayesian optimization-based method.

Parameters:

Name Type Description Default
num_sensing int

Number of sensing locations per robot to optimize.

required
X_objective ndarray

Array of shape (n, d) used to define the underlying objective. The bounds of this set are used to define the BO search space.

required
kernel Kernel

GPflow kernel used inside the objective.

required
noise_variance float

Observation noise variance used inside the objective.

required
transform Transform | None

Optional transform applied to the candidate solution before evaluating the objective (and constraints). For example, an IPPTransform.

None
num_robots int

Number of robots / agents. Defaults to 1.

1
X_candidates ndarray | None

Optional discrete candidate set of locations with shape (c, d). If provided, the final continuous solution is snapped to the nearest candidate locations.

None
num_dim int | None

Dimensionality of the sensing locations. If None, defaults to X_objective.shape[-1].

None
objective str | Objective

Objective specification. Either a string key understood by get_objective (e.g. 'SLogMI', 'MI') or an already-instantiated Objective object.

'SLogMI'
**kwargs Any

Additional keyword arguments forwarded to the objective constructor when objective is a string.

{}
Source code in sgptools/methods.py
def __init__(self,
             num_sensing: int,
             X_objective: np.ndarray,
             kernel: gpflow.kernels.Kernel,
             noise_variance: float,
             transform: Optional[Transform] = None,
             num_robots: int = 1,
             X_candidates: Optional[np.ndarray] = None,
             num_dim: Optional[int] = None,
             objective: Union[str, Objective] = 'SLogMI',
             **kwargs: Any):
    """Initialize a Bayesian optimization-based method.

    Args:
        num_sensing (int):
            Number of sensing locations per robot to optimize.
        X_objective (np.ndarray):
            Array of shape `(n, d)` used to define the underlying objective.
            The bounds of this set are used to define the BO search space.
        kernel (gpflow.kernels.Kernel):
            GPflow kernel used inside the objective.
        noise_variance (float):
            Observation noise variance used inside the objective.
        transform (Transform | None):
            Optional transform applied to the candidate solution before
            evaluating the objective (and constraints). For example, an
            `IPPTransform`.
        num_robots (int):
            Number of robots / agents. Defaults to 1.
        X_candidates (np.ndarray | None):
            Optional discrete candidate set of locations with shape `(c, d)`.
            If provided, the final continuous solution is snapped to the
            nearest candidate locations.
        num_dim (int | None):
            Dimensionality of the sensing locations. If `None`, defaults to
            `X_objective.shape[-1]`.
        objective (str | Objective):
            Objective specification. Either a string key understood by
            `get_objective` (e.g. `'SLogMI'`, `'MI'`) or an already-instantiated
            `Objective` object.
        **kwargs (Any):
            Additional keyword arguments forwarded to the objective constructor
            when `objective` is a string.
    """
    super().__init__(num_sensing, X_objective, kernel, noise_variance,
                     transform, num_robots, X_candidates, num_dim)
    self.transform = transform

    if isinstance(objective, str):
        self.objective = get_objective(objective)(X_objective, kernel,
                                                  noise_variance, **kwargs)
    else:
        self.objective = objective

    # Use the coordinate-wise min/max of X_objective as BO bounds
    pbounds_dims: List[Tuple[float, float]] = []
    for i in range(self.num_dim):
        pbounds_dims.append(
            (np.min(X_objective[:, i]), np.max(X_objective[:, i])))
    self.pbounds: Dict[str, Tuple[float, float]] = {}
    for i in range(self.num_dim * self.num_sensing * self.num_robots):
        self.pbounds[f'x{i}'] = pbounds_dims[i % self.num_dim]

get_hyperparameters()

Return the current kernel and noise variance used by the objective.

Returns:

Type Description
Tuple[Kernel, float]

Tuple[gpflow.kernels.Kernel, float]: A deep copy of the kernel and the current noise variance.

Source code in sgptools/methods.py
def get_hyperparameters(self) -> Tuple[gpflow.kernels.Kernel, float]:
    """Return the current kernel and noise variance used by the objective.

    Returns:
        Tuple[gpflow.kernels.Kernel, float]:
            A deep copy of the kernel and the current noise variance.
    """
    return deepcopy(self.objective.kernel), \
           self.objective.noise_variance

optimize(max_steps=50, init_points=10, verbose=False, seed=None, **kwargs)

Run Bayesian optimization to obtain informative sensing locations.

Parameters:

Name Type Description Default
max_steps int

Number of Bayesian optimization iterations after the initial random exploration. Defaults to 50.

50
init_points int

Number of purely random evaluations before BO starts. Defaults to 10.

10
verbose bool

If True, print progress messages from BayesianOptimization.

False
seed int | None

Optional random seed to make BO reproducible.

None
**kwargs Any

Extra keyword arguments forwarded to BayesianOptimization (currently unused in this wrapper, but accepted for flexibility).

{}

Returns:

Type Description
ndarray

np.ndarray: Array of shape (num_robots, num_sensing, num_dim) containing the optimized sensing locations in the original coordinate space.

Source code in sgptools/methods.py
def optimize(self,
             max_steps: int = 50,
             init_points: int = 10,
             verbose: bool = False,
             seed: Optional[int] = None,
             **kwargs: Any) -> np.ndarray:
    """Run Bayesian optimization to obtain informative sensing locations.

    Args:
        max_steps (int):
            Number of Bayesian optimization iterations after the initial random
            exploration. Defaults to 50.
        init_points (int):
            Number of purely random evaluations before BO starts. Defaults to 10.
        verbose (bool):
            If `True`, print progress messages from `BayesianOptimization`.
        seed (int | None):
            Optional random seed to make BO reproducible.
        **kwargs (Any):
            Extra keyword arguments forwarded to `BayesianOptimization`
            (currently unused in this wrapper, but accepted for flexibility).

    Returns:
        np.ndarray:
            Array of shape `(num_robots, num_sensing, num_dim)` containing the
            optimized sensing locations in the original coordinate space.
    """
    verbose = 1 if verbose else 0
    optimizer = BayesianOptimization(f=self._objective,
                                     pbounds=self.pbounds,
                                     verbose=verbose,
                                     random_state=seed,
                                     allow_duplicate_points=True)
    optimizer.maximize(init_points=init_points, n_iter=max_steps)

    sol: List[float] = []
    for i in range(self.num_dim * self.num_sensing * self.num_robots):
        sol.append(optimizer.max['params'][f'x{i}'])

    # Reshape BO solution to (total_points, num_dim)
    sol_np = np.array(sol).reshape(-1, self.num_dim)
    if self.transform is not None:
        # Use the transform for constraints and internal path logic,
        # but disable sensor model expansion (e.g., FoV) when returning
        # waypoint locations.
        sol_np = self.transform.expand(sol_np,
                                       expand_sensor_model=False)

        if not isinstance(sol_np, np.ndarray):
            sol_np = sol_np.numpy()

    # Optionally snap to candidate set
    if self.X_candidates is not None:
        sol_np = cont2disc(sol_np, self.X_candidates)

    sol_np = sol_np.reshape(self.num_robots, -1, self.num_dim)
    return sol_np

update(kernel, noise_variance)

Update the kernel and noise variance used by the underlying objective.

Parameters:

Name Type Description Default
kernel Kernel

New GPflow kernel instance.

required
noise_variance float

New observation noise variance.

required
Source code in sgptools/methods.py
def update(self, kernel: gpflow.kernels.Kernel,
           noise_variance: float) -> None:
    """Update the kernel and noise variance used by the underlying objective.

    Args:
        kernel (gpflow.kernels.Kernel):
            New GPflow kernel instance.
        noise_variance (float):
            New observation noise variance.
    """
    self.objective.update(kernel, noise_variance)