Skip to content

DifferentiableObjective

sgptools.methods.DifferentiableObjective

Bases: Method

Informative sensor placement / path planning by directly differentiating through the objective function.

The sensing locations (or waypoints) are represented as TensorFlow variables, and a first-order optimizer (e.g., L-BFGS, Adam) is used to minimize a scalar loss built from the objective and constraints. This can be more sample-efficient than black-box methods, but is more sensitive to local minima.

Attributes:

Name Type Description
transform Transform | None

Optional transform applied to the current solution.

X_sol Variable

TensorFlow variable representing the current solution locations.

objective Objective

Objective object that maps (transformed) sensing locations to a scalar value.

Source code in sgptools/methods.py
class DifferentiableObjective(Method):
    """Informative sensor placement / path planning by directly differentiating
    through the objective function.

    The sensing locations (or waypoints) are represented as TensorFlow
    variables, and a first-order optimizer (e.g., L-BFGS, Adam) is used to
    minimize a scalar loss built from the objective and constraints. This can
    be more sample-efficient than black-box methods, but is more sensitive to
    local minima.

    Attributes:
        transform (Transform | None):
            Optional transform applied to the current solution.
        X_sol (tf.Variable):
            TensorFlow variable representing the current solution locations.
        objective (Objective):
            Objective object that maps (transformed) sensing locations to a scalar
            value.
    """

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

        Args:
            num_sensing (int):
                Number of sensing locations per robot.
            X_objective (np.ndarray):
                Array of shape `(n, d)` used to define the objective (e.g., GP
                training inputs).
            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 solution before evaluating the
                objective and constraints.
            num_robots (int):
                Number of robots / agents. The total number of optimized points is
                `num_sensing * num_robots`.
            X_candidates (np.ndarray | None):
                Optional candidate set `(c, d)` to which the final continuous
                solution can be snapped.
            num_dim (int | None):
                Dimensionality of sensing locations. If `None`, defaults to
                `X_objective.shape[-1]`, or to `X_init.shape[-1]` if given.
            objective (str | Objective):
                Objective specification (string key or `Objective` instance) used to
                construct the reward function.
            X_init (np.ndarray | None):
                Initial sensing locations with shape `(num_sensing * num_robots, d)`.
                If `None`, points are selected via `get_inducing_pts`. If given,
                its dimensionality overrides `num_dim`.
            X_time (np.ndarray | None):
                (Reserved for future use with spatio-temporal models; not used
                directly here.)
            orientation (bool):
                If `True` and `X_init` is not provided, `get_inducing_pts` may add
                an orientation dimension to the initial points.
            **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 X_candidates is None:
            self.X_candidates = X_objective  # Default candidates to objective points

        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 solution
            self.num_dim = X_init.shape[-1]
        self.X_sol = tf.Variable(X_init, dtype=X_init.dtype)

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

    def update(self, kernel: gpflow.kernels.Kernel,
               noise_variance: float) -> None:
        """Update the kernel and noise variance used by the 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 = 500,
                 optimizer: str = 'scipy.L-BFGS-B',
                 verbose: bool = False,
                 **kwargs: Any) -> np.ndarray:
        """Optimize sensing locations by differentiating through the objective.

        `self.X_sol` is treated as a trainable variable and optimized using the
        specified optimizer and the internal `_objective` as the scalar loss.

        Args:
            max_steps (int):
                Maximum number of optimization steps. Defaults to 500.
            optimizer (str):
                Optimizer specification `"backend.method"` (e.g., `'scipy.L-BFGS-B'`,
                `'tf.adam'`) passed to `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 sensing locations.
        """
        _ = optimize_model(
            training_loss=self._objective,
            max_steps=max_steps,
            trainable_variables=[self.X_sol],
            optimizer=optimizer,
            verbose=verbose,
            **kwargs)

        sol: tf.Tensor = self.X_sol
        if self.transform is not None:
            sol = self.transform.expand(sol,
                                        expand_sensor_model=False)
        if not isinstance(sol, np.ndarray):
            sol_np = sol.numpy()
        else:
            sol_np = sol

        # 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

    def _objective(self) -> float:
        """Scalar loss function used by `optimize_model`.

        The objective is built as:

        .. code-block:: text

            loss = objective(X_expanded) + constraint_penalty

        where both terms are produced by the `Transform`. Depending on the
        sign conventions of `objective` and `constraints`, this loss can be
        interpreted as either a negative reward or a penalized reward. The
        optimizer *minimizes* this loss.

        Returns:
            tf.Tensor:
                Scalar TensorFlow value representing the loss to minimize.
        """
        constraint_penality: float = 0.0
        if self.transform is not None:
            X_expanded = self.transform.expand(self.X_sol)
            constraint_penality = self.transform.constraints(self.X_sol)
            reward = self.objective(X_expanded)  # maximize (before sign handling)
        else:
            reward = self.objective(self.X_sol)  # maximize (before sign handling)

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

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

Initialize a differentiable-objective method.

Parameters:

Name Type Description Default
num_sensing int

Number of sensing locations per robot.

required
X_objective ndarray

Array of shape (n, d) used to define the objective (e.g., GP training inputs).

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 solution before evaluating the objective and constraints.

None
num_robots int

Number of robots / agents. The total number of optimized points is num_sensing * num_robots.

1
X_candidates ndarray | None

Optional candidate set (c, d) to which the final continuous solution can be snapped.

None
num_dim int | None

Dimensionality of sensing locations. If None, defaults to X_objective.shape[-1], or to X_init.shape[-1] if given.

None
objective str | Objective

Objective specification (string key or Objective instance) used to construct the reward function.

'SLogMI'
X_init ndarray | None

Initial sensing locations with shape (num_sensing * num_robots, d). If None, points are selected via get_inducing_pts. If given, its dimensionality overrides num_dim.

None
X_time ndarray | None

(Reserved for future use with spatio-temporal models; not used directly here.)

None
orientation bool

If True and X_init is not provided, get_inducing_pts may add an orientation dimension to the initial points.

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

    Args:
        num_sensing (int):
            Number of sensing locations per robot.
        X_objective (np.ndarray):
            Array of shape `(n, d)` used to define the objective (e.g., GP
            training inputs).
        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 solution before evaluating the
            objective and constraints.
        num_robots (int):
            Number of robots / agents. The total number of optimized points is
            `num_sensing * num_robots`.
        X_candidates (np.ndarray | None):
            Optional candidate set `(c, d)` to which the final continuous
            solution can be snapped.
        num_dim (int | None):
            Dimensionality of sensing locations. If `None`, defaults to
            `X_objective.shape[-1]`, or to `X_init.shape[-1]` if given.
        objective (str | Objective):
            Objective specification (string key or `Objective` instance) used to
            construct the reward function.
        X_init (np.ndarray | None):
            Initial sensing locations with shape `(num_sensing * num_robots, d)`.
            If `None`, points are selected via `get_inducing_pts`. If given,
            its dimensionality overrides `num_dim`.
        X_time (np.ndarray | None):
            (Reserved for future use with spatio-temporal models; not used
            directly here.)
        orientation (bool):
            If `True` and `X_init` is not provided, `get_inducing_pts` may add
            an orientation dimension to the initial points.
        **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 X_candidates is None:
        self.X_candidates = X_objective  # Default candidates to objective points

    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 solution
        self.num_dim = X_init.shape[-1]
    self.X_sol = tf.Variable(X_init, dtype=X_init.dtype)

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

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=500, optimizer='scipy.L-BFGS-B', verbose=False, **kwargs)

Optimize sensing locations by differentiating through the objective.

self.X_sol is treated as a trainable variable and optimized using the specified optimizer and the internal _objective as the scalar loss.

Parameters:

Name Type Description Default
max_steps int

Maximum number of optimization steps. Defaults to 500.

500
optimizer str

Optimizer specification "backend.method" (e.g., 'scipy.L-BFGS-B', 'tf.adam') passed to 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 sensing 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 sensing locations by differentiating through the objective.

    `self.X_sol` is treated as a trainable variable and optimized using the
    specified optimizer and the internal `_objective` as the scalar loss.

    Args:
        max_steps (int):
            Maximum number of optimization steps. Defaults to 500.
        optimizer (str):
            Optimizer specification `"backend.method"` (e.g., `'scipy.L-BFGS-B'`,
            `'tf.adam'`) passed to `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 sensing locations.
    """
    _ = optimize_model(
        training_loss=self._objective,
        max_steps=max_steps,
        trainable_variables=[self.X_sol],
        optimizer=optimizer,
        verbose=verbose,
        **kwargs)

    sol: tf.Tensor = self.X_sol
    if self.transform is not None:
        sol = self.transform.expand(sol,
                                    expand_sensor_model=False)
    if not isinstance(sol, np.ndarray):
        sol_np = sol.numpy()
    else:
        sol_np = sol

    # 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 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 objective.

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