AcquisitionFunction#

Layer 3: Extend Components

SurrogatePredictStage passes a Surrogate’s predictions to AcquisitionStage, which delegates scalar scoring to AcquisitionFunction. AcquisitionFunction receives candidates, predictions, the archive, context, and prepared data, but it does not know the Surrogate’s internal implementation or the algorithm that produced the predictions.

AcquisitionFunction’s role#

AcquisitionFunction requires evaluate(candidates_x, prediction, archive, ctx=None, *, prepared=...) -> AcquisitionResult. For pointwise criteria, the PointwiseAcquisition convenience base class splits this into two methods.

compute_reference(archive, rng=None) -> Any: On PointwiseAcquisition, computes a reference value used for scoring (such as the current best value) from the archive. An acquisition function that doesn’t use a reference value may return None.

score(prediction, reference, rng=None) -> np.ndarray: On PointwiseAcquisition, computes a score from SurrogatePrediction and the reference value. As with saealib’s overall convention, a higher score is better.

compute_reference() receives no ctx, so a subclass that needs runtime state (such as LowerConfidenceBound’s optional beta_schedule, which reads ctx.decision_count) overrides prepare(archive, ctx=None) directly instead; whatever it returns still flows into score() as reference.

The class attribute requires_uncertainty: bool indicates whether this acquisition function needs SurrogatePrediction.std (uncertainty). direction_sensitive: bool (default True) indicates whether Optimizer automatically injects problem.direction into this acquisition function at the start of a run. Acquisition functions with no notion of objective direction, such as feasibility probability, set direction_sensitive = False to disable this auto-injection.

Built-in AcquisitionFunctions#

Class

Characteristics

requires_uncertainty

MeanPrediction

The simplest acquisition function, using only the predictive mean

False

ExpectedImprovement

Expected Improvement (EI) [24]

True

BatchExpectedImprovement

Joint EI with batch diversity for qEI-style selection

True

LowerConfidenceBound

Lower Confidence Bound (LCB) [25]

True

MaxUncertainty

The larger the predictive uncertainty, the better (exploration-leaning)

True

EHVIAcquisition

Expected Hypervolume Improvement [26, 27, 28]. For multi-objective use

True

SMSEGOAcquisition

An SMS-EMOA-style hypervolume indicator (SMS-EGO, proposed by Ponweiser et al., 2008). For multi-objective use

True

ParEGOAcquisition

ParEGO via random scalarization [29, 30]. For multi-objective use

True

ProbabilityOfFeasibility

Feasibility probability for a single constraint [31, 32]

True

ProductOfFeasibility

Product of feasibility probabilities across multiple constraints [32]

True

NoveltyAcquisition

Mean distance to the nearest archive points

False

InverseDensityAcquisition

Inverse archive neighborhood density

False

MaximinDistanceAcquisition

Candidate and archive diversity criterion

False

The uncertainty-based acquisitions in the table have requires_uncertainty=True. Archive-based acquisitions (NoveltyAcquisition, InverseDensityAcquisition, and MaximinDistanceAcquisition) do not require predictive uncertainty. To use an uncertainty-based acquisition function, the Surrogate it’s paired with must return std (provides_uncertainty=True). Joint batch acquisitions additionally require a candidate covariance or another explicit joint posterior representation; see the examples for a deterministic implementation of that contract. Among the built-in surrogates, only SklearnGPRSurrogate satisfies this. See the uncertainty-support table in Surrogate for details.

MaxUncertainty/ProbabilityOfFeasibility/ProductOfFeasibility have direction_sensitive = False. This is because the magnitude of uncertainty or a feasibility probability has no notion of maximizing vs. minimizing direction.

ProbabilityOfFeasibility/ProductOfFeasibility are used paired with a classification or regression surrogate that predicts a constraint value g. The typical usage is to extract training data with TrainingSet’s ConstraintObjectiveSet, and combine it with the objective-side acquisition function (e.g. EI) via CompositeAcquisition’s product_combine.

ei_manager = GlobalSurrogateManager(gp_surrogate, ArchiveObjectiveSet())
pof_manager = GlobalSurrogateManager(
    PerObjectiveSurrogate([gp_g1, gp_g2]),
    ConstraintObjectiveSet(),
)
optimizer.set_surrogate_manager(
    CompositeSurrogateManager({"objective": ei_manager, "feasibility": pof_manager})
)
optimizer.set_acquisition(
    CompositeAcquisition(
        {"objective": ExpectedImprovement(), "feasibility": ProductOfFeasibility()},
        product_combine,
    )
)

The meaning of the weights/direction arguments#

Some acquisition functions, such as MeanPrediction, can scalarize multi-objective predictions via the weights argument. Use a negative weight for objectives you want to minimize, e.g. weights=np.array([-1.0]).

Specifying the direction argument produces a sign-only scalarization with no magnitude, and takes priority over weights. If direction isn’t specified explicitly, problem.direction is injected automatically at the start of a run (only for acquisition functions where direction_sensitive is True).

Implementing a custom AcquisitionFunction#

If you need a custom pointwise scoring scheme, subclass PointwiseAcquisition and implement compute_reference()/score(). The following example is a simple acquisition function that gives a higher score to candidates whose predictive mean falls further below a threshold.

from saealib import PointwiseAcquisition


class ThresholdAcquisition(PointwiseAcquisition):
    """Gives a higher score to candidates whose predictive mean falls further below a threshold (assumes minimization)."""

    def __init__(self, threshold: float = 0.0):
        self.threshold = threshold

    def compute_reference(self, archive, rng=None):
        return None

    def score(self, prediction, reference=None, rng=None):
        m = prediction.value[:, 0]
        return self.threshold - m

Combine it by passing it to Optimizer.set_acquisition().

Note

Optimizer.validate() detects and warns about a mismatch between the acquisition function’s requires_uncertainty and the surrogate’s provides_uncertainty. This warning will catch you if you pair an acquisition function with requires_uncertainty=True with a surrogate that doesn’t return std.

References#