Skip to content

GreedyObjective

sgptools.methods.GreedyObjective

Bases: Method

Implements informative sensor placement/path optimization using a greedy approach based on a specified objective function.

This method iteratively selects the best sensing location from a set of candidates that maximizes the objective function. It currently supports only single-robot scenarios.

Refer to the following papers for more details
  • Near-Optimal Sensor Placements in Gaussian Processes: Theory, Efficient Algorithms and Empirical Studies [Krause et al., 2008]
  • Data-driven learning and planning for environmental sampling [Ma et al., 2018]

Attributes:

Name Type Description
objective object

The objective function to be maximized (e.g., Mutual Information).

transform Optional[Transform]

Transform object applied to selected locations.

Source code in sgptools/methods.py
class GreedyObjective(Method):
    """
    Implements informative sensor placement/path optimization using a greedy approach based on a specified objective function.

    This method iteratively selects the best sensing location from a set of candidates
    that maximizes the objective function. It currently supports only single-robot scenarios.

    Refer to the following papers for more details:
        - Near-Optimal Sensor Placements in Gaussian Processes: Theory, Efficient Algorithms and Empirical Studies [Krause et al., 2008]
        - Data-driven learning and planning for environmental sampling [Ma et al., 2018]

    Attributes:
        objective (object): The objective function to be maximized (e.g., Mutual Information).
        transform (Optional[Transform]): Transform object applied to selected locations.
    """

    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, Any] = 'SLogMI',
                 **kwargs: Any):
        """
        Initializes the GreedyObjective optimizer.

        Args:
            num_sensing (int): Number of sensing locations to select.
            X_objective (np.ndarray): (n, d); Data points used to define the objective function.
            kernel (gpflow.kernels.Kernel): GPflow kernel function.
            noise_variance (float): Data noise variance.
            transform (Optional[Transform]): Transform object to apply to inducing points. Defaults to None.
            num_robots (int): Number of robots/agents. Defaults to 1.
            X_candidates (Optional[np.ndarray]): (c, d); Discrete set of candidate locations for sensor placement.
                                                 If None, X_objective is used as candidates.
            num_dim (Optional[int]): Dimensionality of the sensing locations. Defaults to dimensonality of X_objective.
            objective (Union[str, Any]): The objective function to use. Can be a string ('SLogMI', 'MI')
                                         or an instance of an objective class. Defaults to 'SLogMI'.
            **kwargs: Additional keyword arguments passed to the objective function.
        """
        super().__init__(num_sensing, X_objective, kernel, noise_variance,
                         transform, num_robots, X_candidates, num_dim)
        self.X_objective = X_objective
        if X_candidates is None:
            self.X_candidates = X_objective  # Default candidates to objective points

        if transform is not None:
            try:
                num_robots_transform = transform.num_robots
            except AttributeError:
                num_robots_transform = 1  # Assume single robot if num_robots not defined in transform
            error = f"num_robots is not equal in transform: {num_robots_transform} and GreedyObjective: {self.num_robots}"
            assert self.num_robots == num_robots_transform, error

        error = f"num_robots={self.num_robots}; GreedyObjective only supports num_robots=1"
        assert self.num_robots == 1, error

        self.transform = transform

        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:
        """
        Updates the kernel and noise variance parameters of the objective function.

        Args:
            kernel (gpflow.kernels.Kernel): Updated GPflow kernel function.
            noise_variance (float): Updated data noise variance.
        """
        self.objective.update(kernel, noise_variance)

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

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

    def optimize(self,
                 optimizer: str = 'naive',
                 verbose: bool = False,
                 **kwargs: Any) -> np.ndarray:
        """
        Optimizes sensor placement using a greedy approach.

        Args:
            optimizer (str): The greedy optimizer strategy (e.g., 'naive', 'lazy'). Defaults to 'naive'.
            verbose (bool): Verbosity, if True additional details will by reported. Defaults to False.
            **kwargs: Additional keyword arguments.

        Returns:
            np.ndarray: (num_robots, num_sensing, num_dim); Optimized sensing locations.

        Usage:
            ```python
            # Assuming X_train, candidates, kernel_opt, noise_variance_opt are defined
            greedy_obj_method = GreedyObjective(
                num_sensing=5,
                X_objective=X_train,
                kernel=kernel_opt,
                noise_variance=noise_variance_opt,
                X_candidates=candidates
            )
            optimized_solution = greedy_obj_method.optimize(optimizer='naive')
            ```
        """
        model = CustomSelection(self.num_sensing,
                                self._objective,
                                optimizer=optimizer,
                                verbose=verbose)

        # apricot's CustomSelection expects indices, so pass a dummy array of indices
        sol_indices = model.fit_transform(
            np.arange(len(self.X_candidates)).reshape(-1, 1))
        sol_indices = np.array(sol_indices).reshape(-1).astype(int)
        sol_locations = self.X_candidates[sol_indices]

        sol_locations = np.array(sol_locations).reshape(-1, self.num_dim)
        if self.transform is not None:
            try:
                sol_locations = self.transform.expand(
                    sol_locations, expand_sensor_model=False)
            except TypeError:
                pass
            if not isinstance(sol_locations, np.ndarray):
                sol_locations = sol_locations.numpy()
        sol_locations = sol_locations.reshape(self.num_robots, -1,
                                              self.num_dim)
        return sol_locations

    def _objective(self, X_indices: np.ndarray) -> float:
        """
        Internal objective function for the greedy selection.

        This function maps the input indices to actual locations, applies any
        transformations, calculates the objective value, and applies a penalty
        for constraint violations.

        Args:
            X_indices (np.ndarray): (n, 1); Array of indices corresponding to candidate locations.

        Returns:
            float: The objective value (reward - constraint penalty) for the given selection.
        """
        # Map solution location indices to locations
        X_indices_flat = np.array(X_indices).reshape(-1).astype(int)
        X_locations = self.X_objective[X_indices_flat].reshape(
            -1, self.num_dim)

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

        reward -= constraint_penality  # minimize
        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)

