"""Probability of Feasibility (PoF) acquisition function module."""
from __future__ import annotations
from typing import TYPE_CHECKING, Any
import numpy as np
from saealib.acquisition.base import PointwiseAcquisition
from saealib.acquisition.kernels import (
probability_of_feasibility_kernel,
product_of_feasibility_kernel,
)
from saealib.surrogate.prediction import SurrogatePrediction
if TYPE_CHECKING:
from saealib.population import Archive
[docs]
class ProbabilityOfFeasibility(PointwiseAcquisition):
"""
Probability of Feasibility (PoF) acquisition function.
Estimates the probability that a candidate satisfies a constraint
g(x) <= 0, using a surrogate model that predicts the constraint value.
PoF(x) = Phi((0 - mu(x)) / sigma(x))
Typically used in combination with another acquisition function
(e.g., EI * PoF) to handle black-box constraints.
Requires a surrogate that provides uncertainty estimates (std).
Parameters
----------
obj_idx : int
Index of the predicted constraint to evaluate. Default: 0.
References
----------
:cite:`schonlau1997pof`: Schonlau, M. (1997). Computer Experiments
and Global Optimization (PhD thesis, University of Waterloo).
(Earliest formulation.)
:cite:`gelbart2014pof`: Gelbart, M. A., Snoek, J., & Adams, R. P.
(2014). Bayesian optimization with unknown constraints. *Proceedings of
the 30th Conference on Uncertainty in Artificial Intelligence (UAI)*.
(Constraint-handling with independent surrogates.)
"""
requires_uncertainty: bool = True
# Feasibility probability has no notion of objective direction.
direction_sensitive: bool = False
[docs]
def __init__(self, obj_idx: int = 0, reference: Any = None):
self.obj_idx = obj_idx
self.reference = reference
[docs]
def compute_reference(
self, archive: Archive, rng: np.random.Generator | None = None
) -> Any:
"""Return fixed reference if set, otherwise None."""
return self.reference
[docs]
def score(
self,
prediction: SurrogatePrediction,
reference: Any = None,
rng: np.random.Generator | None = None,
) -> np.ndarray:
"""
Compute Probability of Feasibility scores.
Parameters
----------
prediction : SurrogatePrediction
Surrogate predictions of constraint values.
Must have std (has_uncertainty == True).
reference : Any
Not used. Accepted for interface compatibility.
Returns
-------
np.ndarray
PoF scores in [0, 1]. shape: (n_samples,)
Higher scores indicate a higher probability of feasibility.
Raises
------
TypeError
If prediction does not contain uncertainty estimates.
"""
if not prediction.has_uncertainty:
raise TypeError(
"ProbabilityOfFeasibility requires a surrogate with uncertainty "
"estimates (prediction.std must not be None)."
)
assert prediction.std is not None
mu = prediction.value[:, self.obj_idx] # (n_samples,)
sigma = prediction.std[:, self.obj_idx] # (n_samples,)
return probability_of_feasibility_kernel(mu, sigma)
[docs]
class ProductOfFeasibility(PointwiseAcquisition):
r"""Joint probability of feasibility across all constraint columns.
Computes the product of per-constraint feasibility probabilities:
.. math::
\text{PoF}_{\text{joint}}(x) = \prod_{i=1}^{m}
\Phi\!\left(\frac{0 - \mu_i(x)}{\sigma_i(x)}\right)
where :math:`\mu_i` and :math:`\sigma_i` are the surrogate's predicted
mean and standard deviation for the *i*-th constraint column.
This is the standard acquisition function for constrained Bayesian
optimisation with independent constraint surrogates
(:cite:`letham2019constraintbo`; Eriksson & Poloczek, 2021). Use it with
a surrogate trained on
``ConstraintObjectiveSet`` (which returns ``archive.g`` as ``train_y``),
typically via ``PerObjectiveSurrogate([GP(), ...] * n_constraints)``.
Combine this acquisition with an objective acquisition through
``CompositeAcquisition`` and configure the result on the optimizer.
Requires a surrogate that provides uncertainty estimates (std).
Notes
-----
Assumes constraints are independent. When constraints are strongly
correlated, a multi-output GP with joint predictions is more accurate
but is not required by this acquisition function.
References
----------
:cite:`letham2019constraintbo`: Letham, B., Karrer, B., Ottoni, G., &
Bakshy, E. (2019). Constrained Bayesian optimization with noisy
experiments. *Bayesian Analysis*, 14(2), 495-519.
Conceptual origin also draws on Eriksson & Poloczek (2021), SCBO.
"""
requires_uncertainty: bool = True
# Feasibility probability has no notion of objective direction.
direction_sensitive: bool = False
[docs]
def compute_reference(
self, archive: Archive, rng: np.random.Generator | None = None
) -> Any:
"""Return None (no reference needed)."""
return None
[docs]
def score(
self,
prediction: SurrogatePrediction,
reference: Any = None,
rng: np.random.Generator | None = None,
) -> np.ndarray:
"""
Compute joint probability of feasibility scores.
Parameters
----------
prediction : SurrogatePrediction
Surrogate predictions of constraint values.
``prediction.value`` shape: ``(n_samples, n_constraints)``.
Must have std (``has_uncertainty == True``).
reference : Any
Not used. Accepted for interface compatibility.
Returns
-------
np.ndarray
Joint PoF scores in [0, 1]. shape: ``(n_samples,)``.
Score is 1 when all constraints are clearly satisfied,
0 when any constraint is clearly violated.
Raises
------
TypeError
If prediction does not contain uncertainty estimates.
"""
if not prediction.has_uncertainty:
raise TypeError(
"ProductOfFeasibility requires a surrogate with uncertainty "
"estimates (prediction.std must not be None)."
)
assert prediction.std is not None
mu = prediction.value # (n_samples, n_constraints)
return product_of_feasibility_kernel(mu, prediction.std)