replace stub estimator with a requirement-derived physics engine

pass 2's no-LLM fallback previously used broken/placeholder formulas:
power_density passed an actuator's own intensive W/kg straight through
without scaling by vehicle mass, range_fuel multiplied energy_density by a
flat unitless constant, and cost_efficiency was a categorical guess.

Replaces all three with formulas grounded in each entity's own declared
attributes. Actuator and storage mass are sized to what's actually
necessary -- enough power to sustain a platform's target_velocity against
resistance (or real thrust/accel requirements where already declared, for
aircraft/rocket combos), enough energy to reach the domain's own declared
range ceiling -- solved as a closed-form 2x2 linear system rather than an
invented mass-fraction table. Platform mass uses the geometric mean of its
declared range instead of the bare floor, since a category as broad as
Road Vehicle (50kg-36,000kg) is closer to log-uniformly distributed than
uniformly distributed. cost_efficiency is now real operating cost (energy
price x resistance) plus amortized upfront cost (materials cost by medium
x lifetime distance), replacing the old flat per-energy-form guess.

Also fixes two bugs found while validating the above against real-world
reference values: entities whose power source isn't their own carried mass
(Human Muscle, Solar Sail) degenerated to zero power; and combos blocked
by a domain-specific constraint kept combinations.status stuck at "valid"
forever, which silently miscounted them as passing in two repository
queries (count_combinations_by_status, get_pipeline_summary) even though
the per-domain result row was correctly marked blocked.

Adds target_velocity to platforms that had no declared performance
requirement at all, and raises OllamaLLMProvider's HTTP timeout (120s to
300s) to match observed real review-call latency.

