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 tools/whatif.py before.yaml --add POL:1 # reuses POL's existing recipe in before.yaml
""" """
import argparse, copy, sys import argparse, copy, sys
from collections import Counter
from pathlib import Path from pathlib import Path
import yaml import yaml
sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) 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 from puga.simulate import simulate
sys.path.insert(0, str(Path(__file__).resolve().parent)) 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) 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): def run(spec, recipes, blds, price, faction, perm, built):
plan = plan_push.build_payload(spec, recipes, {b["building_ticker"] for b in blds}) 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) 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_profit = r_a["profit"] - r_b["profit"]
d_capex = r_a["new_capex"] - r_b["new_capex"] 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"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"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 reclaim_lines:
if d_capex > 0 and d_profit > 0: print(f"\nRECLAIMED MATERIALS (demolishing what's removed, sell value, real FIO data):")
print(f" ROI/day {100*d_profit/d_capex:.2f}% payback {d_capex/d_profit:.1f} d") for line in reclaim_lines:
elif d_capex <= 0: print(line)
print(" (no new capex)") 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: 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%") print("\nWORKFORCE (after) need supply open eff%")
newly_overloaded = [] newly_overloaded = []