Initializes the GreedyObjective optimizer.

Parameters:

Name Type Description Default
num_sensing int

Number of sensing locations to select.

required
X_objective ndarray

(n, d); Data points used to define the objective function.

required
kernel Kernel

GPflow kernel function.

required
noise_variance float

Data noise variance.

required
transform Optional[Transform]

Transform object to apply to inducing points. Defaults to None.

None
num_robots int

Number of robots/agents. Defaults to 1.

1
X_candidates Optional[ndarray]

(c, d); Discrete set of candidate locations for sensor placement. If None, X_objective is used as candidates.

None
num_dim Optional[int]

Dimensionality of the sensing locations. Defaults to dimensonality of X_objective.

None
objective Union[str, Any]

The objective function to use. Can be a string ('SLogMI', 'MI') or an instance of an objective class. Defaults to 'SLogMI'.

'SLogMI'
**kwargs Any

Additional keyword arguments passed to the objective function.

{}
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, Any] = 'SLogMI',
             **kwargs: Any):
    """
    Initializes the GreedyObjective optimizer.

    Args:
        num_sensing (int): Number of sensing locations to select.
        X_objective (np.ndarray): (n, d); Data points used to define the objective function.
        kernel (gpflow.kernels.Kernel): GPflow kernel function.
        noise_variance (float): Data noise variance.
        transform (Optional[Transform]): Transform object to apply to inducing points. Defaults to None.
        num_robots (int): Number of robots/agents. Defaults to 1.
        X_candidates (Optional[np.ndarray]): (c, d); Discrete set of candidate locations for sensor placement.
                                             If None, X_objective is used as candidates.
        num_dim (Optional[int]): Dimensionality of the sensing locations. Defaults to dimensonality of X_objective.
        objective (Union[str, Any]): The objective function to use. Can be a string ('SLogMI', 'MI')
                                     or an instance of an objective class. Defaults to 'SLogMI'.
        **kwargs: Additional keyword arguments passed to the objective function.
    """
    super().__init__(num_sensing, X_objective, kernel, noise_variance,
                     transform, num_robots, X_candidates, num_dim)
    self.X_objective = X_objective
    if X_candidates is None:
        self.X_candidates = X_objective  # Default candidates to objective points

    if transform is not None:
        try:
            num_robots_transform = transform.num_robots
        except AttributeError:
            num_robots_transform = 1  # Assume single robot if num_robots not defined in transform
        error = f"num_robots is not equal in transform: {num_robots_transform} and GreedyObjective: {self.num_robots}"
        assert self.num_robots == num_robots_transform, error

    error = f"num_robots={self.num_robots}; GreedyObjective only supports num_robots=1"
    assert self.num_robots == 1, error

    self.transform = transform

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

