Skip to content

ContinuousSGP

sgptools.methods.ContinuousSGP

Bases: Method

Informative sensing / path optimization via direct optimization of Sparse Gaussian Process (SGP) inducing points.

This method treats the inducing locations of an AugmentedSGPR model as the decision variables and optimizes them with respect to the SGP's ELBO (or another internal objective implemented by AugmentedSGPR).

References

  • Jakkala & Akella, 2024. Multi-Robot Informative Path Planning from Regression with Sparse Gaussian Processes.
  • Jakkala & Akella, 2025. Fully differentiable sensor placement and informative path planning.

Attributes

sgpr: AugmentedSGPR model whose inducing points are being optimized.

Source code in sgptools/methods.py
class ContinuousSGP(Method):
    """
    Informative sensing / path optimization via direct optimization of
    Sparse Gaussian Process (SGP) inducing points.

    This method treats the inducing locations of an `AugmentedSGPR` model as
    the decision variables and optimizes them with respect to the SGP's ELBO
    (or another internal objective implemented by `AugmentedSGPR`).

    References
    ----------
    - Jakkala & Akella, 2024. *Multi-Robot Informative Path Planning from
      Regression with Sparse Gaussian Processes.*
    - Jakkala & Akella, 2025. *Fully differentiable sensor placement and 
      informative path planning.*

    Attributes
    ----------
    sgpr:
        `AugmentedSGPR` model whose inducing points are being optimized.
    """

    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,
                 X_init: Optional[np.ndarray] = None,
                 X_time: Optional[np.ndarray] = None,
                 orientation: bool = False,
                 **kwargs: Any):
        """
        Initialize a continuous SGP-based optimization method.

        Parameters
        ----------
        num_sensing:
            Number of inducing points (sensing locations) per robot.
        X_objective:
            Array of shape `(n, d)` used to define the spatial domain and
            training inputs for the SGP.
        kernel:
            GPflow kernel for the SGP model.
        noise_variance:
            Observation noise variance for the SGP model.
        transform:
            Optional `Transform` to apply to inducing points for IPP or FoV
            modeling. Passed directly into `AugmentedSGPR`.
        num_robots:
            Number of robots / agents. The total number of inducing points is
            `num_sensing * num_robots`. Defaults to 1.
        X_candidates:
            Optional candidate set `(c, d)` used to snap the final continuous
            inducing locations to discrete locations.
        num_dim:
            Dimensionality of sensing locations. If `None`, defaults to
            `X_objective.shape[-1]`, or to `X_init.shape[-1]` if an initial
            solution is provided.
        X_init:
            Initial inducing points with shape `(num_sensing * num_robots, d)`.
            If `None`, points are chosen via `get_inducing_pts`. If given,
            its dimensionality overrides `num_dim`.
        X_time:
            Optional temporal coordinates (e.g. for spatio-temporal models),
            passed as `inducing_variable_time` to `AugmentedSGPR`.
        orientation:
            If `True` and `X_init` is not provided, `get_inducing_pts` is
            allowed to include an orientation dimension for the inducing points.
        **kwargs:
            Additional keyword arguments forwarded to `AugmentedSGPR` if needed
            (currently unused here but accepted for flexibility).
        """
        super().__init__(num_sensing, X_objective, kernel, noise_variance,
                         transform, num_robots, X_candidates, num_dim)
        if X_init is None:
            X_init = get_inducing_pts(X_objective,
                                      num_sensing * self.num_robots,
                                      orientation=orientation)
        else:
            # Override num_dim with the dimensionality of the initial inducing points
            self.num_dim = X_init.shape[-1]

        # Initialize the SGP
        dtype = X_objective.dtype
        train_set: Tuple[tf.Tensor, tf.Tensor] = (tf.constant(X_objective,
                                                              dtype=dtype),
                                                  tf.zeros(
                                                      (len(X_objective), 1),
                                                      dtype=dtype))
        self.sgpr = AugmentedSGPR(train_set,
                                  noise_variance=noise_variance,
                                  kernel=kernel,
                                  inducing_variable=X_init,
                                  inducing_variable_time=X_time,
                                  transform=transform)

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

        Parameters
        ----------
        kernel:
            New GPflow kernel instance.
        noise_variance:
            New observation noise variance.
        """
        self.sgpr.update(kernel, noise_variance)

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

        Returns
        -------
        (gpflow.kernels.Kernel, float)
            A deep copy of the kernel and the current likelihood variance.
        """
        return deepcopy(self.sgpr.kernel), \
               self.sgpr.likelihood.variance.numpy()

    def optimize(self,
                 max_steps: int = 500,
                 optimizer: str = 'scipy.L-BFGS-B',
                 verbose: bool = False,
                 **kwargs: Any) -> np.ndarray:
        """
        Optimize the inducing points of the SGP model.

        The ELBO (or equivalent objective defined within `AugmentedSGPR`) is
        optimized w.r.t. the inducing locations only; kernel hyperparameters
        are kept fixed.

        Parameters
        ----------
        max_steps:
            Maximum number of optimization steps. Defaults to 500.
        optimizer:
            Optimizer specification in the form `"backend.method"` (e.g.
            `'scipy.L-BFGS-B'`, `'tf.adam'`), as expected by `optimize_model`.
        verbose:
            If `True`, print progress information during optimization.
        **kwargs:
            Extra keyword arguments forwarded to `optimize_model`.

        Returns
        -------
        np.ndarray
            Array of shape `(num_robots, num_sensing, num_dim)` containing the
            optimized inducing locations.
        """
        _ = optimize_model(
            self.sgpr,
            max_steps=max_steps,
            optimize_hparams=
            False,  # Inducing points are optimized, not kernel hyperparameters
            optimizer=optimizer,
            verbose=verbose,
            **kwargs)

        sol: tf.Tensor = self.sgpr.inducing_variable.Z
        sol_expanded = self.transform.expand(sol,
                                             expand_sensor_model=False)
        if not isinstance(sol_expanded, np.ndarray):
            sol_np = sol_expanded.numpy()
        else:
            sol_np = sol_expanded

        # Snap to candidate set if provided
        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

    @property
    def transform(self) -> Transform:
        """
        Transform associated with the underlying SGP model.

        Returns
        -------
        Transform
            The `Transform` instance used by `AugmentedSGPR`.
        """
        return self.sgpr.transform