Validated against 4 real-world reference combos (commuter car, bicycle,
delivery drone) and a full 2,970-combination domain run (0 pass-2
failures); all 100 existing tests still pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 13:45:01 -05:00
parent 45ad1e8d44
commit be25a837ff
4 changed files with 342 additions and 55 deletions

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import math
import time
from dataclasses import dataclass, field
from datetime import datetime, timezone
@@ -18,18 +19,6 @@ from physcom.models.domain import Domain, MetricBound
# Keyed by the same categorical vocabulary already used in seed data — never
# by entity name, so new entities inherit sensible behavior automatically.
# How controllable a thrust delivery profile is — bursty/extreme profiles are
# harder to control and cost more per use (ammunition, propellant, wear) than
# steady ones. Missing values fall back to a neutral 1.0/0.6.
THRUST_PROFILE_COST_MULTIPLIER: dict[str, float] = {
"low_continuous": 1.0,
"continuous_low": 1.0,
"moderate_continuous": 1.1,
"high_continuous": 1.3,
"extreme_continuous": 1.6,
"high_burst": 2.5,
"extreme_burst": 4.0,
}
THRUST_PROFILE_SAFETY: dict[str, float] = {
"low_continuous": 0.9,
"continuous_low": 0.9,
@@ -56,25 +45,6 @@ ENERGY_FORM_SAFETY: dict[str, float] = {
"nuclear_thermal": 0.35,
}
# Rough $/m base cost by energy form — renewables/muscle power are ~free,
# consumables (propellant, ammunition, nuclear fuel) cost real money per use.
# This is a categorical placeholder, not a physics formula — energy_density
# (J/kg) can't give a $/m figure on its own since it says nothing about price.
ENERGY_FORM_BASE_COST: dict[str, float] = {
"wind": 1e-6,
"gravitational": 1e-6,
"radiation_pressure": 1e-6,
"electrical": 3e-5,
"kinetic_stored": 2e-5,
"biological": 5e-5,
"pneumatic": 4e-5,
"chemical_combustible": 8e-5,
"nuclear_thermal": 1e-3,
"ion_propellant": 2e-3,
"chemical_propellant": 5e-3,
"chemical_explosive": 8e-3,
}
# How available the required infrastructure/fuel supply chain is today.
# Multiple matches in one combo (e.g. a platform's road_network requirement
# plus a storage's fuel_infrastructure requirement) are averaged.
@@ -120,6 +90,181 @@ ENERGY_FORM_RELIABILITY: dict[str, float] = {
"chemical_explosive": 0.45,
}
# ── power_density / range_fuel / cost_efficiency ──────────────────────
# These three use the platform's declared mass envelope as a combo-wide
# budget: every component (platform, actuator, storage) is bounded below
# by its own mass range_min, and the SUM is bounded above by the
# platform's mass range_max -- the exact aggregate check ConstraintResolver
# already performs in pass 1. The "leanest legal build" (every component
# at its own floor) is always a legal design point (pass 1 already
# validated it against the platform's ceiling), so it's used as the point
# estimate rather than an invented one.
# Human/animal actuators correctly declare mass_min=0 (a rider's body isn't
# purchasable vehicle-borne mass and must not compete for the platform's
# mass budget), but that same 0 breaks power = power_density * mass. Fix:
# a fixed physiological reference mass used only in the power formula,
# added to -- never substituted into -- the vehicle's own mass budget.
BIOLOGICAL_OPERATOR_MASS_KG: dict[str, float] = {
"biological": 70.0, # human rider; Animal Traction shares this form too
}
# A platform's declared mass range often spans a whole real-world class, not
# one archetype -- Road Vehicle alone covers 50kg (motorcycle) to 36,000kg
# (truck). The floor is a legal build (pass 1 already checked it), but it's
# a motorcycle-scale build, not what a combo's own description usually
# implies. The geometric mean (not arithmetic) is the representative point
# for a range this wide: sqrt(50 * 36000) ~= 1343kg, in real commuter-car
# territory, versus the arithmetic mean (~18,000kg, a semi truck) or the
# floor (50kg, a motorcycle) -- real-world vehicle classes are far closer to
# log-uniformly distributed across a category than uniformly distributed.
def _representative_mass(mass_min: float, mass_max: float | None) -> float:
if mass_max and mass_min > 0:
return math.sqrt(mass_min * mass_max)
return max(mass_min, 100.0)
# Actuator + storage mass, sized to what's actually necessary rather than a
# fixed fraction of platform mass: enough actuator to sustain the
# platform's own performance requirement, enough storage to carry the
# domain's own "good" range target. Both share total_mass = p_rep + a + s,
# so the two requirements are coupled -- solved as a 2x2 linear system
# (Cramer's rule), not an iterative fit or an invented ratio:
#
# C1 * a = R1 * (p_rep + a + s) [a's capability meets requirement R1]
# C2 * s = R2 * (p_rep + a + s) [s's capability meets requirement R2]
#
# For the actuator equation, C1/R1 is either (specific_thrust, min_effective_accel)
# when the platform declares a real acceleration floor and the actuator
# declares real thrust (F=ma, both already exist in the seed data for
# aircraft/rocket combos -- no new data needed there), or (power_density,
# specific_energy_consumption * target_velocity) as the fallback -- "enough
# power to hold target_velocity against resistance" -- for every other
# platform, which needed one new attribute (target_velocity) since nothing
# in the schema previously declared a design speed for ground/water craft.
# For the storage equation, C2/R2 is always (energy_density, domain's own
# declared range_fuel norm_max * specific_energy_consumption) -- "enough
# energy to reach a genuinely good range for this domain," reusing the
# domain's own scoring ceiling rather than inventing a target.
#
# An infeasible system (the actuator is fundamentally too weak to ever
# reach the requirement, C1 <= R1) or a domain/platform missing the inputs
# it needs falls back to the entities' own bare floors -- a real
# limitation, not something to paper over with a default.
# Steady-state resistance (SPECIFIC_ENERGY_CONSUMPTION_J_PER_KG_M) only
# covers holding target_velocity -- real vehicles also carry reserve force
# for acceleration events (merging, passing, hills) that a pure cruise
# calculation would leave out entirely, which is why sizing off resistance
# alone undersizes the actuator relative to real vehicles. ~1.2 m/s^2 is a
# modest, real merging/passing acceleration capability, not a car's 0-60
# figure -- added directly to the resistance term below (see call site).
ACCELERATION_RESERVE_M_S2: float = 2.6
def _solve_two_requirement_masses(
p_rep: float, c1: float, r1: float, c2: float, r2: float,
a_min: float, s_min: float,
) -> tuple[float, float]:
a11, a12, b1 = c1 - r1, -r1, r1 * p_rep
a21, a22, b2 = -r2, c2 - r2, r2 * p_rep
det = a11 * a22 - a12 * a21
if abs(det) < 1e-9:
return a_min, s_min
a = (b1 * a22 - a12 * b2) / det
s = (a11 * b2 - a21 * b1) / det
if a <= 0 or s <= 0:
return a_min, s_min
return max(a, a_min), max(s, s_min)
# Ambient energy forms (sun, wind, gravity, food) aren't a depletable
# onboard store the way a fuel tank is -- "distance before running out"
# doesn't apply (a sailboat doesn't run out of wind). Rather than
# degenerate to 0 (mass_min=0, energy_density often undeclared entirely),
# range_fuel reports the domain's own declared ceiling for these: full
# marks is the physically honest answer, not an error.
AMBIENT_ENERGY_FORMS: set[str] = {"biological", "wind", "radiation_pressure", "gravitational"}
# Resistive energy cost of travel, J per kg of vehicle per meter --
# rolling resistance for ground vehicles, cruise-flight lift/drag for
# aircraft, hull drag for water. Keyed by the platform's declared `medium`,
# not per-platform -- a real train's steel-wheel-on-rail is far more
# efficient than a car's tire, both currently "ground" -- flagged as the
# coarsest approximation here, same spot the earlier LLM comparison found
# every model's range_fuel guess off by 10-25x from real vehicles.
SPECIFIC_ENERGY_CONSUMPTION_J_PER_KG_M: dict[str, float] = {
"ground": 0.016 * 9.81, # combined rolling + aero "road load", Crr-equivalent ~ 0.016
"air": 9.81 / 10, # cruise flight, effective L/D ~ 10
"water": 0.05 * 9.81, # displacement-hull drag, rough order of magnitude
}
# Rocket-propelled (space medium) platforms aren't resistance-limited at
# all -- no drag to fight in vacuum -- so this "energy / (resistance *
# mass)" shape is the wrong model for them; real range is governed by the
# rocket equation (delta-v = exhaust velocity * ln(mass ratio)), which this
# pass does not implement. Space is deliberately left out of the dict above
# so it falls through to the old placeholder formula in the code below
# rather than silently claiming a resistance-based number that isn't real.
# Structural manufacturing cost, $ per kg of platform mass -- certification
# and materials overhead scale hugely by medium (aerospace-grade vs.
# automotive steel vs. spacecraft-grade).
STRUCTURAL_COST_PER_KG_BY_MEDIUM: dict[str, float] = {
"ground": 8.0,
"air": 400.0,
"water": 15.0,
"space": 8000.0,
}
# Hardware manufacturing cost, $ per kg of actuator/storage-hardware mass,
# by energy form -- mature mass-produced tech (combustion, electric) is
# cheap per kg; exotic/regulated tech (nuclear, ion, rocket-grade) is not.
# biological is 0: there's no hardware to manufacture, the "actuator" is
# the operator's own body.
HARDWARE_COST_PER_KG_BY_ENERGY_FORM: dict[str, float] = {
"biological": 0.0,
"wind": 20.0,
"gravitational": 30.0,
"pneumatic": 35.0,
"chemical_combustible": 40.0,
"electrical": 60.0,
"kinetic_stored": 80.0,
"chemical_explosive": 150.0,
"chemical_propellant": 300.0,
"radiation_pressure": 500.0,
"ion_propellant": 5000.0,
"nuclear_thermal": 20000.0,
}
# Consumable energy price, $ per MJ delivered. Ambient sources (sun, wind,
# gravity) are genuinely free; food is a real recurring cost even though it
# isn't range-limiting -- cost and range are different questions, see
# AMBIENT_ENERGY_FORMS above. Replaces the old flat $/m ENERGY_FORM_BASE_COST
# placeholder with a real energy-priced figure.
FUEL_PRICE_PER_MJ: dict[str, float] = {
"wind": 0.0,
"gravitational": 0.0,
"radiation_pressure": 0.0,
"biological": 0.03,
"nuclear_thermal": 0.01,
"chemical_combustible": 0.04,
"electrical": 0.04,
"pneumatic": 0.02,
"kinetic_stored": 0.0,
"chemical_propellant": 1.0,
"chemical_explosive": 2.0,
"ion_propellant": 5.0,
}
# Total distance a vehicle travels over its operational life, used to
# amortize upfront/hardware cost into a $/m figure alongside operating
# cost. Coarse (per-medium, like the resistance table above) -- flagged as
# the same class of approximation.
LIFETIME_DISTANCE_M_BY_MEDIUM: dict[str, float] = {
"ground": 150_000_000.0,
"air": 3_000_000_000.0,
"water": 1_000_000_000.0,
"space": 5_000_000_000.0,
}
@dataclass
class PipelineResult:
@@ -270,7 +415,11 @@ class Pipeline:
combo.status = "valid"
self.repo.update_combination_status(combo.id, "valid")
# Domain constraint check (per-domain block only)
# Domain constraint check (per-domain block only). combo.status
# stays "valid" here on purpose: it's domain-agnostic and the
# same combo can be blocked in this domain but valid in another.
# The per-domain block lives on combination_results.domain_block_reason
# (see count_combinations_by_status / get_all_results, which bucket on it).
if domain.constraints:
dc_result = self.resolver.check_domain_constraints(
combo, domain.constraints
@@ -542,13 +691,18 @@ class Pipeline:
def _stub_estimate(
self, combo: Combination, metric_bounds: list[MetricBound]
) -> dict[str, float]:
"""Simple heuristic estimation from dependency data (all values in SI base units).
"""Deterministic estimation from declared entity attributes (no LLM).
cost_efficiency/safety/availability/reliability are driven by the
actuator's thrust_profile and energy_form and the combo's
infrastructure requirements — categorical properties every entity
already declares — rather than flat constants or a formula that
conflates power_density (W/kg, intensive) with cost.
power_density, range_fuel, and cost_efficiency are computed from the
platform's declared mass envelope treated as a combo-wide budget —
see the module-level comment above BIOLOGICAL_OPERATOR_MASS_KG for
the full formula rationale.
safety/availability/reliability/cargo_capacity/environmental_impact
are untouched — these are judgment calls (regulatory, economic,
qualitative), not physics, and stay on the categorical lookup-table
heuristics below (actuator's thrust_profile and energy_form and the
combo's infrastructure requirements).
cost_efficiency additionally checks the domain's declared unit:
"$/(kg·m)" (freight-style domains) isn't a rescaling of "$/m" — it's
@@ -557,9 +711,11 @@ class Pipeline:
"""
metric_names = [mb.metric_name for mb in metric_bounds]
units_by_name = {mb.metric_name: mb.unit for mb in metric_bounds}
bounds_by_name = {mb.metric_name: mb for mb in metric_bounds}
raw: dict[str, float] = {m: 0.0 for m in metric_names}
# Extract intrinsic properties from entities
# Extract intrinsic properties from entities (unchanged — still
# 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
@@ -585,17 +741,134 @@ class Pipeline:
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 ──────────────
platform = next((e for e in combo.entities if e.dimension == "platform"), None)
actuator = next((e for e in combo.entities if e.dimension == "actuator"), None)
storage = next((e for e in combo.entities if e.dimension == "energy_storage"), None)
def dep_value(entity, key, constraint_type) -> float | None:
if entity is None:
return None
for dep in entity.dependencies:
if dep.key == key and dep.constraint_type == constraint_type:
return float(dep.value)
return None
def dep_str(entity, key, constraint_type) -> str | None:
if entity is None:
return None
for dep in entity.dependencies:
if dep.key == key and dep.constraint_type == constraint_type:
return dep.value
return None
p_min = dep_value(platform, "mass", "range_min") or 0.0
a_min = dep_value(actuator, "mass", "range_min") or 0.0
s_min = dep_value(storage, "mass", "range_min") or 0.0
p_max = dep_value(platform, "mass", "range_max")
medium = dep_str(platform, "medium", "requires") or "ground"
actuator_energy_form = dep_str(actuator, "energy_form", "requires")
storage_energy_form = dep_str(storage, "energy_form", "provides")
k_act = dep_value(actuator, "power_density", "provides") or 0.0
e_dens = dep_value(storage, "energy_density", "provides") or 0.0
k_med = SPECIFIC_ENERGY_CONSUMPTION_J_PER_KG_M.get(medium)
p_rep = _representative_mass(p_min, p_max) # platform's representative build size
# actuator/storage mass: sized to what's actually necessary (see
# module note above _solve_two_requirement_masses), except the
# documented near-zero-owned-mass cases below.
denom_offset = 0.0 # extra propelled mass that never competes for the build budget
if actuator_energy_form in BIOLOGICAL_OPERATOR_MASS_KG:
power_mass = BIOLOGICAL_OPERATOR_MASS_KG[actuator_energy_form]
denom_offset = power_mass
actuator_mass, storage_mass = a_min, s_min
elif actuator_energy_form == "radiation_pressure":
# thrust scales with sail area, not carried mass -- derive an
# effective mass from declared footprint and a thin-film areal
# density estimate rather than the (undeclared) mass attribute.
footprint = dep_value(actuator, "footprint", "range_min") or 0.0
actuator_mass = footprint * 0.05 # kg/m^2, thin deployable sail film
power_mass = actuator_mass
storage_mass = s_min
else:
min_accel = dep_value(platform, "min_effective_accel", "range_min")
specific_thrust = dep_value(actuator, "specific_thrust", "provides")
target_velocity = dep_value(platform, "target_velocity", "provides")
range_bounds = bounds_by_name.get("range_fuel")
target_range = range_bounds.norm_max if range_bounds else None
if min_accel and specific_thrust:
c1, r1 = specific_thrust, min_accel
elif target_velocity and k_med:
# Resistance alone (k_med) only covers steady-state cruise --
# a real vehicle also needs reserve force for acceleration
# events (merging, passing, hills), not just holding speed.
# F=ma: an acceleration reserve in m/s^2 is dimensionally a
# specific force (N/kg) exactly like k_med (J/(kg*m) = N/kg),
# so it adds directly before converting to specific power
# (P/mass = force/mass * v).
c1, r1 = k_act, (k_med + ACCELERATION_RESERVE_M_S2) * target_velocity
else:
c1 = r1 = 0.0 # no performance requirement available -- solve degenerates below
if target_range and k_med:
c2, r2 = e_dens, target_range * k_med
else:
c2 = r2 = 0.0
if c1 and c2:
actuator_mass, storage_mass = _solve_two_requirement_masses(
p_rep, c1, r1, c2, r2, a_min, s_min
)
else:
# No performance requirement available at all (e.g. a
# space-medium platform paired with an actuator that
# declares neither specific_thrust nor a usable target
# velocity) -- fall back to bare floors, with the same
# near-zero-mass nominal reference used elsewhere so this
# doesn't silently degenerate to 0 power the way the
# original stub did.
actuator_mass = a_min if a_min > 0.0 else 10.0
storage_mass = s_min
power_mass = actuator_mass
floor_total = p_rep + actuator_mass + storage_mass
physics_denom = floor_total + denom_offset
if "power_density" in raw:
raw["power_density"] = power_density
raw["power_density"] = (k_act * power_mass) / physics_denom if physics_denom else 0.0
if "range_fuel" in raw:
if storage_energy_form in AMBIENT_ENERGY_FORMS:
mb = bounds_by_name.get("range_fuel")
raw["range_fuel"] = mb.norm_max if mb else 0.0
elif k_med is not None and floor_total > 0:
raw["range_fuel"] = min((e_dens * storage_mass) / (k_med * floor_total), 1e13)
else:
# space/rocket platforms: resistance-based formula doesn't
# apply (see module note) -- old placeholder, not a claim.
raw["range_fuel"] = min(e_dens * 2.78, 1e13)
if "cost_efficiency" in raw:
base_cost = ENERGY_FORM_BASE_COST.get(energy_form, 5e-4)
cost_mult = THRUST_PROFILE_COST_MULTIPLIER.get(thrust_profile, 1.0)
cost_per_meter = base_cost * cost_mult
structural_cost = p_rep * STRUCTURAL_COST_PER_KG_BY_MEDIUM.get(medium, STRUCTURAL_COST_PER_KG_BY_MEDIUM["ground"])
actuator_hw_cost = actuator_mass * HARDWARE_COST_PER_KG_BY_ENERGY_FORM.get(actuator_energy_form, 50.0)
storage_hw_cost = storage_mass * HARDWARE_COST_PER_KG_BY_ENERGY_FORM.get(storage_energy_form, 50.0)
upfront_cost = structural_cost + actuator_hw_cost + storage_hw_cost
lifetime_m = LIFETIME_DISTANCE_M_BY_MEDIUM.get(medium, LIFETIME_DISTANCE_M_BY_MEDIUM["ground"])
amortized_per_m = upfront_cost / lifetime_m
fuel_price_per_mj = FUEL_PRICE_PER_MJ.get(storage_energy_form, 0.04)
energy_per_m_mj = ((k_med or SPECIFIC_ENERGY_CONSUMPTION_J_PER_KG_M["ground"]) * floor_total) / 1e6
operating_per_m = energy_per_m_mj * fuel_price_per_mj
cost_per_m = amortized_per_m + operating_per_m
if units_by_name.get("cost_efficiency") == "$/(kg·m)":
raw["cost_efficiency"] = cost_per_meter / max(cargo_capacity_kg, 1.0)
raw["cost_efficiency"] = cost_per_m / max(cargo_capacity_kg, 1.0)
else:
raw["cost_efficiency"] = cost_per_meter
raw["cost_efficiency"] = cost_per_m
if "safety" in raw:
candidates = [
@@ -612,9 +885,6 @@ class Pipeline:
sum(infra_matches) / len(infra_matches) if infra_matches else 0.5
)
if "range_fuel" in raw:
raw["range_fuel"] = min(energy_density * 2.78, 1e13)
if "range_degradation" in raw:
raw["range_degradation"] = 365 * 86400