Skip to content

feat(design): coupled FactorConstraint for build_grid (#103)#106

Merged
jc-macdonald merged 1 commit into
mainfrom
feat/103-constrained-qmc
May 12, 2026
Merged

feat(design): coupled FactorConstraint for build_grid (#103)#106
jc-macdonald merged 1 commit into
mainfrom
feat/103-constrained-qmc

Conversation

@jc-macdonald
Copy link
Copy Markdown
Contributor

Closes #103.

Summary

Adds FactorConstraint for design-time coupled-factor constraints (e.g. "if method == 'elbo_only' then patience <= 2") and plumbs a constraints= kwarg through build_grid for full, lhs, sobol, and halton.

This is distinct from the existing protocols.Constraint, which filters trials after simulation based on observables. FactorConstraint filters candidate configs before evaluation so QMC budget isn't wasted on points that would be discarded post-hoc.

Behavior

  • Full factorial: filters the Cartesian product directly.
  • LHS / Sobol / Halton: rejection sampling with doubling oversample (cap 64×).
    • Emits a UserWarning when realized feasibility ratio < 10% (QMC space-filling is degraded by rejection).
    • Raises ValueError if the budget is exhausted before n_samples feasible configs are collected.
  • Backward compatible — constraints defaults to None and existing call sites are unchanged.

Example

from trade_study import Factor, FactorType, FactorConstraint, build_grid

factors = [
    Factor("method", FactorType.CATEGORICAL, levels=["elbo_only", "mixed", "full"]),
    Factor("patience", FactorType.DISCRETE, levels=[1, 2, 3, 5, 10]),
]
constraints = [
    FactorConstraint(
        lambda c: c["method"] != "elbo_only" or c["patience"] <= 2,
        name="elbo_short_patience",
    ),
]
grid = build_grid(factors, method="sobol", n_samples=64, seed=0, constraints=constraints)

Tests

8 new tests in tests/test_design.py covering:

  • predicate evaluation
  • full-factorial filtering and the all-rejected edge case
  • deterministic seeding under constraints (parametrized over sobol/halton/lhs)
  • low-feasibility warning
  • budget-exhausted ValueError

CI

just ci clean: lint, mypy strict, 275 tests pass, design.py at 100% line coverage.

Follow-ups

Add FactorConstraint dataclass that wraps a predicate over candidate
configs and plumb a constraints= kwarg through build_grid for full
factorial, LHS, and Sobol/Halton QMC.

- Full factorial filters the Cartesian product directly.
- LHS / QMC use rejection sampling with doubling oversample (cap 64x)
  and warn when the realized feasibility ratio drops below 10%.
- Raises ValueError if the budget is exhausted before n_samples
  feasible configs are collected.

Distinct from protocols.Constraint (output feasibility, post-sim);
FactorConstraint is design-time so QMC budgets are not wasted on
configs that will be filtered after evaluation.

Tests cover deterministic seeding, full-factorial filtering, the
all-rejected edge case, the low-feasibility warning, and the
budget-exhausted error path.
@jc-macdonald jc-macdonald merged commit f95b0bd into main May 12, 2026
4 checks passed
@jc-macdonald jc-macdonald deleted the feat/103-constrained-qmc branch May 12, 2026 14:22
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.

Constrained QMC: coupled factor constraints in design generation

1 participant