transform property

Transform associated with the underlying SGP model.

Returns

Transform The Transform instance used by AugmentedSGPR.

__init__(num_sensing, X_objective, kernel, noise_variance, transform=None, num_robots=1, X_candidates=None, num_dim=None, X_init=None, X_time=None, orientation=False, **kwargs)

Initialize a continuous SGP-based optimization method.

Parameters

num_sensing: Number of inducing points (sensing locations) per robot. X_objective: Array of shape (n, d) used to define the spatial domain and training inputs for the SGP. kernel: GPflow kernel for the SGP model. noise_variance: Observation noise variance for the SGP model. transform: Optional Transform to apply to inducing points for IPP or FoV modeling. Passed directly into AugmentedSGPR. num_robots: Number of robots / agents. The total number of inducing points is num_sensing * num_robots. Defaults to 1. X_candidates: Optional candidate set (c, d) used to snap the final continuous inducing locations to discrete locations. num_dim: Dimensionality of sensing locations. If None, defaults to X_objective.shape[-1], or to X_init.shape[-1] if an initial solution is provided. X_init: Initial inducing points with shape (num_sensing * num_robots, d). If None, points are chosen via get_inducing_pts. If given, its dimensionality overrides num_dim. X_time: Optional temporal coordinates (e.g. for spatio-temporal models), passed as inducing_variable_time to AugmentedSGPR. orientation: If True and X_init is not provided, get_inducing_pts is allowed to include an orientation dimension for the inducing points. **kwargs: Additional keyword arguments forwarded to AugmentedSGPR if needed (currently unused here but accepted for flexibility).

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,
             X_init: Optional[np.ndarray] = None,
             X_time: Optional[np.ndarray] = None,
             orientation: bool = False,
             **kwargs: Any):
    """
    Initialize a continuous SGP-based optimization method.

    Parameters
    ----------
    num_sensing:
        Number of inducing points (sensing locations) per robot.
    X_objective:
        Array of shape `(n, d)` used to define the spatial domain and
        training inputs for the SGP.
    kernel:
        GPflow kernel for the SGP model.
    noise_variance:
        Observation noise variance for the SGP model.
    transform:
        Optional `Transform` to apply to inducing points for IPP or FoV
        modeling. Passed directly into `AugmentedSGPR`.
    num_robots:
        Number of robots / agents. The total number of inducing points is
        `num_sensing * num_robots`. Defaults to 1.
    X_candidates:
        Optional candidate set `(c, d)` used to snap the final continuous
        inducing locations to discrete locations.
    num_dim:
        Dimensionality of sensing locations. If `None`, defaults to
        `X_objective.shape[-1]`, or to `X_init.shape[-1]` if an initial
        solution is provided.
    X_init:
        Initial inducing points with shape `(num_sensing * num_robots, d)`.
        If `None`, points are chosen via `get_inducing_pts`. If given,
        its dimensionality overrides `num_dim`.
    X_time:
        Optional temporal coordinates (e.g. for spatio-temporal models),
        passed as `inducing_variable_time` to `AugmentedSGPR`.
    orientation:
        If `True` and `X_init` is not provided, `get_inducing_pts` is
        allowed to include an orientation dimension for the inducing points.
    **kwargs:
        Additional keyword arguments forwarded to `AugmentedSGPR` if needed
        (currently unused here but accepted for flexibility).
    """
    super().__init__(num_sensing, X_objective, kernel, noise_variance,
                     transform, num_robots, X_candidates, num_dim)
    if X_init is None:
        X_init = get_inducing_pts(X_objective,
                                  num_sensing * self.num_robots,
                                  orientation=orientation)
    else:
        # Override num_dim with the dimensionality of the initial inducing points
        self.num_dim = X_init.shape[-1]

    # Initialize the SGP
    dtype = X_objective.dtype
    train_set: Tuple[tf.Tensor, tf.Tensor] = (tf.constant(X_objective,
                                                          dtype=dtype),
                                              tf.zeros(
                                                  (len(X_objective), 1),
                                                  dtype=dtype))
    self.sgpr = AugmentedSGPR(train_set,
                              noise_variance=noise_variance,
                              kernel=kernel,
                              inducing_variable=X_init,
                              inducing_variable_time=X_time,
                              transform=transform)

