dws-city-res-solve/docs/superpowers/specs/2026-06-11-configurable-objective-design.md
Pagwin d18e533559
spec: configurable maximize criteria for solve()
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 19:07:58 -04:00

4.2 KiB
Raw Blame History

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):
  • None for either argument falls back to the module-level default, the same convention resource_constraints already 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_mode not 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 with AddMultiplicationEquality, carrying a running upper bound multiplied from the Phase-1 caps. This replaces the hand-chained prodEE/prodSS/ prodBB/prodEB/obj block and the Eprod/Bprod/Sprod bound helpers.
  • sum: maximize Σₖ factorₖ · finalₖ — a single m.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.
  • C gets 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 ≤ cap constraints 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 by 10^(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.py keeps 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).