derive cargo_capacity from the actual optimized build, not the declared-floor sum

cargo_capacity/cargo_capacity_kg were computed from the SUM of each
entity's own declared minimum mass (the same floor pass 1 checks for
legality) -- completely disconnected from the platform/actuator/storage
masses _decide_masses actually optimizes and every other metric
(power_density, speed, range_fuel, cost_efficiency) already uses. Two
builds of the same combo with wildly different actual masses scored
identically on cargo capacity, and the explore sliders had no effect on
it at all.

Moved the cargo_capacity/cargo_capacity_kg calculation into
_raw_physics_from_masses, deriving it from floor_total (the real
assembled mass) the same deadweight/lightship-ratio way as before, just
against the right mass. Removed the cargo_capacity_kg parameter that
threaded a precomputed constant through _decide_masses and
_raw_physics_from_masses -- it's now computed fresh at each point the
optimizer/explore sliders try, exactly like power_density/speed already
are. cost_efficiency's $/(kg·m) division now uses whichever cargo
convention the domain actually scores (cargo_capacity's 2.5x ratio or
cargo_capacity_kg's 0.3x ratio) instead of always assuming the former,
so the cost-per-cargo-kg number and the cargo capacity shown alongside
it always agree.

Confirmed on combo #876: cargo_capacity_kg moved from 5.7kg (declared
floor sum: 5+5+9=19kg * 0.3) to 18kg (actual 60kg build * 0.3), and now
responds to the explore sliders (9.4kg-35.2kg across actuator/storage
choices) the way every other metric already did.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 20:20:45 -05:00
parent 3ed3918964
commit f786f3da79

View File

