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:
@@ -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()
|
||||
Reference in New Issue
Block a user