Add real ship flight fuel/cost model (puga/shipping.py)

Calibrated from a controlled slider sweep across 5 real destinations, read
from the live APEX Ship Flight Control DOM. Covers STL cruise, FTL
jumps/charges, gateway hops (measured LOCK fees for ANT<->HRT/BEN), and
per-planet landing fuel. Scoped to this account's one hull/reactor; open
gaps (gravity dependence, hull generalization, gateway one-way links,
per-gateway volume caps) are documented in the module docstring.

tools/shiptrip.py is the simple entry point (ship + from + to, works out
jumps/charges/landing itself); tools/shipcost.py takes explicit physics
params for when the breakdown is already known. puga/route.py now also
resolves CX station codes (ANT, HRT, ...), not just planets/systems - this
was needed for the gateway-aware lookups to work at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-10-09 13:32:58 +02:00
co-authored by Claude Sonnet 5
parent 88dae754ca
commit 9a4c7cc57c
5 changed files with 406 additions and 2 deletions
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env python3
"""Fuel/AIC cost for a ship trip FROM EXPLICIT PARAMETERS (jumps, sliders, gateways) - for when
you already know the trip's physics breakdown (e.g. from tools/route.py, or copying a known
route). If you just want "ship X from A to B, what's it cost", use tools/shiptrip.py instead -
it works all of this out for you. SCOPED TO the account's one hull/reactor (blueprint
BP-STRT-0000, stock reactor), shared by both ships (AVI-07JFB, AVI-07JFC). See puga/shipping.py's
module docstring for exactly what's confirmed vs still open (generalization to a different
hull/reactor is untested; gravity vs distance on landing fuel is confounded; v0's meaning is
relative to current fuel load, not an absolute unit).
tools/shipcost.py --v0 0.5 --v1 0.65 --jumps 1 --to ZV-759c # one direct jump to Deimos
tools/shipcost.py --v0 1.0 --jumps 2 --charges 1 --to ZV-194a # two-jump route to Nike
tools/shipcost.py --v0 1.0 --gateways 3 --lock-fee 6000:AIC --lock-fee 6000:AIC --lock-fee 4000:CIS # Benten-style - fees read off the live APEX LOCK segment, not formulaic/lookupable
"""
import argparse, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from puga import config, market, shipping
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--v0", type=float, default=1.0, help="fuel-usage slider, 0..1 (relative to current STL tank level, not absolute)")
ap.add_argument("--v1", type=float, default=1.0, help="reactor-usage slider, 0..1")
ap.add_argument("--jumps", type=int, default=0, help="number of direct FTL jumps (JMP segments)")
ap.add_argument("--charges", type=int, default=0, help="number of FTL recharges (CHRG) between consecutive jumps")
ap.add_argument("--cruise-legs", type=int, default=2, help="number of STL cruise legs (DEP+APP=2 for a simple direct route)")
ap.add_argument("--gateways", type=int, default=0, help="number of gateway hops (each: free GTW + a LOCK fee you supply)")
ap.add_argument("--lock-fee", action="append", default=[], help="AMOUNT:CURRENCY per gateway hop, repeatable, matched in order to --gateways - read the fee off the live APEX LOCK segment, there's no API/formula for it")
ap.add_argument("--to", help="destination planet natural id, for a looked-up (not predicted) landing fuel cost")
ap.add_argument("--cx", default=config.DEFAULT_CX)
a = ap.parse_args()
if a.gateways and len(a.lock_fee) != a.gateways:
print(f"warning: {a.gateways} gateways but {len(a.lock_fee)} --lock-fee given - fees you didn't supply are unknown, not zero", file=sys.stderr)
lock_fees = [(float(amt), cur) for amt, cur in (lf.split(":") for lf in a.lock_fee)]
result = shipping.trip_fuel(a.v0, a.v1, a.jumps, a.charges, a.cruise_legs, a.to, lock_fees)
if a.to and result["landing_fuel_included"] is None:
print(f"note: landing fuel for {a.to} hasn't been measured - not included in total, see puga/shipping.py LANDING_FUEL", file=sys.stderr)
snap = market.snapshot()
sf_px = (snap.get(("SF", a.cx)) or market.Quote("SF", a.cx)).ask or 0
ff_px = (snap.get(("FF", a.cx)) or market.Quote("FF", a.cx)).ask or 0
stl, ftl, fees = result["stl_fuel"], result["ftl_fuel"], result["lock_fees"]
lnd = result["landing_fuel_included"]
print(f"jumps {a.jumps} charges {a.charges} cruise legs {a.cruise_legs}" + (f" landing +{lnd:.0f} STL" if lnd else ""))
print(f"STL fuel: {stl:.1f} units -> {stl*sf_px:,.0f} AIC")
print(f"FTL fuel: {ftl:.1f} units -> {ftl*ff_px:,.0f} AIC")
if fees:
print("Gateway LOCK fees: " + ", ".join(f"{amt:,.0f} {cur}" for amt, cur in fees))
total_aic_fuel = stl * sf_px + ftl * ff_px
aic_fees = sum(amt for amt, cur in fees if cur == "AIC")
print(f"\nTotal (fuel, AIC-denominated fees only): {total_aic_fuel + aic_fees:,.0f} AIC" + (" + non-AIC fees above" if any(cur != "AIC" for _, cur in fees) else ""))
if __name__ == "__main__":
main()
+83
View File
@@ -0,0 +1,83 @@
#!/usr/bin/env python3
"""Simplest possible ship-trip cost: give it a ship, a start and a stop, done - it works out
jumps/charges/sliders/landing fuel itself. For when you don't know (and shouldn't need to know)
the physics breakdown - if you already have jump counts/slider values/gateway fees in hand, use
the lower-level tools/shipcost.py instead.
tools/shiptrip.py AVI-07JFB ZV-307 ZV-759c # Antares -> Deimos
tools/shiptrip.py AVI-07JFC "Antares Station" Nike # names/planets/systems all work
Defaults to max sliders (v0=v1=1.0, fastest) since that's the common case; pass --v0/--v1 to
model a slower/cheaper trip. Covers direct-jump routes (puga/route.py's FTL jump graph) and the
handful of gateway chains with a measured LOCK fee (puga.shipping.KNOWN_GATEWAY_ROUTES) - for
any other gateway route, the LOCK fee genuinely cannot be looked up (no FIO/PRUNplanner endpoint
has it), and this tool tells you exactly where to read it live and what to run afterward.
"""
import argparse, math, sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
from puga import config, fio, market, shipping
def check_ship(reg: str):
"""Warn (don't fail) if the given ship's live specs don't match what this model is
calibrated against - see shipping.CALIBRATED_SPEC."""
try:
ships = fio.own(f"/ship/ships/{fio.me()}")
except Exception:
return None
ship = next((s for s in ships if s.get("Registration") == reg), None)
if ship is None:
print(f"warning: {reg!r} isn't a ship on this account (known: "
+ ", ".join(s.get("Registration", "?") for s in ships) + ") - proceeding anyway", file=sys.stderr)
return None
spec = shipping.CALIBRATED_SPEC
def differs(k, v):
return v != spec[k] if isinstance(spec[k], str) else not math.isclose(v or 0, spec[k], rel_tol=1e-6)
mismatches = [k for k, v in {"blueprint": ship.get("BlueprintNaturalId"), "acceleration": ship.get("Acceleration"),
"thrust": ship.get("Thrust"), "reactor_power": ship.get("ReactorPower"),
"emitter_power": ship.get("EmitterPower")}.items()
if differs(k, v)]
if mismatches:
print(f"warning: {reg} differs from the calibrated hull/reactor on {', '.join(mismatches)} - "
f"this model (puga/shipping.py) has NOT been validated for this ship, numbers may be wrong", file=sys.stderr)
return ship
def main():
ap = argparse.ArgumentParser()
ap.add_argument("ship", help="ship registration, e.g. AVI-07JFB")
ap.add_argument("src", help="start: planet natural id, system natural id, or name")
ap.add_argument("dst", help="stop: planet natural id, system natural id, or name")
ap.add_argument("--v0", type=float, default=1.0, help="fuel-usage slider, 0..1 (default 1.0 = fastest)")
ap.add_argument("--v1", type=float, default=1.0, help="reactor-usage slider, 0..1 (default 1.0 = fastest)")
ap.add_argument("--cx", default=config.DEFAULT_CX)
a = ap.parse_args()
check_ship(a.ship)
snap = market.snapshot()
sf_px = (snap.get(("SF", a.cx)) or market.Quote("SF", a.cx)).ask or 0
ff_px = (snap.get(("FF", a.cx)) or market.Quote("FF", a.cx)).ask or 0
result = shipping.leg_cost(a.src, a.dst, sf_px, ff_px, a.v0, a.v1)
if result is None:
sys.exit(f"no priceable route between {a.src} and {a.dst} - not a direct-jump route, and not a measured "
f"gateway chain (puga.shipping.KNOWN_GATEWAY_ROUTES). The LOCK fee can't be looked up (no "
f"FIO/PRUNplanner endpoint has it) - open the Ship Flight Control screen in APEX for this exact "
f"route, read each LOCK segment's fee, then run:\n"
f" tools/shipcost.py --v0 {a.v0} --gateways N --lock-fee AMOUNT:CURRENCY [--lock-fee ...]")
lnd = result["landing_fuel_included"]
print(f"{a.src} -> {a.dst}" + (" (landing fuel not measured for this destination - not included)" if lnd is None else ""))
print(f"STL fuel: {result['stl_fuel']:.1f} units -> {result['stl_fuel']*sf_px:,.0f} AIC")
print(f"FTL fuel: {result['ftl_fuel']:.1f} units -> {result['ftl_fuel']*ff_px:,.0f} AIC")
if result["non_aic_fees"]:
print("Gateway LOCK fees (non-AIC): " + ", ".join(f"{amt:,.0f} {cur}" for amt, cur in result["non_aic_fees"]))
print(f"\nTotal (AIC): {result['aic']:,.0f}" + (" + non-AIC fees above" if result["non_aic_fees"] else ""))
if __name__ == "__main__":
main()