added intermediate printing

This commit is contained in:
Pagwin 2026-06-05 13:57:00 -04:00
parent 87b6529671
commit 2db8ff63c7
No known key found for this signature in database
GPG key ID: 81137023740CA260
3 changed files with 185 additions and 12 deletions

3
.gitignore vendored Normal file
View file

@ -0,0 +1,3 @@
.serena
__pycache__
output.txt

174
main.py
View file

@ -11,7 +11,7 @@ LP/MIP solver. Read the comments at:
""" """
from ortools.sat.python import cp_model from ortools.sat.python import cp_model
import printer
# ====================================================================== # ======================================================================
# PARAMETERS -- edit these # PARAMETERS -- edit these
@ -24,10 +24,10 @@ INITIAL = (3, 3, 3, 3)
# at the START of that step. Types: 'H' Hub, 'F' Foundry, 'M' Metropolis, 'N' Monument. # at the START of that step. Types: 'H' Hub, 'F' Foundry, 'M' Metropolis, 'N' Monument.
# Total cities across all steps must be <= 7. Arriving cities act that same step. # Total cities across all steps must be <= 7. Arriving cities act that same step.
ARRIVALS = { ARRIVALS = {
1: ["H", "F", "H", "N", "F"], 1: ["H", "F", "H"],
2: ["M"], 2: ["F"],
3: [], 3: [],
4: [], 4: ["N"],
5: [], 5: [],
} }
@ -81,9 +81,12 @@ def solve(
# action variables, t in 1..NUM_STEPS # action variables, t in 1..NUM_STEPS
col, ua, ub, ud = {}, {}, {}, {} # collect / upgrade a,b,d col, ua, ub, ud = {}, {}, {}, {} # collect / upgrade a,b,d
ow = {} # overwork (global limit: 1 per step)
rH, rF, rM = {}, {}, {} # renovate -> Hub/Foundry/Metro rH, rF, rM = {}, {}, {} # renovate -> Hub/Foundry/Metro
cvE, cvB, cvS = {}, {}, {} # foundry: which vat collected cvE, cvB, cvS = {}, {}, {} # foundry: which vat collected (normal collect)
owcvE, owcvB, owcvS = {}, {}, {} # foundry: which vat collected (overwork)
mE, mB, mS, mC = {}, {}, {}, {} # metropolis: resources picked (+1 each) mE, mB, mS, mC = {}, {}, {}, {} # metropolis: resources picked (+1 each)
owmE, owmB, owmS, owmC = {}, {}, {}, {} # metropolis: resources picked (overwork)
for i in range(N): for i in range(N):
a_step, a_type = cities[i] a_step, a_type = cities[i]
@ -110,16 +113,24 @@ def solve(
ua[i, t] = m.NewBoolVar(f"ua_{i}_{t}") ua[i, t] = m.NewBoolVar(f"ua_{i}_{t}")
ub[i, t] = m.NewBoolVar(f"ub_{i}_{t}") ub[i, t] = m.NewBoolVar(f"ub_{i}_{t}")
ud[i, t] = m.NewBoolVar(f"ud_{i}_{t}") ud[i, t] = m.NewBoolVar(f"ud_{i}_{t}")
ow[i, t] = m.NewBoolVar(f"ow_{i}_{t}")
rH[i, t] = m.NewBoolVar(f"rH_{i}_{t}") rH[i, t] = m.NewBoolVar(f"rH_{i}_{t}")
rF[i, t] = m.NewBoolVar(f"rF_{i}_{t}") rF[i, t] = m.NewBoolVar(f"rF_{i}_{t}")
rM[i, t] = m.NewBoolVar(f"rM_{i}_{t}") rM[i, t] = m.NewBoolVar(f"rM_{i}_{t}")
cvE[i, t] = m.NewBoolVar(f"cvE_{i}_{t}") cvE[i, t] = m.NewBoolVar(f"cvE_{i}_{t}")
cvB[i, t] = m.NewBoolVar(f"cvB_{i}_{t}") cvB[i, t] = m.NewBoolVar(f"cvB_{i}_{t}")
cvS[i, t] = m.NewBoolVar(f"cvS_{i}_{t}") cvS[i, t] = m.NewBoolVar(f"cvS_{i}_{t}")
owcvE[i, t] = m.NewBoolVar(f"owcvE_{i}_{t}")
owcvB[i, t] = m.NewBoolVar(f"owcvB_{i}_{t}")
owcvS[i, t] = m.NewBoolVar(f"owcvS_{i}_{t}")
mE[i, t] = m.NewIntVar(0, 3, f"mE_{i}_{t}") mE[i, t] = m.NewIntVar(0, 3, f"mE_{i}_{t}")
mB[i, t] = m.NewIntVar(0, 3, f"mB_{i}_{t}") mB[i, t] = m.NewIntVar(0, 3, f"mB_{i}_{t}")
mS[i, t] = m.NewIntVar(0, 3, f"mS_{i}_{t}") mS[i, t] = m.NewIntVar(0, 3, f"mS_{i}_{t}")
mC[i, t] = m.NewIntVar(0, 3, f"mC_{i}_{t}") mC[i, t] = m.NewIntVar(0, 3, f"mC_{i}_{t}")
owmE[i, t] = m.NewIntVar(0, 6, f"owmE_{i}_{t}")
owmB[i, t] = m.NewIntVar(0, 6, f"owmB_{i}_{t}")
owmS[i, t] = m.NewIntVar(0, 6, f"owmS_{i}_{t}")
owmC[i, t] = m.NewIntVar(0, 6, f"owmC_{i}_{t}")
# ---- per-step gain/cost accumulators (linear expressions) --------- # ---- per-step gain/cost accumulators (linear expressions) ---------
gain_E = {t: [] for t in range(1, NUM_STEPS + 1)} gain_E = {t: [] for t in range(1, NUM_STEPS + 1)}
@ -150,7 +161,30 @@ def solve(
for v in (isH, isF, isM, isMon, hasA, hasB, hasD, vE, vB, vS): for v in (isH, isF, isM, isMon, hasA, hasB, hasD, vE, vB, vS):
m.Add(v[i, t] == 0) m.Add(v[i, t] == 0)
if t <= NUM_STEPS: if t <= NUM_STEPS:
for v in (col, ua, ub, ud, rH, rF, rM, cvE, cvB, cvS, mE, mB, mS, mC): for v in (
col,
ua,
ub,
ud,
ow,
rH,
rF,
rM,
cvE,
cvB,
cvS,
owcvE,
owcvB,
owcvS,
mE,
mB,
mS,
mC,
owmE,
owmB,
owmS,
owmC,
):
m.Add(v[i, t] == 0) m.Add(v[i, t] == 0)
# action + transition logic for active steps # action + transition logic for active steps
@ -167,6 +201,7 @@ def solve(
+ ua[i, t] + ua[i, t]
+ ub[i, t] + ub[i, t]
+ ud[i, t] + ud[i, t]
+ ow[i, t]
+ rH[i, t] + rH[i, t]
+ rF[i, t] + rF[i, t]
+ rM[i, t] + rM[i, t]
@ -192,6 +227,12 @@ def solve(
m.Add(ua[i, t] == 0).OnlyEnforceIf(isMon[i, t]) m.Add(ua[i, t] == 0).OnlyEnforceIf(isMon[i, t])
m.Add(ub[i, t] == 0).OnlyEnforceIf(isMon[i, t]) m.Add(ub[i, t] == 0).OnlyEnforceIf(isMon[i, t])
m.Add(ud[i, t] == 0).OnlyEnforceIf(isMon[i, t]) m.Add(ud[i, t] == 0).OnlyEnforceIf(isMon[i, t])
m.Add(ow[i, t] == 0).OnlyEnforceIf(isMon[i, t])
# overwork cooldown: city that overworked last step can't collect or overwork
if t > a_step:
m.Add(col[i, t] == 0).OnlyEnforceIf(ow[i, t - 1])
m.Add(ow[i, t] == 0).OnlyEnforceIf(ow[i, t - 1])
# ---- type transition t -> t+1 ---- # ---- type transition t -> t+1 ----
m.Add(isH[i, t + 1] == isH[i, t]).OnlyEnforceIf(no_ren) m.Add(isH[i, t + 1] == isH[i, t]).OnlyEnforceIf(no_ren)
@ -272,6 +313,61 @@ def solve(
gain_B[t].append(AND(cvB[i, t], hasB[i, t])) gain_B[t].append(AND(cvB[i, t], hasB[i, t]))
gain_S[t].append(AND(cvS[i, t], hasB[i, t])) gain_S[t].append(AND(cvS[i, t], hasB[i, t]))
# ---- overwork sub-choices (double-collect; no Capital cost) ----
fow = AND(isF[i, t], ow[i, t])
mow = AND(isM[i, t], ow[i, t])
how = AND(isH[i, t], ow[i, t])
# foundry overwork: exactly one vat chosen iff foundry overworks
m.Add(owcvE[i, t] + owcvB[i, t] + owcvS[i, t] == fow)
# metropolis overwork: pick 2*(2 + hasB) resources; else nothing
ow_npick = m.NewIntVar(0, 6, "")
m.Add(ow_npick == 2 * (2 + hasB[i, t])).OnlyEnforceIf(mow)
m.Add(ow_npick == 0).OnlyEnforceIf(mow.Not())
m.Add(owmE[i, t] + owmB[i, t] + owmS[i, t] + owmC[i, t] == ow_npick)
# ================= OVERWORK GAINS (2x normal collect) =================
# Hub overwork: 2*(2 + hasB) Capital
hub_ow_gain = m.NewIntVar(0, 6, "")
m.Add(hub_ow_gain == 2 * (2 + hasB[i, t])).OnlyEnforceIf(how)
m.Add(hub_ow_gain == 0).OnlyEnforceIf(how.Not())
gain_C[t].append(hub_ow_gain)
# Metropolis overwork: +1 per pick (picks are already doubled via ow_npick)
gain_E[t].append(owmE[i, t])
gain_B[t].append(owmB[i, t])
gain_S[t].append(owmS[i, t])
gain_C[t].append(owmC[i, t])
# Foundry overwork: 2 * chosen vat's value
owgEf = m.NewIntVar(0, 2 * max_vat, "")
owgBf = m.NewIntVar(0, 2 * max_vat, "")
owgSf = m.NewIntVar(0, 2 * max_vat, "")
_owE = m.NewIntVar(0, max_vat, "")
_owB = m.NewIntVar(0, max_vat, "")
_owS = m.NewIntVar(0, max_vat, "")
m.AddMultiplicationEquality(_owE, [owcvE[i, t], vE[i, t]])
m.AddMultiplicationEquality(_owB, [owcvB[i, t], vB[i, t]])
m.AddMultiplicationEquality(_owS, [owcvS[i, t], vS[i, t]])
m.Add(owgEf == 2 * _owE)
m.Add(owgBf == 2 * _owB)
m.Add(owgSf == 2 * _owS)
gain_E[t].append(owgEf)
gain_B[t].append(owgBf)
gain_S[t].append(owgSf)
# Collect Bonus (b) for foundry overwork: 2 * (+1) = +2 of that resource
ow_be = AND(owcvE[i, t], hasB[i, t])
ow_bb = AND(owcvB[i, t], hasB[i, t])
ow_bs = AND(owcvS[i, t], hasB[i, t])
gain_E[t].append(ow_be)
gain_E[t].append(ow_be) # added twice == *2
gain_B[t].append(ow_bb)
gain_B[t].append(ow_bb)
gain_S[t].append(ow_bs)
gain_S[t].append(ow_bs)
# ---- vat update producing vat[i, t+1] ---- # ---- vat update producing vat[i, t+1] ----
# increment added to the two non-collected vats (1, or 2 with upgrade d) # increment added to the two non-collected vats (1, or 2 with upgrade d)
inc = m.NewIntVar(1, 2, "") inc = m.NewIntVar(1, 2, "")
@ -293,11 +389,24 @@ def solve(
m.Add(vSn == 0).OnlyEnforceIf(cvS[i, t]) m.Add(vSn == 0).OnlyEnforceIf(cvS[i, t])
m.Add(vEn == vE[i, t] + inc).OnlyEnforceIf(cvS[i, t]) m.Add(vEn == vE[i, t] + inc).OnlyEnforceIf(cvS[i, t])
m.Add(vBn == vB[i, t] + inc).OnlyEnforceIf(cvS[i, t]) m.Add(vBn == vB[i, t] + inc).OnlyEnforceIf(cvS[i, t])
# foundry but not collecting (upgrade / renovate-away): vats unchanged # overwork vat transitions: same reset/increment as normal collect
f_noncollect = AND(isF[i, t], col[i, t].Not()) m.Add(vEn == 0).OnlyEnforceIf(owcvE[i, t])
m.Add(vEn == vE[i, t]).OnlyEnforceIf(f_noncollect) m.Add(vBn == vB[i, t] + inc).OnlyEnforceIf(owcvE[i, t])
m.Add(vBn == vB[i, t]).OnlyEnforceIf(f_noncollect) m.Add(vSn == vS[i, t] + inc).OnlyEnforceIf(owcvE[i, t])
m.Add(vSn == vS[i, t]).OnlyEnforceIf(f_noncollect) m.Add(vBn == 0).OnlyEnforceIf(owcvB[i, t])
m.Add(vEn == vE[i, t] + inc).OnlyEnforceIf(owcvB[i, t])
m.Add(vSn == vS[i, t] + inc).OnlyEnforceIf(owcvB[i, t])
m.Add(vSn == 0).OnlyEnforceIf(owcvS[i, t])
m.Add(vEn == vE[i, t] + inc).OnlyEnforceIf(owcvS[i, t])
m.Add(vBn == vB[i, t] + inc).OnlyEnforceIf(owcvS[i, t])
# foundry but neither collecting nor overworking: vats unchanged
# f_noncollect_noow = isF AND NOT fcol AND NOT fow = isF - fcol - fow
f_noncollect_noow = m.NewBoolVar("")
m.Add(f_noncollect_noow == isF[i, t] - fcol - fow)
m.Add(vEn == vE[i, t]).OnlyEnforceIf(f_noncollect_noow)
m.Add(vBn == vB[i, t]).OnlyEnforceIf(f_noncollect_noow)
m.Add(vSn == vS[i, t]).OnlyEnforceIf(f_noncollect_noow)
# assign vat[i, t+1]: # assign vat[i, t+1]:
# renovate-to-foundry -> reset to 1 # renovate-to-foundry -> reset to 1
@ -319,6 +428,10 @@ def solve(
m.Add(vB[i, t + 1] == 0).OnlyEnforceIf(not_F_next) m.Add(vB[i, t + 1] == 0).OnlyEnforceIf(not_F_next)
m.Add(vS[i, t + 1] == 0).OnlyEnforceIf(not_F_next) m.Add(vS[i, t + 1] == 0).OnlyEnforceIf(not_F_next)
# ---- global overwork limit: at most 1 city overworks per step ----
for t in range(1, NUM_STEPS + 1):
m.Add(sum(ow[i, t] for i in range(N)) <= 1)
# ---- resource pool recursion -------------------------------------- # ---- resource pool recursion --------------------------------------
E = {1: m.NewIntVar(initial[0], initial[0], "E1")} E = {1: m.NewIntVar(initial[0], initial[0], "E1")}
B = {1: m.NewIntVar(initial[1], initial[1], "B1")} B = {1: m.NewIntVar(initial[1], initial[1], "B1")}
@ -382,7 +495,12 @@ def solve(
solver = cp_model.CpSolver() solver = cp_model.CpSolver()
solver.parameters.max_time_in_seconds = time_limit solver.parameters.max_time_in_seconds = time_limit
solver.parameters.num_search_workers = num_workers solver.parameters.num_search_workers = num_workers
status = solver.Solve(m) status = solver.Solve(
m,
printer.IntermediateSolutionPrinter(
{"electrum": finalE, "brass": finalB, "steel": finalS}
),
)
if verbose: if verbose:
print(f"(resource ceilings used: E<={capE} B<={capB} S<={capS})") print(f"(resource ceilings used: E<={capE} B<={capB} S<={capS})")
@ -399,16 +517,24 @@ def solve(
ua, ua,
ub, ub,
ud, ud,
ow,
rH, rH,
rF, rF,
rM, rM,
cvE, cvE,
cvB, cvB,
cvS, cvS,
owcvE,
owcvB,
owcvS,
mE, mE,
mB, mB,
mS, mS,
mC, mC,
owmE,
owmB,
owmS,
owmC,
hasA, hasA,
hasB, hasB,
hasD, hasD,
@ -439,16 +565,24 @@ def _report(
ua, ua,
ub, ub,
ud, ud,
ow,
rH, rH,
rF, rF,
rM, rM,
cvE, cvE,
cvB, cvB,
cvS, cvS,
owcvE,
owcvB,
owcvS,
mE, mE,
mB, mB,
mS, mS,
mC, mC,
owmE,
owmB,
owmS,
owmC,
hasA, hasA,
hasB, hasB,
hasD, hasD,
@ -497,6 +631,22 @@ def _report(
picks += [nm] * solver.Value(var[i, t]) picks += [nm] * solver.Value(var[i, t])
return "Collect {" + ",".join(picks) + "}" return "Collect {" + ",".join(picks) + "}"
return "Collect (+Capital)" return "Collect (+Capital)"
if solver.Value(ow[i, t]):
if typ_name[i, t] == "Foundry":
v = (
"E"
if solver.Value(owcvE[i, t])
else "B"
if solver.Value(owcvB[i, t])
else "S"
)
return f"Overwork vat {v} (2x)"
if typ_name[i, t] == "Metro":
picks = []
for nm, var in (("E", owmE), ("B", owmB), ("S", owmS), ("C", owmC)):
picks += [nm] * solver.Value(var[i, t])
return "Overwork {" + ",".join(picks) + "} (2x)"
return "Overwork (+Capital 2x)"
if solver.Value(ua[i, t]): if solver.Value(ua[i, t]):
return "Upgrade a (cost-reduction)" return "Upgrade a (cost-reduction)"
if solver.Value(ub[i, t]): if solver.Value(ub[i, t]):

20
printer.py Normal file
View file

@ -0,0 +1,20 @@
from ortools.sat.python import cp_model
class IntermediateSolutionPrinter(cp_model.CpSolverSolutionCallback):
"""Callback that prints intermediate solutions."""
def __init__(self, variables):
cp_model.CpSolverSolutionCallback.__init__(self)
self._variables = variables
self._solution_count = 0
def on_solution_callback(self):
"""Called each time an improving solution is found."""
print("\n--- Solution ---")
for name, var in self._variables.items():
print(f"{name} = {self.Value(var)}")
@property
def solution_count(self):
return self._solution_count