"""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}