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
+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()