get_hyperparameters()

Retrieves the current kernel and noise variance hyperparameters from the objective.

Returns:

Type Description
Tuple[Kernel, float]

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

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

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

optimize(optimizer='naive', verbose=False, **kwargs)

Optimizes sensor placement using a greedy approach.

Parameters:

Name Type Description Default
optimizer str

The greedy optimizer strategy (e.g., 'naive', 'lazy'). Defaults to 'naive'.

'naive'
verbose bool

Verbosity, if True additional details will by reported. Defaults to False.

False
**kwargs Any

Additional keyword arguments.

{}

Returns:

Type Description
ndarray

np.ndarray: (num_robots, num_sensing, num_dim); Optimized sensing locations.

Usage
# Assuming X_train, candidates, kernel_opt, noise_variance_opt are defined
greedy_obj_method = GreedyObjective(
    num_sensing=5,
    X_objective=X_train,
    kernel=kernel_opt,
    noise_variance=noise_variance_opt,
    X_candidates=candidates
)
optimized_solution = greedy_obj_method.optimize(optimizer='naive')
Source code in sgptools/methods.py
def optimize(self,
             optimizer: str = 'naive',
             verbose: bool = False,
             **kwargs: Any) -> np.ndarray:
    """
    Optimizes sensor placement using a greedy approach.

    Args:
        optimizer (str): The greedy optimizer strategy (e.g., 'naive', 'lazy'). Defaults to 'naive'.
        verbose (bool): Verbosity, if True additional details will by reported. Defaults to False.
        **kwargs: Additional keyword arguments.

    Returns:
        np.ndarray: (num_robots, num_sensing, num_dim); Optimized sensing locations.

    Usage:
        ```python
        # Assuming X_train, candidates, kernel_opt, noise_variance_opt are defined
        greedy_obj_method = GreedyObjective(
            num_sensing=5,
            X_objective=X_train,
            kernel=kernel_opt,
            noise_variance=noise_variance_opt,
            X_candidates=candidates
        )
        optimized_solution = greedy_obj_method.optimize(optimizer='naive')
        ```
    """
    model = CustomSelection(self.num_sensing,
                            self._objective,
                            optimizer=optimizer,
                            verbose=verbose)

    # apricot's CustomSelection expects indices, so pass a dummy array of indices
    sol_indices = model.fit_transform(
        np.arange(len(self.X_candidates)).reshape(-1, 1))
    sol_indices = np.array(sol_indices).reshape(-1).astype(int)
    sol_locations = self.X_candidates[sol_indices]

    sol_locations = np.array(sol_locations).reshape(-1, self.num_dim)
    if self.transform is not None:
        try:
            sol_locations = self.transform.expand(
                sol_locations, expand_sensor_model=False)
        except TypeError:
            pass
        if not isinstance(sol_locations, np.ndarray):
            sol_locations = sol_locations.numpy()
    sol_locations = sol_locations.reshape(self.num_robots, -1,
                                          self.num_dim)
    return sol_locations

update(kernel, noise_variance)

Updates the kernel and noise variance parameters of the objective function.

Parameters:

Name Type Description Default
kernel Kernel

Updated GPflow kernel function.

required
noise_variance float

Updated data noise variance.

required
Source code in sgptools/methods.py
def update(self, kernel: gpflow.kernels.Kernel,
           noise_variance: float) -> None:
    """
    Updates the kernel and noise variance parameters of the objective function.

    Args:
        kernel (gpflow.kernels.Kernel): Updated GPflow kernel function.
        noise_variance (float): Updated data noise variance.
    """
    self.objective.update(kernel, noise_variance)