4.2 KiB
Configurable Maximize Criteria for solve()
Date: 2026-06-11 Status: Approved
Problem
The objective in solve.py is hardcoded at the bottom of solve() (the
"OBJECTIVE IS SET HERE" block): it maximizes the monomial
finalE² · finalB¹ · finalS² via hand-chained AddMultiplicationEquality
calls, with intermediate bounds derived from the Phase-1 per-resource
ceilings. Changing the criteria means editing solver internals. The actual
usage pattern (per git history) is varying the per-resource powers, so the
criteria should be a parameter of solve().
API
Two new keyword-only arguments on solve(), with module-level defaults in
the PARAMETERS section (matching the existing INITIAL / FIXED_CHOICES
pattern):
OBJECTIVE_FACTORS = {"E": 2, "B": 1, "S": 2} # missing keys = 0 (excluded)
OBJECTIVE_MODE = "product" # "product" or "sum"
def solve(*, ..., objective_factors=None, objective_mode=None):
Nonefor either argument falls back to the module-level default, the same conventionresource_constraintsalready uses.- Defaults reproduce the current hardcoded objective (
E²·B·S²) exactly. - Valid factor keys:
"E","B","S","C"(Capital becomes targetable). Values are integers.
Validation (fail fast with ValueError)
- Unknown key in
objective_factors. - All factors zero (or empty dict) — degenerate objective.
objective_modenot in{"product", "sum"}.- Non-integer factor values.
- Negative factor values in product mode only: a negative exponent means division, which integer CP-SAT cannot express. Sum mode accepts negative weights — CP-SAT handles negative coefficients in linear objectives natively.
Semantics
The factor means exponent in product mode and weight in sum mode.
product: maximizeΠₖ finalₖ^factorₖ. Built generically: expand the factors to a flat list of final-resource vars (e.g.[finalE, finalE, finalB, finalS, finalS]), then fold pairwise withAddMultiplicationEquality, carrying a running upper bound multiplied from the Phase-1 caps. This replaces the hand-chainedprodEE/prodSS/prodBB/prodEB/objblock and theEprod/Bprod/Sprodbound helpers.sum: maximizeΣₖ factorₖ · finalₖ— a singlem.Maximize(...)on a linear expression; no auxiliary variables. May be negative when negative weights are used.
A factor of 0 drops the resource from the objective (exponent 0 → factor of 1 in product mode), matching current behavior where Capital simply isn't in the objective. It does not force the resource to zero.
Phase-1 ceilings
Ceilings (_ceiling) are computed only for resources with a nonzero
factor. Rationale:
- Each ceiling solve costs up to 20 s; skipping unused resources is a real win.
Cgets a ceiling only when it appears in a product objective, where the bound is required for the intermediate product variables.- Sum mode does not need caps for bounds, but the redundant
final ≤ capconstraints are still added for computed ceilings since they prune the search.
Reporting
_report receives the resolved objective spec (factors + mode) and prints
the expression that was actually maximized along with its value, replacing
the hardcoded product(scaled) = E·B·S line. Examples:
- product mode:
objective E^2*B*S^2 = <value>with the value descaled by10^(sum of exponents). - sum mode:
objective 2E + B - S = <value>descaled by 10.
finalC (C[NUM_STEPS + 1]) participates in the printout when "C" has a
nonzero factor; _report already receives the C pool dict. The
per-resource FINAL line and the intermediate solution printer (E/B/S)
remain unchanged.
Unchanged
- The intermediate solution printer still tracks E/B/S.
web_solve.pykeeps working unmodified — both new arguments are optional with behavior-preserving defaults.
Bounds note
With MAX_RES = 2000, the default product bound is
2000⁵ = 3.2 × 10¹⁶, comfortably inside CP-SAT's int64 objective range.
Extreme factor values could overflow; the fold computes the running bound
explicitly, so an overflow would surface as a CP-SAT model error rather
than silent wraparound. No additional guard is included (YAGNI).