Skip to content

Add Constraint primitive; replace set_outcome_constraints string DSL#27

Merged
499602D2 merged 1 commit into
mainfrom
feat/constraint-primitive
Apr 24, 2026
Merged

Add Constraint primitive; replace set_outcome_constraints string DSL#27
499602D2 merged 1 commit into
mainfrom
feat/constraint-primitive

Conversation

@499602D2

@499602D2 499602D2 commented Apr 22, 2026

Copy link
Copy Markdown
Owner

Replaces the set_outcome_constraints string DSL with a proper Constraint primitive modeled as a peer of Objective. Constraint metrics register as tracking metrics on the Ax experiment before the OutcomeConstraint references them, so their values flow through raw_data alongside objectives and Ax never silently marks a trial failed for "missing required metric". Bounds are absolute; Ax's OutcomeConstraint defaults to relative, which would silently change the meaning of every gte/lte supplied.

Addresses #24. The pattern Bulut tried in the comment thread (register a metric as both an objective and a constraint via the string DSL) crashed Ax with "Cannot constrain on objective metric", and the alternative of referencing an unregistered metric name silently marked every trial failed. Constraint is the peer-of-Objective registration asked for in the thread.

Self-bounds on Objective (Objective(..., lte=X)) as sketched in #24 are a follow-up.

Migration

# Before: string DSL. Only worked if the constraint name was also a
# registered objective, which then triggered
# "Cannot constrain on objective metric".
session.backend.set_objectives([objective, Objective(name="pressure", ...)])
session.backend.set_outcome_constraints(["pressure >= 45.0"])

# After
pressure = Constraint(
    name="pressure", objective_function=read_pressure, gte=45.0,
)
session.backend.set_objectives([objective])
session.backend.set_constraints([pressure])

Validation

Constraint.__init__ rejects at construction:

  • No bound (either gte or lte must be supplied)
  • Inverted bounds (gte > lte)
  • Single-point feasible range (gte == lte): meaningless for a soft outcome constraint on a continuous GP surrogate; use gte=X-eps, lte=X+eps for a tight tolerance
  • Non-finite bounds (NaN, ±inf): use None for "no bound on this side"

Backend.set_constraints rejects at registration:

  • Anything that is not a Constraint (an Objective smuggled in used to crash later with AttributeError; now rejects up front with a clear TypeError)

Tests

Unit tests cover every validation branch. Three canaries drive real Ax end-to-end:

  • TestConstraintConvergenceCanary: BO on a 2D quadratic with x1 >= 2; asserts best feasible point is near the constrained optimum.
  • TestMOOWithConstraint: two Objectives in Pareto MOO mode plus one Constraint. Asserts BO explores both ends of the feasible Pareto front.
  • TestSessionLoopWithConstraint: drives Session.local_optimization end-to-end with a real AxBackend and a Constraint, staging cases in the archival dir between cycles. Catches orchestration bugs in the Case metadata to backend.tell handoff.

@499602D2 499602D2 requested a review from blttkgl April 22, 2026 22:03
@499602D2

Copy link
Copy Markdown
Owner Author

@blttkgl this is the top PR, built on top of #25 and #26.

@blttkgl

blttkgl commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Hey @499602D2 ,

Things seems to work on my end. I pushed two commits, one is making changes in session.py to print/log the scalarizedObjectives and constraints, the other one is changing the aerofoil tutorial to use a lift constraint. If you could test it on your end I think we can merge this PR alongside #26 .

@blttkgl

blttkgl commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

Having said that I do get a few warnings during the tutorial run, pasting them below. Any thoughts?


/home/blttkgl/flowboost/.venv/lib/python3.12/site-packages/gpytorch/likelihoods/noise_models.py:150: NumericalWarning: Very small noise values detected. This will likely lead to numerical instabilities. Rounding small noise values up to 1e-06.
  warnings.warn(
/home/blttkgl/flowboost/.venv/lib/python3.12/site-packages/botorch/optim/optimize.py:796: RuntimeWarning: Optimization failed in `gen_candidates_scipy` with the following warning(s):
[OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL: .'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL: .'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL: .'), OptimizationWarning('Optimization failed within `scipy.optimize.minimize` with status 2 and message ABNORMAL: .')]
Trying again with a new set of initial conditions.
  return _optimize_acqf_batch(opt_inputs=opt_inputs)
/home/blttkgl/flowboost/.venv/lib/python3.12/site-packages/botorch/optim/optimize.py:796: RuntimeWarning: Optimization failed on the second try, after generating a new set of initial conditions.
  return _optimize_acqf_batch(opt_inputs=opt_inputs)

@blttkgl

blttkgl commented Apr 23, 2026

Copy link
Copy Markdown
Collaborator

I wonder if we should also add a random seed in the aerofoil tutorial, for better reproducibility? I had a test where I reached the best design by design 12, and current one is still stuck at a local maxima around design 20.. It just seems that the model is very big on exploitation, but exploration is secondary. Do you think this is due to non-deterministic seed values, or is there a way on Ax backend to prioritize exploration over exploitation, similar to e.g. GpyOPT?

image

The best design is somewhere along angleOfAttack ~10, but we are kinda stuck around 17.

@499602D2 499602D2 force-pushed the refactor/scalarized-objective branch 3 times, most recently from 400f5ac to b72c173 Compare April 24, 2026 10:38
@499602D2 499602D2 force-pushed the feat/constraint-primitive branch from 2614b20 to d34b6f0 Compare April 24, 2026 11:00
@499602D2 499602D2 changed the base branch from refactor/scalarized-objective to main April 24, 2026 11:00
@499602D2 499602D2 force-pushed the feat/constraint-primitive branch from d34b6f0 to c01b1d7 Compare April 24, 2026 11:04
@499602D2 499602D2 merged commit b5d9b2d into main Apr 24, 2026
6 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants