whatif.py: model demolition correctly - reclaim materials aren't a dead loss

Was wrong earlier this session claiming demolition gives nothing back.
FIO's /rain/userplanetbuildingreclaimables confirms partial material
return (observed 50-90% depending on material, not a fixed fraction -
looked up live, not approximated). --remove now prices the real
reclaim for anything actually built and nets it against new capex,
so a demolish-and-reinvest question gets the true net cash picture
instead of double-penalizing the removed building.

Used this to check "demolish BHP's HWP, build a 2nd CHP or POL
instead" - both are clearly net losers even counting the ~57,750 AIC
reclaim credit (net capex still positive, and marginal profit is
negative either way) - dominated by just adding the new building
without removing anything, since nothing scarce actually forces the
swap.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 10:36:05 +02:00
co-authored by Claude Sonnet 5
parent 59245ee54d
commit 88dae754ca
+56 -7
View File
@@ -10,10 +10,11 @@ session - e.g. PP2 overloading pioneer housing after POL+CHP already maxed it).
tools/whatif.py before.yaml --add POL:1 # reuses POL's existing recipe in before.yaml
"""
import argparse, copy, sys
from collections import Counter
from pathlib import Path
import yaml
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from puga import config, market, prunplanner as pp
from puga import config, fio, market, prunplanner as pp
from puga.simulate import simulate
sys.path.insert(0, str(Path(__file__).resolve().parent))
@@ -59,6 +60,34 @@ def apply_infra(spec: dict, infra: str):
inf[ticker] = inf.get(ticker, 0) + int(amount)
def reclaim_value(ticker: str, qty: int, built_count: int, cx: str):
"""Sell value of the materials you'd actually get back demolishing `qty` of a building you
already own `built_count` of - real per-site data from FIO's own reclaimables endpoint
(demolition is NOT a dead loss: observed ~50-90%% of original materials back, varies by
material, not a fixed fraction - don't approximate it, look it up). None if you don't
actually own one (can't reclaim a hypothetical building)."""
if built_count <= 0 or qty <= 0:
return None
u = fio.me()
rec = fio.own(f"/rain/userplanetbuildingreclaimables/{u}", ttl=3600)
totals = Counter()
for x in rec:
if x["BuildingId"].startswith(f"{ticker}-"):
totals[x["Material"]] += x["Amount"]
if not totals:
return None
value = 0.0
items = {}
for mat, amt in totals.items():
q = round(amt / built_count * min(qty, built_count)) # amt is already summed over all built_count units
if q <= 0:
continue
w = market.walk(mat, cx, q, "sell")
items[mat] = q
value += w["total"]
return {"value": value, "items": items}
def run(spec, recipes, blds, price, faction, perm, built):
plan = plan_push.build_payload(spec, recipes, {b["building_ticker"] for b in blds})
planet = pp._g(f"/data/planet/{plan['planet_natural_id']}/", 3600)
@@ -122,15 +151,35 @@ def main():
d_profit = r_a["profit"] - r_b["profit"]
d_capex = r_a["new_capex"] - r_b["new_capex"]
before_counts = Counter(b["building"] for b in before_spec.get("buildings", []) for _ in range(b["amount"]))
after_counts = Counter(b["building"] for b in after_spec.get("buildings", []) for _ in range(b["amount"]))
reclaim_total = 0.0
reclaim_lines = []
for ticker in before_counts:
removed_qty = before_counts[ticker] - after_counts.get(ticker, 0)
if removed_qty > 0:
r = reclaim_value(ticker, removed_qty, built.get(ticker, 0), a.cx)
if r:
reclaim_total += r["value"]
items = ", ".join(f"{q}{m}" for m, q in r["items"].items())
reclaim_lines.append(f" {ticker} x{removed_qty}: {r['value']:,.0f} AIC ({items})")
print(f"BEFORE profit/day {r_b['profit']:,.0f} new_capex {r_b['new_capex']:,.0f}")
print(f"AFTER profit/day {r_a['profit']:,.0f} new_capex {r_a['new_capex']:,.0f}")
print(f"\nMARGINAL profit/day {d_profit:,.0f} new_capex {d_capex:,.0f}", end="")
if d_capex > 0 and d_profit > 0:
print(f" ROI/day {100*d_profit/d_capex:.2f}% payback {d_capex/d_profit:.1f} d")
elif d_capex <= 0:
print(" (no new capex)")
if reclaim_lines:
print(f"\nRECLAIMED MATERIALS (demolishing what's removed, sell value, real FIO data):")
for line in reclaim_lines:
print(line)
print(f" total: {reclaim_total:,.0f} AIC")
net_capex = d_capex - reclaim_total
net_label = f" net cash GAINED {-net_capex:,.0f}" if net_capex < 0 and reclaim_total else (f" net new capex {net_capex:,.0f}" if reclaim_total else "")
print(f"\nMARGINAL profit/day {d_profit:,.0f} new_capex {d_capex:,.0f}" + net_label, end="")
if net_capex > 0 and d_profit > 0:
print(f" ROI/day {100*d_profit/net_capex:.2f}% payback {net_capex/d_profit:.1f} d")
elif net_capex <= 0 and d_profit > 0:
print(" (no net new capex after reclaim)" if reclaim_total else " (no new capex)")
else:
print(" (marginal profit <= 0 - not worth it)")
print(" (marginal profit <= 0 - not worth it, regardless of reclaim)")
print("\nWORKFORCE (after) need supply open eff%")
newly_overloaded = []