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>
211 lines
13 KiB
Python
211 lines
13 KiB
Python
"""Ship flight fuel/cost model - STL cruise, FTL jumps/charges, gateway hops.
|
|
|
|
SCOPED TO ONE HULL/REACTOR: calibrated on AVI-07JFB (Freighter, blueprint BP-STRT-0000, stock
|
|
reactor). The account has two ships - AVI-07JFB and AVI-07JFC - but both are the SAME blueprint
|
|
with identical Acceleration/Thrust/ReactorPower/EmitterPower/OperatingEmptyMass (confirmed via
|
|
FIO `/ship/ships/{user}`), so this model should apply to both; only their current cargo mass
|
|
differs, and mass is already confirmed not to affect fuel/time (see below). Coefficients below
|
|
are NOT confirmed to generalize to any OTHER hull or reactor module - there isn't one on this
|
|
account to test against yet. Do not trust the magnitudes for a different hull/reactor without
|
|
recalibrating (see "OPEN GAPS" below). Coefficients are fit from a controlled 4x4-grid sweep
|
|
(fuel-usage x reactor-usage sliders) across 5 real destinations (Deimos, Nike, Vulcan, and
|
|
gateway-routed Benten/Hortus), with exact values read from the live APEX Ship Flight Control DOM
|
|
(never screenshots/estimates). Raw data: empire/data/shipping_calibration/.
|
|
|
|
CONFIRMED (high confidence, R^2 close to 1 on 4-point linear fits, cross-checked on 5 routes):
|
|
- Fuel-usage slider (v0, 0..1 - NOT an absolute unit, see OPEN GAPS) and reactor-usage slider
|
|
(v1, 0..1) are fully INDEPENDENT/additive - zero cross-term on any segment, confirmed on full
|
|
4x4 grids. STL fuel never changes across different v1 at fixed v0, and vice versa for FTL.
|
|
- STL cruise fuel (DEP/APP/long-TRA segments) = STL_INTERCEPT + STL_SLOPE * v0. Slope ~240
|
|
units/unit-v0, essentially identical for DEP and APP and for long in-system TRA legs on
|
|
gateway routes. Intercept is small (~1-8 units) and segment-specific.
|
|
- FTL jump fuel = JUMP_FTL_SLOPE * v1 (no intercept). FTL recharge (CHRG, needed between two
|
|
consecutive jumps with no landing in between) = CHARGE_FTL_SLOPE * v1 (no intercept, lower
|
|
slope than a jump - recharging is cheaper than jumping).
|
|
- LND (final landing) fuel is a CONSTANT per destination, independent of BOTH sliders -
|
|
confirmed across full 4x4 grids on 3 planets (zero variation). This is NOT a formula, it's a
|
|
per-planet constant that must be measured - see LANDING_FUEL below and its gravity/distance
|
|
caveat.
|
|
- Gateway (GTW) hops cost ZERO fuel (STL and FTL) - confirmed by direct inspection of raw
|
|
segment rows on 2 different gateway-routed destinations (Benten: 3 gateways, Hortus: 2
|
|
gateways). Cost is a fixed LOCK fee instead (AIC or CIS, varies per specific gateway, not
|
|
predictable by formula - read it off the real route) plus a small fixed 5-unit STL cost for
|
|
the LOCK maneuver itself. DCAY and short inter-gateway TRA hops are small fixed STL amounts
|
|
(5-15 units) independent of both sliders. The reactor-usage slider is INERT on pure-gateway
|
|
routes (no direct JMP segment) - confirmed its full allowed range changes nothing.
|
|
|
|
OPEN GAPS - do not assume these are resolved:
|
|
1. v0's absolute meaning: at v0=1, STL fuel always came out to exactly the ship's then-current
|
|
STL tank level (525-526, matching a tank at 526/1500 at calibration time) regardless of
|
|
route distance - v0 looks like "fraction of CURRENTLY BUNKERED fuel to spend on this leg",
|
|
not an absolute physical speed/fuel unit. This model's v0->fuel formula is calibrated
|
|
against THAT fuel load; if the ship's tank level is very different, recalibrate rather than
|
|
trust the magnitudes.
|
|
2. DEP's departure-side gravity dependence is UNTESTED - every DEP segment in the calibration
|
|
sweep departed from Antares Station (same origin, same gravity every time). A hypothesized
|
|
"gravity-well-escape" term on departure has neither been confirmed nor refuted.
|
|
3. LND fuel vs gravity vs distance is CONFOUNDED, not resolved - Deimos (gravity 1.032) and
|
|
Vulcan (gravity 1.238) have near-identical landing distance (4,243 vs 4,441 km) and IDENTICAL
|
|
landing fuel (38); Nike (gravity 0.624, lower) has a much longer landing distance (7,034 km)
|
|
and higher fuel (48). This points at distance mattering more than gravity, but n=3 isn't
|
|
enough for a real coefficient, and gravity/distance aren't varied independently in the
|
|
sample. Don't predict LND fuel for an unmeasured planet - look it up for real.
|
|
4. Hull/reactor generalization beyond this one blueprint (BP-STRT-0000, stock reactor) is
|
|
COMPLETELY UNTESTED - both ships on this account (AVI-07JFB, AVI-07JFC) share it, so there's
|
|
nothing else to cross-check against yet. The reactor-usage slider's own allowed range was
|
|
only 0.303-1 even on direct-jump routes, suggesting the stock reactor is itself constrained -
|
|
a different reactor module would need its own sweep, not an assumption that these
|
|
slopes/intercepts carry over.
|
|
5. KNOWN_GATEWAY_ROUTES assumes a gateway chain is usable and equally priced in both directions.
|
|
Per the official handbook (handbook.apex.prosperousuniverse.com/wiki/infrastructure-gateway),
|
|
some gateway links are ONE-WAY (map arrows show the permitted direction) - the reverse leg of
|
|
a known route may not exist at all, not just cost something different. round_trip_cost()
|
|
and leg_cost() have no way to detect this; don't trust a return leg on a gateway route until
|
|
it's separately confirmed live.
|
|
6. Each gateway has its own maximum per-jump VOLUME (m3) cap, per the handbook, independent of
|
|
ship cargo capacity - not measured or modelled anywhere here. This matters for any "use a
|
|
bigger ship to bundle more cargo through one gateway trip" reasoning (see tools/lmscan.py
|
|
analysis) - a bigger hold doesn't help if the gateway's own cap is lower.
|
|
7. TRIP TIME (not just fuel) is not modelled anywhere in this file. For what we do know about
|
|
GTW segment duration (scales ~linearly with parsec distance at ~20 min/parsec, contradicting
|
|
a pre-launch dev statement about a possibly-fixed duration), see docs/mechanics.md's
|
|
"Gateway (GTW) jump transit time" section - built from the same raw data this module uses.
|
|
"""
|
|
|
|
# Calibrated on AVI-07JFB, 2026-10-09. See module docstring for what's confirmed vs open.
|
|
STL_SLOPE = 240.0 # units per unit of v0 (fuel-usage slider), DEP/APP/long-TRA alike
|
|
STL_INTERCEPT = 3.0 # small, roughly the DEP/APP average; noise-level, not precise
|
|
JUMP_FTL_SLOPE = 28.4 # units per unit of v1 (reactor-usage slider), per direct JMP
|
|
CHARGE_FTL_SLOPE = 22.8 # units per unit of v1, per CHRG (recharge between consecutive jumps)
|
|
|
|
# Fixed, slider-independent costs on gateway-routed segments (confirmed on Benten/Hortus):
|
|
GATEWAY_HOP_FIXED_STL = 5.0 # the LOCK maneuver itself
|
|
SHORT_TRA_FIXED_STL = 15.0 # the ~10s hop immediately around a gateway
|
|
DCAY_FIXED_STL = 5.0 # post-gateway decay step
|
|
|
|
# The hull/reactor this file is calibrated against (both AVI-07JFB and AVI-07JFC match this via
|
|
# FIO /ship/ships/{user} - confirmed 2026-10-09). tools/shiptrip.py checks a given ship's live
|
|
# specs against this and warns if they don't match (a third, different ship would invalidate
|
|
# the coefficients above - see OPEN GAPS #4).
|
|
CALIBRATED_SPEC = {"blueprint": "BP-STRT-0000", "acceleration": 78.4800033569336, "thrust": 125000.0,
|
|
"reactor_power": 2400.0, "emitter_power": 725.0}
|
|
|
|
# LOOKUP, not a formula (see OPEN GAPS #3) - only destinations we've actually measured.
|
|
# {planet_natural_id: (landing_fuel_STL, landing_distance_km, gravity)}
|
|
LANDING_FUEL = {
|
|
"ZV-759c": (38, 4243, 1.032), # Deimos
|
|
"ZV-759b": (38, 4441, 1.238), # Vulcan
|
|
"ZV-194a": (48, 7034, 0.624), # Nike
|
|
}
|
|
|
|
|
|
def cruise_leg_fuel(v0: float) -> float:
|
|
"""One STL cruise leg (DEP, APP, or a long in-system TRA on a gateway route)."""
|
|
return STL_INTERCEPT + STL_SLOPE * v0
|
|
|
|
|
|
def jump_fuel(v1: float) -> float:
|
|
"""One direct FTL jump (JMP segment, not a gateway hop)."""
|
|
return JUMP_FTL_SLOPE * v1
|
|
|
|
|
|
def charge_fuel(v1: float) -> float:
|
|
"""One FTL recharge (CHRG) between two consecutive jumps with no landing between them."""
|
|
return CHARGE_FTL_SLOPE * v1
|
|
|
|
|
|
def gateway_hop_cost(lock_fee_amount: float, lock_fee_currency: str):
|
|
"""One gateway hop (LOCK + GTW + DCAY + the short TRA around it): the GTW segment itself
|
|
is free (confirmed), but LOCK charges a fixed real-money fee you must supply (not
|
|
predictable - read it off the actual route, it varies per specific gateway pair)."""
|
|
stl = GATEWAY_HOP_FIXED_STL + SHORT_TRA_FIXED_STL + DCAY_FIXED_STL
|
|
return {"stl_fuel": stl, "fee_amount": lock_fee_amount, "fee_currency": lock_fee_currency}
|
|
|
|
|
|
def landing_fuel(planet_natural_id: str):
|
|
"""Looked-up, not predicted - returns None if we haven't actually measured this planet.
|
|
See OPEN GAPS #3: don't extrapolate this to an unmeasured planet."""
|
|
row = LANDING_FUEL.get(planet_natural_id)
|
|
return row[0] if row else None
|
|
|
|
|
|
# Specific gateway-chain routes we've actually measured, keyed by frozenset so the SAME entry
|
|
# is used for either direction - NOT confirmed to be valid: the handbook says some gateway links
|
|
# are one-way, so the reverse leg may not exist or may cost differently (see OPEN GAPS #5).
|
|
# {frozenset({a, b}): [(fee_amount, fee_currency), ...]}
|
|
KNOWN_GATEWAY_ROUTES = {
|
|
frozenset({"ANT", "HRT"}): [(6000, "AIC"), (6000, "AIC")],
|
|
frozenset({"ANT", "BEN"}): [(6000, "AIC"), (6000, "AIC"), (4000, "CIS")],
|
|
}
|
|
|
|
|
|
def cx_system(cx: str) -> str | None:
|
|
"""CX code (AI1, CI1, ...) -> the natural id of its physical station, from FIO's own
|
|
/exchange/station (ComexCode). Feeds leg_cost/round_trip_cost's src argument when the origin
|
|
is "the home exchange" rather than a specific planet. None if the code isn't a real station."""
|
|
from . import fio
|
|
row = next((s for s in fio.stations() if s.get("ComexCode") == cx), None)
|
|
return row["NaturalId"] if row else None
|
|
|
|
|
|
def leg_cost(src: str, dst: str, sf_price: float, ff_price: float, v0: float = 1.0, v1: float = 1.0):
|
|
"""One-way fuel (+ gateway fee) AIC cost between two places (station/planet natural ids).
|
|
Prefers a measured gateway chain (KNOWN_GATEWAY_ROUTES) when this exact pair has one;
|
|
otherwise tries the direct-jump graph (puga.route). Returns None if neither applies - an
|
|
unmeasured gateway route, where the LOCK fee can't be priced at all. Non-AIC gateway fees
|
|
(e.g. CIS) are reported separately, not folded into `aic`."""
|
|
from . import route
|
|
key = frozenset({src, dst})
|
|
if key in KNOWN_GATEWAY_ROUTES:
|
|
result = trip_fuel(v0, v1, jumps=0, charges=0, cruise_legs=2, dest_planet_natural_id=dst,
|
|
gateway_lock_fees=KNOWN_GATEWAY_ROUTES[key])
|
|
else:
|
|
try:
|
|
src_sys, dst_sys = route.resolve_system(src), route.resolve_system(dst)
|
|
except ValueError:
|
|
return None
|
|
if src_sys == dst_sys:
|
|
jumps = 0
|
|
else:
|
|
r = route.shortest_path(src_sys, dst_sys)
|
|
if r is None:
|
|
return None
|
|
jumps = r["jumps"]
|
|
result = trip_fuel(v0, v1, jumps, max(jumps - 1, 0), cruise_legs=2, dest_planet_natural_id=dst)
|
|
aic = result["stl_fuel"] * sf_price + result["ftl_fuel"] * ff_price
|
|
aic += sum(amt for amt, cur in result["lock_fees"] if cur == "AIC")
|
|
non_aic_fees = [(amt, cur) for amt, cur in result["lock_fees"] if cur != "AIC"]
|
|
return {"aic": aic, "non_aic_fees": non_aic_fees, "stl_fuel": result["stl_fuel"], "ftl_fuel": result["ftl_fuel"],
|
|
"landing_fuel_included": result["landing_fuel_included"]}
|
|
|
|
|
|
def round_trip_cost(src: str, dst: str, sf_price: float, ff_price: float, v0: float = 1.0, v1: float = 1.0):
|
|
"""AIC cost of a round trip (there and back) between two places - the sum of two `leg_cost`
|
|
calls, there and back. Returns None if either leg can't be priced (an unmeasured gateway
|
|
route) or either leg carries a non-AIC fee (no AIC exchange rate assumed here - see
|
|
`leg_cost`'s `non_aic_fees`, which this collapses rather than silently drop)."""
|
|
there, back = leg_cost(src, dst, sf_price, ff_price, v0, v1), leg_cost(dst, src, sf_price, ff_price, v0, v1)
|
|
if there is None or back is None or there["non_aic_fees"] or back["non_aic_fees"]:
|
|
return None
|
|
return there["aic"] + back["aic"]
|
|
|
|
|
|
def trip_fuel(v0: float, v1: float, jumps: int, charges: int, cruise_legs: int = 2,
|
|
dest_planet_natural_id: str | None = None, gateway_lock_fees: list[tuple[float, str]] = ()):
|
|
"""Total STL/FTL fuel (plus any gateway LOCK fees passed straight through) for one trip.
|
|
This is the one place the cost arithmetic lives - tools/shipcost.py (manual/explicit
|
|
params) and tools/shiptrip.py (--ship/--from/--to, auto-derives jumps/charges/landing)
|
|
both call this instead of duplicating it. Returns {stl_fuel, ftl_fuel, lock_fees:
|
|
[(amount, currency), ...], landing_fuel_included: bool}."""
|
|
stl = cruise_legs * cruise_leg_fuel(v0)
|
|
ftl = jumps * jump_fuel(v1) + charges * charge_fuel(v1)
|
|
fees = []
|
|
for amount, currency in gateway_lock_fees:
|
|
g = gateway_hop_cost(amount, currency)
|
|
stl += g["stl_fuel"]
|
|
fees.append((g["fee_amount"], g["fee_currency"]))
|
|
lnd = landing_fuel(dest_planet_natural_id) if dest_planet_natural_id else None
|
|
if lnd is not None:
|
|
stl += lnd
|
|
return {"stl_fuel": stl, "ftl_fuel": ftl, "lock_fees": fees, "landing_fuel_included": lnd}
|