get_hyperparameters()

Return the current kernel and noise variance of the SGP model.

Returns

(gpflow.kernels.Kernel, float) A deep copy of the kernel and the current likelihood variance.

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

    Returns
    -------
    (gpflow.kernels.Kernel, float)
        A deep copy of the kernel and the current likelihood variance.
    """
    return deepcopy(self.sgpr.kernel), \
           self.sgpr.likelihood.variance.numpy()

optimize(max_steps=500, optimizer='scipy.L-BFGS-B', verbose=False, **kwargs)

Optimize the inducing points of the SGP model.

The ELBO (or equivalent objective defined within AugmentedSGPR) is optimized w.r.t. the inducing locations only; kernel hyperparameters are kept fixed.

Parameters

max_steps: Maximum number of optimization steps. Defaults to 500. optimizer: Optimizer specification in the form "backend.method" (e.g. 'scipy.L-BFGS-B', 'tf.adam'), as expected by optimize_model. verbose: If True, print progress information during optimization. **kwargs: Extra keyword arguments forwarded to optimize_model.

Returns

np.ndarray Array of shape (num_robots, num_sensing, num_dim) containing the optimized inducing locations.

Source code in sgptools/methods.py
def optimize(self,
             max_steps: int = 500,
             optimizer: str = 'scipy.L-BFGS-B',
             verbose: bool = False,
             **kwargs: Any) -> np.ndarray:
    """
    Optimize the inducing points of the SGP model.

    The ELBO (or equivalent objective defined within `AugmentedSGPR`) is
    optimized w.r.t. the inducing locations only; kernel hyperparameters
    are kept fixed.

    Parameters
    ----------
    max_steps:
        Maximum number of optimization steps. Defaults to 500.
    optimizer:
        Optimizer specification in the form `"backend.method"` (e.g.
        `'scipy.L-BFGS-B'`, `'tf.adam'`), as expected by `optimize_model`.
    verbose:
        If `True`, print progress information during optimization.
    **kwargs:
        Extra keyword arguments forwarded to `optimize_model`.

    Returns
    -------
    np.ndarray
        Array of shape `(num_robots, num_sensing, num_dim)` containing the
        optimized inducing locations.
    """
    _ = optimize_model(
        self.sgpr,
        max_steps=max_steps,
        optimize_hparams=
        False,  # Inducing points are optimized, not kernel hyperparameters
        optimizer=optimizer,
        verbose=verbose,
        **kwargs)

    sol: tf.Tensor = self.sgpr.inducing_variable.Z
    sol_expanded = self.transform.expand(sol,
                                         expand_sensor_model=False)
    if not isinstance(sol_expanded, np.ndarray):
        sol_np = sol_expanded.numpy()
    else:
        sol_np = sol_expanded

    # Snap to candidate set if provided
    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 SGP model.

Parameters

kernel: New GPflow kernel instance. noise_variance: New observation noise variance.

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 SGP model.

    Parameters
    ----------
    kernel:
        New GPflow kernel instance.
    noise_variance:
        New observation noise variance.
    """
    self.sgpr.update(kernel, noise_variance)