From f786f3da7907f783fa66f9646f68d642d7d4adf5 Mon Sep 17 00:00:00 2001 From: Andrew Simonson Date: Sat, 15 Aug 2026 20:20:45 -0500 Subject: [PATCH] derive cargo_capacity from the actual optimized build, not the declared-floor sum MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/physcom/engine/pipeline.py | 64 +++++++++++++++++++++------------- 1 file changed, 40 insertions(+), 24 deletions(-) diff --git a/src/physcom/engine/pipeline.py b/src/physcom/engine/pipeline.py index e01b455..bd3ff9f 100644 --- a/src/physcom/engine/pipeline.py +++ b/src/physcom/engine/pipeline.py @@ -925,13 +925,19 @@ class Pipeline: storage_mass: float, bounds_by_name: dict[str, MetricBound], units_by_name: dict[str, str], - cargo_capacity_kg: float, platform_mass: float | None = None, ) -> dict[str, float]: - """power_density/range_fuel/cost_efficiency for an EXPLICIT mass - allocation. `platform_mass` defaults to the platform's - representative mass (ctx.p_rep) -- pass an explicit value to - explore a specific weight class instead (see evaluate_allocation).""" + """power_density/range_fuel/cost_efficiency/cargo_capacity for an + EXPLICIT mass allocation. `platform_mass` defaults to the + platform's representative mass (ctx.p_rep) -- pass an explicit + 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 out: dict[str, float] = {} floor_total = p_mass + actuator_mass + storage_mass @@ -940,6 +946,19 @@ class Pipeline: if "power_density" in bounds_by_name: 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 # build's actual power_density, the medium's mass-proportional # 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 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: out["cost_efficiency"] = cost_per_m @@ -1005,7 +1030,6 @@ class Pipeline: ctx: "_PhysicsContext", bounds_by_name: dict[str, MetricBound], units_by_name: dict[str, str], - cargo_capacity_kg: float, ) -> tuple[float, float, float, bool]: """Pick the platform/actuator/storage mass for the build this domain 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: raw = self._raw_physics_from_masses( ctx, actuator_mass, storage_mass, - bounds_by_name, units_by_name, cargo_capacity_kg, + bounds_by_name, units_by_name, platform_mass=platform_mass, ) scores, weights = [], [] @@ -1285,7 +1309,6 @@ class Pipeline: # drives the untouched blocks below). power_density = 0.0 # W/kg energy_density = 0.0 # J/kg - mass_total = 0.0 # kg, extensive — components share one vehicle thrust_profile: str | None = None energy_form: str | None = None infra_matches: list[float] = [] @@ -1295,8 +1318,6 @@ class Pipeline: power_density = max(power_density, float(dep.value)) if dep.key == "energy_density" and dep.constraint_type == "provides": 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": thrust_profile = dep.value if dep.key == "energy_form" and dep.constraint_type == "requires": @@ -1305,20 +1326,19 @@ class Pipeline: match = INFRASTRUCTURE_AVAILABILITY.get((dep.key, dep.value)) if match is not None: 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 - # power_density / range_fuel / cost_efficiency only ────────────── + # power_density / range_fuel / cost_efficiency / cargo_capacity + # only ───────────────────────────────────────────────────────── ctx = self._physics_context(combo, bounds_by_name) feasible = True if ctx is not None: 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( ctx, actuator_mass, storage_mass, - bounds_by_name, units_by_name, cargo_capacity_kg, + bounds_by_name, units_by_name, platform_mass=platform_mass, )) @@ -1340,11 +1360,8 @@ class Pipeline: if "range_degradation" in raw: raw["range_degradation"] = 365 * 86400 - if "cargo_capacity" in raw: - raw["cargo_capacity"] = cargo_capacity_kg - - if "cargo_capacity_kg" in raw: - raw["cargo_capacity_kg"] = mass * 0.3 + # cargo_capacity / cargo_capacity_kg are set by _raw_physics_from_masses + # above, from the actual build mass -- not recomputed here. if "environmental_impact" in raw: 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: 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( - 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 a_mass = default_actuator if actuator_mass is None else actuator_mass @@ -1405,7 +1421,7 @@ class Pipeline: raw = self._raw_physics_from_masses( ctx, a_mass, s_mass, - bounds_by_name, units_by_name, cargo_capacity_kg, + bounds_by_name, units_by_name, platform_mass=p_mass, )