import re
from collections.abc import Sequence
from dataclasses import dataclass
from ax.api.protocols import IMetric
[docs]
@dataclass(frozen=True, kw_only=True)
class Objective:
"""
An objective to optimize.
An objective represents a measurable outcome that you want to optimize.
The optimizer will try to minimize or maximize this outcome based on the
acquired data and evaluation function.
Attributes
----------
name : str
The name of the objective. This must match the key returned by the
evaluation function for this outcome.
minimize : bool
Whether to minimize or maximize the objective. Set to True for minimization
(e.g., reducing beam width) or False for maximization (e.g., increasing intensity).
Examples
--------
Define an objective to maximize beam intensity:
>>> from blop.ax.objective import Objective
>>> objective = Objective(name="beam_intensity", minimize=False)
Define an objective to minimize beam width:
>>> objective = Objective(name="beam_width", minimize=True)
"""
name: str
minimize: bool
[docs]
class ScalarizedObjective:
"""
A scalarized objective is a weighted sum of other objectives.
Use this to combine multiple objectives into a single optimization target
when you want to optimize a weighted combination of outcomes. This is useful
for multi-objective optimization where you have clear trade-off preferences.
Parameters
----------
expression : str
The expression to evaluate containing the objectives.
minimize : bool
Whether to minimize or maximize the expression.
**objective_names : str
Keyword arguments mapping variables in the expression to objective names.
Examples
--------
Create a scalarized objective for minimizing a weighted sum:
>>> from blop.ax.objective import ScalarizedObjective
>>> scalarized_obj = ScalarizedObjective(
... expression="x + 2 * y",
... minimize=True,
... x="objective1",
... y="objective2",
... )
>>> print(scalarized_obj)
-(objective1 + 2 * objective2)
Notes
-----
The variable names used in the expression (e.g., "intensity", "width") are
arbitrary and do not need to match the objective names. They are mapped to
objectives via the keyword arguments for readability.
"""
def __init__(self, expression: str, *, minimize: bool, **objective_names: str):
self._expression = expression
self._objective_names = objective_names
self._minimize = minimize
if not self._objective_names:
raise ValueError("ScalarizedObjective requires at least one objective.")
invalidated = [key for key in self._objective_names if key not in self._expression]
if invalidated:
raise ValueError(f"Objectives {invalidated} not found in expression '{self._expression}'.")
@property
def ax_expression(self) -> str:
"""
Convert the scalarized objective to a string that can be used by Ax.
Returns
-------
str
The expression with objective names substituted, negated if minimizing.
"""
template = self._expression
for key in self._objective_names:
template = re.sub(f"\\b{key}\\b", f"{{{key}}}", template)
objective_str = template.format(**self._objective_names)
return f"-({objective_str})" if self._minimize else objective_str
def __repr__(self) -> str:
return (
f"ScalarizedObjective('{self._expression}', minimize={self._minimize}, "
f"{', '.join([f'{k}={v}' for k, v in self._objective_names.items()])})"
)
def __str__(self) -> str:
return self.ax_expression
[docs]
class OutcomeConstraint:
"""
A constraint on an outcome of a suggestion.
Outcome constraints guide the optimizer to prefer solutions that satisfy certain
conditions on the measured outcomes. This is a *soft* constraint, meaning that
the constraint may be violated during exploration but will be increasingly
satisfied as optimization progresses.
Parameters
----------
constraint : str
The constraint expression to evaluate. Must be a valid inequality using
operators like <=, >=, <, >. Variable names in the expression are mapped
to outcomes via keyword arguments.
**outcomes : Objective | IMetric
Keyword arguments mapping variables in the expression to objectives or metrics.
Examples
--------
Constrain an objective to be below a threshold:
>>> from blop.ax.objective import Objective, OutcomeConstraint
>>> temp_obj = Objective(name="temperature", minimize=True)
>>> constraint = OutcomeConstraint("temp <= 100", temp=temp_obj)
>>> print(constraint)
temperature <= 100
For complete examples with multiple constraints, see :doc:`/how-to-guides/set-outcome-constraints`.
Notes
-----
The variable names used in the constraint expression (e.g., "temp", "i", "w")
are arbitrary and do not need to match the objective names. They are mapped
to objectives via the keyword arguments for readability.
Outcome constraints differ from DOF constraints in that they constrain the
measured outcomes rather than the input parameters.
"""
def __init__(self, constraint: str, **outcomes: Objective | IMetric):
self._constraint = constraint
self._outcomes = outcomes
self._validate_outcomes()
def _validate_outcomes(self) -> None:
if not self._outcomes:
raise ValueError("OutcomeConstraint requires at least one outcome.")
invalidated = [name for name in self._outcomes if name not in self._constraint]
if invalidated:
raise ValueError(f"Outcomes {invalidated} not found in constraint '{self._constraint}'.")
@property
def ax_constraint(self) -> str:
"""
Convert the constraint to a string that can be used by Ax.
Returns
-------
str
The constraint expression with objective names substituted.
"""
template = self._constraint
for key in self._outcomes:
template = re.sub(f"\\b{key}\\b", f"{{{key}}}", template)
return template.format(**{key: outcome.name for key, outcome in self._outcomes.items()})
def __repr__(self) -> str:
outcomes_str = ", ".join(f"{k}={v.name}" for k, v in self._outcomes.items())
return f"OutcomeConstraint('{self._constraint}', {outcomes_str})"
def __str__(self) -> str:
return self.ax_constraint
def to_ax_objective_str(objectives: Sequence[Objective]) -> str:
"""
Convert a list of objectives to a string that can be used by Ax.
This is a utility function used internally to format objectives for Ax's API.
Minimized objectives are prefixed with a minus sign.
Parameters
----------
objectives : Sequence[Objective]
The objectives to convert to a string.
Returns
-------
str
The string representation of the objectives, comma-separated with minus
signs for minimization.
Examples
--------
>>> from blop.ax.objective import Objective, to_ax_objective_str
>>> objectives = [
... Objective(name="intensity", minimize=False),
... Objective(name="width", minimize=True)
... ]
>>> to_ax_objective_str(objectives)
'intensity, -width'
"""
return ", ".join([o.name if not o.minimize else f"-{o.name}" for o in objectives])