@@ -925,13 +925,19 @@ class Pipeline:
storage_mass: float, storage_mass: float,
bounds_by_name: dict[str, MetricBound], bounds_by_name: dict[str, MetricBound],
units_by_name: dict[str, str], units_by_name: dict[str, str],
cargo_capacity_kg: float,
platform_mass: float | None = None, platform_mass: float | None = None,
) -> dict[str, float]: ) -> dict[str, float]:
"""power_density/range_fuel/cost_efficiency for an EXPLICIT mass """power_density/range_fuel/cost_efficiency/cargo_capacity for an
allocation. `platform_mass` defaults to the platform's EXPLICIT mass allocation. `platform_mass` defaults to the
representative mass (ctx.p_rep) -- pass an explicit value to platform's representative mass (ctx.p_rep) -- pass an explicit
explore a specific weight class instead (see evaluate_allocation).""" value to explore a specific weight class instead (see
evaluate_allocation). Cargo capacity is derived from THIS build's
actual assembled mass (floor_total below), not a separate
declared-floor constant -- a deadweight/lightship-style ratio of
"how much this vehicle could additionally carry, proportional to
its own (empty of cargo) mass" only makes sense against the mass
it's actually built to, so it responds to the same optimizer/
explore-slider choices every other metric here does."""
p_mass = ctx.p_rep if platform_mass is None else platform_mass p_mass = ctx.p_rep if platform_mass is None else platform_mass
out: dict[str, float] = {} out: dict[str, float] = {}
floor_total = p_mass + actuator_mass + storage_mass floor_total = p_mass + actuator_mass + storage_mass
@@ -940,6 +946,19 @@ class Pipeline:
if "power_density" in bounds_by_name: if "power_density" in bounds_by_name:
out["power_density"] = power_density_value out["power_density"] = power_density_value
# Deadweight/lightship-style cargo capacity, proportional to this
# build's own actual mass -- two conventions coexist because heavy
# freight/maritime vehicles genuinely carry a much larger multiple
# of their own mass in cargo than light personal/delivery vehicles
# do (see CARGO_KG_PER_STRUCTURAL_KG's module comment); which one
# a domain scores is just which metric_name it declares.
cargo_capacity_2_5x = floor_total * CARGO_KG_PER_STRUCTURAL_KG
cargo_capacity_0_3x = floor_total * 0.3
if "cargo_capacity" in bounds_by_name:
out["cargo_capacity"] = cargo_capacity_2_5x
if "cargo_capacity_kg" in bounds_by_name:
out["cargo_capacity_kg"] = cargo_capacity_0_3x
# Achieved steady-state cruise speed, DERIVED from this specific # Achieved steady-state cruise speed, DERIVED from this specific
# build's actual power_density, the medium's mass-proportional # build's actual power_density, the medium's mass-proportional
# resistance, and (ground only, see DRAG_POWER_COEFF_BY_MEDIUM) a # resistance, and (ground only, see DRAG_POWER_COEFF_BY_MEDIUM) a
@@ -994,7 +1013,13 @@ class Pipeline:
cost_per_m = amortized_per_m + operating_per_m cost_per_m = amortized_per_m + operating_per_m
if units_by_name.get("cost_efficiency") == "$/(kg·m)": if units_by_name.get("cost_efficiency") == "$/(kg·m)":
out["cost_efficiency"] = cost_per_m / max(cargo_capacity_kg, 1.0) # Divide by whichever cargo convention this domain actually
# scores, so cost-per-cargo-kg and the cargo_capacity number
# shown alongside it always agree; default to the heavy-
# vehicle ratio if a domain scores $/(kg·m) without scoring
# either cargo metric explicitly (matches prior behavior).
cargo_basis = out.get("cargo_capacity_kg", out.get("cargo_capacity", cargo_capacity_2_5x))
out["cost_efficiency"] = cost_per_m / max(cargo_basis, 1.0)
else: else:
out["cost_efficiency"] = cost_per_m out["cost_efficiency"] = cost_per_m
@@ -1005,7 +1030,6 @@ class Pipeline:
ctx: "_PhysicsContext", ctx: "_PhysicsContext",
bounds_by_name: dict[str, MetricBound], bounds_by_name: dict[str, MetricBound],
units_by_name: dict[str, str], units_by_name: dict[str, str],
cargo_capacity_kg: float,
) -> tuple[float, float, float, bool]: ) -> tuple[float, float, float, bool]:
"""Pick the platform/actuator/storage mass for the build this domain """Pick the platform/actuator/storage mass for the build this domain
actually scores. First, the platform's declared physical actually scores. First, the platform's declared physical
@@ -1142,7 +1166,7 @@ class Pipeline:
def objective(platform_mass: float, actuator_mass: float, storage_mass: float) -> float: def objective(platform_mass: float, actuator_mass: float, storage_mass: float) -> float:
raw = self._raw_physics_from_masses( raw = self._raw_physics_from_masses(
ctx, actuator_mass, storage_mass, ctx, actuator_mass, storage_mass,
bounds_by_name, units_by_name, cargo_capacity_kg, bounds_by_name, units_by_name,
platform_mass=platform_mass, platform_mass=platform_mass,
) )
scores, weights = [], [] scores, weights = [], []
@@ -1285,7 +1309,6 @@ class Pipeline:
# drives the untouched blocks below). # drives the untouched blocks below).
power_density = 0.0 # W/kg power_density = 0.0 # W/kg
energy_density = 0.0 # J/kg energy_density = 0.0 # J/kg
mass_total = 0.0 # kg, extensive — components share one vehicle
thrust_profile: str | None = None thrust_profile: str | None = None
energy_form: str | None = None energy_form: str | None = None
infra_matches: list[float] = [] infra_matches: list[float] = []
@@ -1295,8 +1318,6 @@ class Pipeline:
power_density = max(power_density, float(dep.value)) power_density = max(power_density, float(dep.value))
if dep.key == "energy_density" and dep.constraint_type == "provides": if dep.key == "energy_density" and dep.constraint_type == "provides":
energy_density = max(energy_density, float(dep.value)) energy_density = max(energy_density, float(dep.value))
if dep.key == "mass" and dep.constraint_type == "range_min":
mass_total += float(dep.value)
if dep.key == "thrust_profile" and dep.constraint_type == "provides": if dep.key == "thrust_profile" and dep.constraint_type == "provides":
thrust_profile = dep.value thrust_profile = dep.value
if dep.key == "energy_form" and dep.constraint_type == "requires": if dep.key == "energy_form" and dep.constraint_type == "requires":
@@ -1305,20 +1326,19 @@ class Pipeline:
match = INFRASTRUCTURE_AVAILABILITY.get((dep.key, dep.value)) match = INFRASTRUCTURE_AVAILABILITY.get((dep.key, dep.value))
if match is not None: if match is not None:
infra_matches.append(match) infra_matches.append(match)
mass = mass_total if mass_total > 0 else 100.0 # kg, default if undeclared
cargo_capacity_kg = mass * CARGO_KG_PER_STRUCTURAL_KG
# ── platform/actuator/storage-specific extraction, for # ── platform/actuator/storage-specific extraction, for
# power_density / range_fuel / cost_efficiency only ────────────── # power_density / range_fuel / cost_efficiency / cargo_capacity
# only ─────────────────────────────────────────────────────────
ctx = self._physics_context(combo, bounds_by_name) ctx = self._physics_context(combo, bounds_by_name)
feasible = True feasible = True
if ctx is not None: if ctx is not None:
actuator_mass, storage_mass, platform_mass, feasible = self._decide_masses( actuator_mass, storage_mass, platform_mass, feasible = self._decide_masses(
ctx, bounds_by_name, units_by_name, cargo_capacity_kg ctx, bounds_by_name, units_by_name
) )
raw.update(self._raw_physics_from_masses( raw.update(self._raw_physics_from_masses(
ctx, actuator_mass, storage_mass, ctx, actuator_mass, storage_mass,
bounds_by_name, units_by_name, cargo_capacity_kg, bounds_by_name, units_by_name,
platform_mass=platform_mass, platform_mass=platform_mass,
)) ))
@@ -1340,11 +1360,8 @@ class Pipeline:
if "range_degradation" in raw: if "range_degradation" in raw:
raw["range_degradation"] = 365 * 86400 raw["range_degradation"] = 365 * 86400
if "cargo_capacity" in raw: # cargo_capacity / cargo_capacity_kg are set by _raw_physics_from_masses
raw["cargo_capacity"] = cargo_capacity_kg # above, from the actual build mass -- not recomputed here.
if "cargo_capacity_kg" in raw:
raw["cargo_capacity_kg"] = mass * 0.3
if "environmental_impact" in raw: if "environmental_impact" in raw:
raw["environmental_impact"] = max(0.0, power_density * 2e-7) raw["environmental_impact"] = max(0.0, power_density * 2e-7)
@@ -1391,9 +1408,8 @@ class Pipeline:
if ctx is None or ctx.p_max is None: if ctx is None or ctx.p_max is None:
return None return None
cargo_capacity_kg = (ctx.p_min + ctx.a_min + ctx.s_min) * CARGO_KG_PER_STRUCTURAL_KG
default_actuator, default_storage, default_platform, _feasible = self._decide_masses( default_actuator, default_storage, default_platform, _feasible = self._decide_masses(
ctx, bounds_by_name, units_by_name, cargo_capacity_kg ctx, bounds_by_name, units_by_name
) )
p_mass = default_platform if platform_mass is None else platform_mass p_mass = default_platform if platform_mass is None else platform_mass
a_mass = default_actuator if actuator_mass is None else actuator_mass a_mass = default_actuator if actuator_mass is None else actuator_mass
@@ -1405,7 +1421,7 @@ class Pipeline:
raw = self._raw_physics_from_masses( raw = self._raw_physics_from_masses(
ctx, a_mass, s_mass, ctx, a_mass, s_mass,
bounds_by_name, units_by_name, cargo_capacity_kg, bounds_by_name, units_by_name,
platform_mass=p_mass, platform_mass=p_mass,
) )