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).

Refer to the following papers for more details
  • Jakkala and Akella, 2024. Multi-Robot Informative Path Planning from Regression with Sparse Gaussian Processes.
  • Jakkala and Akella, 2025. Fully differentiable sensor placement and informative path planning.

Attributes:

Name Type Description
sgpr AugmentedSGPR

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`).

    Refer to the following papers for more details:
        - Jakkala and Akella, 2024. *Multi-Robot Informative Path Planning from
        Regression with Sparse Gaussian Processes.*
        - Jakkala and Akella, 2025. *Fully differentiable sensor placement and 
        informative path planning.*

    Attributes:
        sgpr (AugmentedSGPR):
            `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.

        Args:
            num_sensing (int):
                Number of inducing points (sensing locations) per robot.
            X_objective (np.ndarray):
                Array of shape `(n, d)` used to define the spatial domain and
                training inputs for the SGP.
            kernel (gpflow.kernels.Kernel):
                GPflow kernel for the SGP model.
            noise_variance (float):
                Observation noise variance for the SGP model.
            transform (Transform | None):
                Optional `Transform` to apply to inducing points for IPP or FoV
                modeling. Passed directly into `AugmentedSGPR`.
            num_robots (int):
                Number of robots / agents. The total number of inducing points is
                `num_sensing * num_robots`. Defaults to 1.
            X_candidates (np.ndarray | None):
                Optional candidate set `(c, d)` used to snap the final continuous
                inducing locations to discrete locations.
            num_dim (int | None):
                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 (np.ndarray | None):
                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 (np.ndarray | None):
                Optional temporal coordinates (e.g. for spatio-temporal models),
                passed as `inducing_variable_time` to `AugmentedSGPR`.
            orientation (bool):
                If `True` and `X_init` is not provided, `get_inducing_pts` is
                allowed to include an orientation dimension for the inducing points.
            **kwargs (Any):
                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.

        Args:
            kernel (gpflow.kernels.Kernel):
                New GPflow kernel instance.
            noise_variance (float):
                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:
            Tuple[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.

        Args:
            max_steps (int):
                Maximum number of optimization steps. Defaults to 500.
            optimizer (str):
                Optimizer specification in the form `"backend.method"` (e.g.
                `'scipy.L-BFGS-B'`, `'tf.adam'`), as expected by `optimize_model`.
            verbose (bool):
                If `True`, print progress information during optimization.
            **kwargs (Any):
                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:

Name Type Description
Transform 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:

Name Type Description Default
num_sensing int

Number of inducing points (sensing locations) per robot.

required
X_objective ndarray

Array of shape (n, d) used to define the spatial domain and training inputs for the SGP.

required
kernel Kernel

GPflow kernel for the SGP model.

required
noise_variance float

Observation noise variance for the SGP model.

required
transform Transform | None

Optional Transform to apply to inducing points for IPP or FoV modeling. Passed directly into AugmentedSGPR.

None
num_robots int

Number of robots / agents. The total number of inducing points is num_sensing * num_robots. Defaults to 1.

1
X_candidates ndarray | None

Optional candidate set (c, d) used to snap the final continuous inducing locations to discrete locations.

None
num_dim int | None

Dimensionality of sensing locations. If None, defaults to X_objective.shape[-1], or to X_init.shape[-1] if an initial solution is provided.

None
X_init ndarray | None

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.

None
X_time ndarray | None

Optional temporal coordinates (e.g. for spatio-temporal models), passed as inducing_variable_time to AugmentedSGPR.

None
orientation bool

If True and X_init is not provided, get_inducing_pts is allowed to include an orientation dimension for the inducing points.

False
**kwargs Any

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.

    Args:
        num_sensing (int):
            Number of inducing points (sensing locations) per robot.
        X_objective (np.ndarray):
            Array of shape `(n, d)` used to define the spatial domain and
            training inputs for the SGP.
        kernel (gpflow.kernels.Kernel):
            GPflow kernel for the SGP model.
        noise_variance (float):
            Observation noise variance for the SGP model.
        transform (Transform | None):
            Optional `Transform` to apply to inducing points for IPP or FoV
            modeling. Passed directly into `AugmentedSGPR`.
        num_robots (int):
            Number of robots / agents. The total number of inducing points is
            `num_sensing * num_robots`. Defaults to 1.
        X_candidates (np.ndarray | None):
            Optional candidate set `(c, d)` used to snap the final continuous
            inducing locations to discrete locations.
        num_dim (int | None):
            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 (np.ndarray | None):
            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 (np.ndarray | None):
            Optional temporal coordinates (e.g. for spatio-temporal models),
            passed as `inducing_variable_time` to `AugmentedSGPR`.
        orientation (bool):
            If `True` and `X_init` is not provided, `get_inducing_pts` is
            allowed to include an orientation dimension for the inducing points.
        **kwargs (Any):
            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:

Type Description
Tuple[Kernel, float]

Tuple[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:
        Tuple[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:

Name Type Description Default
max_steps int

Maximum number of optimization steps. Defaults to 500.

500
optimizer str

Optimizer specification in the form "backend.method" (e.g. 'scipy.L-BFGS-B', 'tf.adam'), as expected by optimize_model.

'scipy.L-BFGS-B'
verbose bool

If True, print progress information during optimization.

False
**kwargs Any

Extra keyword arguments forwarded to optimize_model.

{}

Returns:

Type Description
ndarray

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.

    Args:
        max_steps (int):
            Maximum number of optimization steps. Defaults to 500.
        optimizer (str):
            Optimizer specification in the form `"backend.method"` (e.g.
            `'scipy.L-BFGS-B'`, `'tf.adam'`), as expected by `optimize_model`.
        verbose (bool):
            If `True`, print progress information during optimization.
        **kwargs (Any):
            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:

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

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