add speed as a derived metric, fix aerodynamic drag gap it exposed, expand urban_commuting
target_velocity was only ever a platform-declared input used to size the actuator -- an achieved-speed OUTPUT never existed anywhere, even though trip time clearly matters for a domain like urban commuting. Added "speed" as a genuine derived metric: achieved steady-state cruise speed computed from the build's own power_density and the medium's resistance, the same way power_density/range_fuel/cost_efficiency are already outputs of a build rather than inputs to it. That immediately surfaced a known, previously-deferred gap: the resistance model was mass-proportional only (rolling resistance), with no velocity-squared aerodynamic drag term, so inverting power/resistance for speed had no ceiling at all -- light vehicles were "achieving" thousands of m/s. Added DRAG_POWER_COEFF_BY_MEDIUM (ground only, a car-like reference cross-section) and a closed-form cubic solve (_solve_achievable_speed_mps, via Cardano's formula, no iteration) for the achieved speed where propulsive power balances resistance + drag. Reused the same effective (drag-inclusive) resistance for range_fuel and cost_efficiency's operating-cost term, since they're the same physical quantity (energy spent per meter) evaluated at the build's actual speed. This also closes the range-overestimation bug flagged much earlier against combo #876 (a real e-bike): range dropped from ~1,032km to ~53km, right in the ~50-80km realistic e-bike range that was the original target. Air and water media are unchanged (air's L/D-based cruise model doesn't have this problem; water hull drag needs its own treatment, not a car's frontal area -- left as a known remaining gap). Also added cargo_capacity_kg to urban_commuting (whether a commute vehicle can carry groceries/passengers/gear matters as much as the metrics already scored there) and renormalized weights across the now five metrics. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -197,6 +197,38 @@ def _solve_two_requirement_masses(
|
|||||||
return a_min, s_min
|
return a_min, s_min
|
||||||
return max(a, a_min), max(s, s_min)
|
return max(a, a_min), max(s, s_min)
|
||||||
|
|
||||||
|
|
||||||
|
def _solve_achievable_speed_mps(
|
||||||
|
power_density: float, floor_total: float, k_med: float, drag_coeff: float,
|
||||||
|
) -> float:
|
||||||
|
"""Invert specific_power = k_med*v + (drag_coeff/floor_total)*v^3 for v
|
||||||
|
-- the steady-state speed at which a build's actual power output exactly
|
||||||
|
balances mass-proportional resistance plus mass-independent aerodynamic
|
||||||
|
drag. A depressed cubic (no v^2 term) with drag_coeff/floor_total > 0
|
||||||
|
and k_med >= 0: A*v^3 + B*v - C = 0 is strictly increasing for v >= 0
|
||||||
|
(derivative 3*A*v^2 + B > 0 everywhere), so it has exactly one
|
||||||
|
non-negative real root -- solved directly via Cardano's formula, no
|
||||||
|
iteration needed. Falls back to the plain linear model (v = power/k_med)
|
||||||
|
when there's no drag coefficient for this medium, so ungraded media
|
||||||
|
behave exactly as before."""
|
||||||
|
if power_density <= 0 or k_med is None:
|
||||||
|
return 0.0
|
||||||
|
if drag_coeff <= 0 or floor_total <= 0:
|
||||||
|
return power_density / k_med if k_med else 0.0
|
||||||
|
A = drag_coeff / floor_total
|
||||||
|
B = k_med
|
||||||
|
C = power_density
|
||||||
|
p, q = B / A, -C / A
|
||||||
|
|
||||||
|
def cbrt(x: float) -> float:
|
||||||
|
return math.copysign(abs(x) ** (1 / 3), x) if x else 0.0
|
||||||
|
|
||||||
|
discriminant = (q / 2) ** 2 + (p / 3) ** 3 # always >= 0 given p, C >= 0
|
||||||
|
sqrt_disc = math.sqrt(discriminant)
|
||||||
|
v = cbrt(-q / 2 + sqrt_disc) + cbrt(-q / 2 - sqrt_disc)
|
||||||
|
return max(v, 0.0)
|
||||||
|
|
||||||
|
|
||||||
# Ambient energy forms (sun, wind, gravity) aren't a depletable onboard
|
# Ambient energy forms (sun, wind, gravity) aren't a depletable onboard
|
||||||
# store the way a fuel tank is -- "distance before running out" doesn't
|
# 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
|
# apply (a sailboat doesn't run out of wind). Rather than degenerate to 0
|
||||||
@@ -228,24 +260,38 @@ SPECIFIC_ENERGY_CONSUMPTION_J_PER_KG_M: dict[str, float] = {
|
|||||||
# so it falls through to the old placeholder formula in the code below
|
# 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.
|
# rather than silently claiming a resistance-based number that isn't real.
|
||||||
#
|
#
|
||||||
# KNOWN GAP: this whole table is mass-proportional resistance only (rolling
|
# FORMERLY A KNOWN GAP, now fixed below: the table above is mass-proportional
|
||||||
# resistance, effectively) -- there's no aerodynamic drag term (force ~
|
# resistance only (rolling resistance, effectively) -- no aerodynamic drag
|
||||||
# frontal_area * velocity^2, independent of mass). That's a reasonable
|
# term (force ~ frontal_area * velocity^2, independent of mass). That's a
|
||||||
# approximation for something car-scale, where rolling resistance genuinely
|
# reasonable approximation for something car-scale, where rolling resistance
|
||||||
# dominates at typical speeds and this was validated against real car range.
|
# genuinely dominates at typical speeds and this was validated against real
|
||||||
# It badly overestimates range for light/human-scale vehicles, where drag
|
# car range. It badly overestimated range for light/human-scale vehicles,
|
||||||
# is the dominant resistance term and doesn't scale down with mass the way
|
# where drag is the dominant resistance term and doesn't scale down with
|
||||||
# this formula assumes -- confirmed on a real combo (Light Personal Vehicle +
|
# mass the way this formula assumes -- confirmed on a real combo (Light
|
||||||
# Electric Motor + Rechargeable Battery, #876): a sane 9kg battery on a
|
# Personal Vehicle + Electric Motor + Rechargeable Battery, #876): a sane
|
||||||
# realistic 31kg vehicle came out to ~1,977km, a 6-9x overestimate against
|
# 9kg battery on a realistic 31kg vehicle came out to ~1,977km, a 6-9x
|
||||||
# real e-bikes on comparable battery energy (~50-80km on ~500Wh). The mass
|
# overestimate against real e-bikes on comparable battery energy (~50-80km
|
||||||
# allocation itself was fine (correctly floor-clamped, nothing oversized) --
|
# on ~500Wh). It also meant an achieved-speed metric derived from power
|
||||||
# this is a missing term in the resistance formula, not an allocation bug,
|
# alone (see DRAG_POWER_COEFF_BY_MEDIUM / _solve_achievable_speed_mps below)
|
||||||
# so a mass-allocation optimizer wouldn't fix it either. Real fix needs a
|
# had no ceiling at all -- without a v^2-scaling force to push back, more
|
||||||
# genuine drag term (frontal-area-ish figure -- `footprint` exists but is a
|
# power always bought proportionally more speed, forever.
|
||||||
# ground-footprint number, not obviously the right proxy for cross-sectional
|
#
|
||||||
# area facing the wind -- and a drag coefficient assumption), scoped
|
# DRAG_POWER_COEFF_BY_MEDIUM below adds that missing term: a mass-INDEPENDENT
|
||||||
# separately from the resistance-constant tuning already done here.
|
# drag power coefficient (0.5 * air_density * drag_coefficient * frontal_area,
|
||||||
|
# W per (m/s)^3) added on top of the existing mass-proportional term. Ground
|
||||||
|
# only for now (the diagnosed case, and where a car-like reference
|
||||||
|
# cross-section is a defensible categorical estimate the way the rest of
|
||||||
|
# this file's constants are); air's existing L/D-based model is already a
|
||||||
|
# reasonable velocity-roughly-linear cruise approximation and doesn't have
|
||||||
|
# this problem, and water hull drag would need its own (different) treatment
|
||||||
|
# rather than reusing a car's frontal area, so it's left as a known
|
||||||
|
# remaining gap rather than guessed at here.
|
||||||
|
DRAG_POWER_COEFF_BY_MEDIUM: dict[str, float] = {
|
||||||
|
# 0.5 * rho_air(1.225 kg/m^3) * Cd(~0.3) * frontal_area(~2.2 m^2, small
|
||||||
|
# car reference) -- sanity check: at 30 m/s (108 km/h) this alone costs
|
||||||
|
# ~11kW, in the right ballpark for real highway cruise power.
|
||||||
|
"ground": 0.5 * 1.225 * 0.3 * 2.2,
|
||||||
|
}
|
||||||
|
|
||||||
# Structural manufacturing cost, $ per kg of platform mass -- certification
|
# Structural manufacturing cost, $ per kg of platform mass -- certification
|
||||||
# and materials overhead scale hugely by medium (aerospace-grade vs.
|
# and materials overhead scale hugely by medium (aerospace-grade vs.
|
||||||
@@ -889,9 +935,31 @@ class Pipeline:
|
|||||||
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
|
||||||
|
power_density_value = (ctx.k_act * actuator_mass) / floor_total if floor_total else 0.0
|
||||||
|
|
||||||
if "power_density" in bounds_by_name:
|
if "power_density" in bounds_by_name:
|
||||||
out["power_density"] = (ctx.k_act * actuator_mass) / floor_total if floor_total else 0.0
|
out["power_density"] = power_density_value
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# mass-independent aerodynamic drag term -- not a platform-declared
|
||||||
|
# constant. A build with more power than the platform's bare
|
||||||
|
# target_velocity requires achieves a genuinely higher speed here;
|
||||||
|
# an underbuilt one achieves less -- speed is an output of the
|
||||||
|
# build, not an input to it. Computed unconditionally (not just
|
||||||
|
# when "speed" is a scored metric) because range_fuel/cost_efficiency
|
||||||
|
# below both need it too: the energy actually spent per meter
|
||||||
|
# depends on how fast this build is actually going, drag included.
|
||||||
|
drag_coeff = DRAG_POWER_COEFF_BY_MEDIUM.get(ctx.medium, 0.0)
|
||||||
|
achieved_speed = _solve_achievable_speed_mps(power_density_value, floor_total, ctx.k_med, drag_coeff)
|
||||||
|
effective_k_med = (
|
||||||
|
(ctx.k_med + drag_coeff * achieved_speed ** 2 / floor_total)
|
||||||
|
if ctx.k_med is not None and floor_total > 0 else ctx.k_med
|
||||||
|
)
|
||||||
|
|
||||||
|
if "speed" in bounds_by_name:
|
||||||
|
out["speed"] = achieved_speed
|
||||||
|
|
||||||
if "range_fuel" in bounds_by_name:
|
if "range_fuel" in bounds_by_name:
|
||||||
if ctx.storage_energy_form in AMBIENT_ENERGY_FORMS or ctx.k_med is None:
|
if ctx.storage_energy_form in AMBIENT_ENERGY_FORMS or ctx.k_med is None:
|
||||||
@@ -899,7 +967,7 @@ class Pipeline:
|
|||||||
out["range_fuel"] = mb.norm_max if mb else 0.0
|
out["range_fuel"] = mb.norm_max if mb else 0.0
|
||||||
elif floor_total > 0:
|
elif floor_total > 0:
|
||||||
out["range_fuel"] = min(
|
out["range_fuel"] = min(
|
||||||
(ctx.e_dens * storage_mass) / (ctx.k_med * floor_total), 1e13
|
(ctx.e_dens * storage_mass) / (effective_k_med * floor_total), 1e13
|
||||||
)
|
)
|
||||||
|
|
||||||
if "cost_efficiency" in bounds_by_name:
|
if "cost_efficiency" in bounds_by_name:
|
||||||
@@ -920,7 +988,7 @@ class Pipeline:
|
|||||||
|
|
||||||
fuel_price_per_mj = FUEL_PRICE_PER_MJ.get(ctx.storage_energy_form, 0.04)
|
fuel_price_per_mj = FUEL_PRICE_PER_MJ.get(ctx.storage_energy_form, 0.04)
|
||||||
energy_per_m_mj = (
|
energy_per_m_mj = (
|
||||||
(ctx.k_med or SPECIFIC_ENERGY_CONSUMPTION_J_PER_KG_M["ground"]) * floor_total
|
(effective_k_med or SPECIFIC_ENERGY_CONSUMPTION_J_PER_KG_M["ground"]) * floor_total
|
||||||
) / 1e6
|
) / 1e6
|
||||||
operating_per_m = energy_per_m_mj * fuel_price_per_mj
|
operating_per_m = energy_per_m_mj * fuel_price_per_mj
|
||||||
|
|
||||||
|
|||||||
@@ -741,9 +741,16 @@ URBAN_COMMUTING = Domain(
|
|||||||
# this project hasn't done -- not scored anywhere for now rather than
|
# this project hasn't done -- not scored anywhere for now rather than
|
||||||
# pretend a quick formula or an equally uninformed LLM guess settles it.
|
# pretend a quick formula or an equally uninformed LLM guess settles it.
|
||||||
# Weights renormalized to sum to 1.0 across the remaining metrics.
|
# Weights renormalized to sum to 1.0 across the remaining metrics.
|
||||||
MetricBound("power_density", weight=0.4167, norm_min=1, norm_max=2000, unit="W/kg"),
|
# speed and cargo_capacity_kg added -- a commute's actual travel
|
||||||
MetricBound("cost_efficiency", weight=0.4167, norm_min=1e-5, norm_max=2e-3, unit="$/m", lower_is_better=True),
|
# time and whether the vehicle can carry groceries/passengers/gear
|
||||||
MetricBound("range_fuel", weight=0.1666, norm_min=5000, norm_max=500000, unit="m"),
|
# both matter as much as raw power_density did on their own; speed
|
||||||
|
# is a genuine build OUTPUT (see _raw_physics_from_masses), not a
|
||||||
|
# platform-declared constant.
|
||||||
|
MetricBound("power_density", weight=0.25, norm_min=1, norm_max=2000, unit="W/kg"),
|
||||||
|
MetricBound("cost_efficiency", weight=0.25, norm_min=1e-5, norm_max=2e-3, unit="$/m", lower_is_better=True),
|
||||||
|
MetricBound("speed", weight=0.25, norm_min=2, norm_max=30, unit="m/s"),
|
||||||
|
MetricBound("range_fuel", weight=0.10, norm_min=5000, norm_max=500000, unit="m"),
|
||||||
|
MetricBound("cargo_capacity_kg", weight=0.15, norm_min=1, norm_max=500, unit="kg"),
|
||||||
],
|
],
|
||||||
constraints=[DomainConstraint("medium", ["ground", "air"])],
|
constraints=[DomainConstraint("medium", ["ground", "air"])],
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user