treat biological actuators as normal budget-competing mass, fix explore-panel visibility

Biological actuators (Human Muscle, Animal Traction) were special-cased
out of the mass optimizer entirely: a fixed 70kg reference used only in
the power formula, excluded from the platform's mass budget and from
the explore-panel sliders. That made "bigger operator" or "more
operators" inexpressible, and required a power_mass/denom_offset
parameter pair throughout the physics code solely to keep this one
case's numerator mass separate from its budget mass.

Operator mass is now a normal, budget-competing, structurally-carried
variable sized by the same joint optimizer as any mechanical actuator,
with BIOLOGICAL_OPERATOR_MASS_KG reinterpreted as a floor (at least one
real operator) rather than a fixed value -- the explore slider now
reads as "how many/how large are the operators." Since every remaining
case set power_mass == actuator_mass and denom_offset == 0.0 anyway,
those parameters were entirely vestigial once biological's special
case was gone, so _raw_physics_from_masses drops them.

Also: the explore section was gated on `explore_result is not none`,
so combos with no free mass to explore (radiation-pressure sails, or
previously biological) showed nothing at all instead of the existing
explanatory message. Gated on `scores` instead, so the section always
renders and the message inside `_explore_result.html` is reachable.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-15 19:17:59 -05:00
parent d871635779
commit 6cdd308583
3 changed files with 71 additions and 61 deletions

View File

@@ -106,13 +106,18 @@ ENERGY_FORM_RELIABILITY: dict[str, float] = {
# 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.
# Human/animal actuators declare mass_min=0 (there's no minimum purchase
# quantity for a rider the way there is for an engine), but treated as a
# literal floor that lets the optimizer size a payload down toward 0kg of
# operator -- nonsensical, and it also breaks power = power_density * mass.
# Used as a FLOOR (not a fixed value) on top of the declared mass_min: at
# least one real operator must be present. Above that floor, actuator mass
# is a free, budget-competing, structurally-carried variable exactly like
# any mechanical actuator -- the "size" slider means more or bigger
# operators (a loaded cargo trike, a two-horse team), sized by the same
# joint optimizer everything else uses, not a fixed physiological constant.
BIOLOGICAL_OPERATOR_MASS_KG: dict[str, float] = {
"biological": 70.0, # human rider; Animal Traction shares this form too
"biological": 70.0, # one average human rider; Animal Traction shares this form too
}
# A platform's declared mass range often spans a whole real-world class, not
@@ -859,28 +864,21 @@ class Pipeline:
ctx: "_PhysicsContext",
actuator_mass: float,
storage_mass: float,
power_mass: float,
denom_offset: 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. `power_mass` is separate from `actuator_mass` for the
biological/radiation-pressure special cases (see _stub_estimate),
where the numerator mass isn't the same as the build-budget mass;
for the normal (solved, optimized, or manually-explored) case
they're the same value. `platform_mass` defaults to the platform's
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)."""
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
physics_denom = floor_total + denom_offset
if "power_density" in bounds_by_name:
out["power_density"] = (ctx.k_act * power_mass) / physics_denom if physics_denom else 0.0
out["power_density"] = (ctx.k_act * actuator_mass) / floor_total if floor_total else 0.0
if "range_fuel" in bounds_by_name:
if ctx.storage_energy_form in AMBIENT_ENERGY_FORMS or ctx.k_med is None:
@@ -927,34 +925,39 @@ class Pipeline:
bounds_by_name: dict[str, MetricBound],
units_by_name: dict[str, str],
cargo_capacity_kg: float,
) -> tuple[float, float, float, float, float, bool]:
) -> tuple[float, float, float, bool]:
"""Pick the platform/actuator/storage mass for the build this domain
actually scores. First, the platform's declared physical
performance target (accel/thrust, or target_velocity/resistance)
sets a FLOOR -- a rotorcraft that can't produce enough thrust to
hover isn't a rotorcraft, regardless of how a smaller/cheaper
engine might score. That floor also sets the smallest platform
mass that could structurally carry it (CARGO_KG_PER_STRUCTURAL_KG
again, applied to the platform carrying its own actuator+storage
instead of cargo) -- below that, no actuator/storage choice is
physically possible. Above that lower bound, platform mass is a
real THIRD search variable, not fixed at p_rep: a bigger platform
also raises the structural cap on how much actuator+storage it can
carry, so growing all three together can score higher than
minimizing platform down to what's merely required. Searched
jointly (outer coarse-to-fine scan over platform mass, inner
coarse-to-fine scan over actuator/storage at each candidate) for
whatever allocation maximizes this domain's own weighted composite
score, using the same normalize()/composite_score() the real
scoring pass uses. Not "just enough to function" and not "best
score regardless of function" -- both, floor then optimize jointly.
Returns (actuator_mass, storage_mass, power_mass, denom_offset,
platform_mass, feasible); see _raw_physics_from_masses for what
power_mass and denom_offset mean. `feasible` is False only when no
platform mass within its own declared ceiling could structurally
carry the required floor -- power_density/range_fuel/cost_efficiency
are all per-kg ratios, so they don't naturally penalize a build
whose absolute mass tramples its own platform's declared ceiling;
engine might score. Biological actuators (a rider's own body) get
the same treatment with one addition: BIOLOGICAL_OPERATOR_MASS_KG
sets a floor under the floor -- at least one real operator, even if
the performance-derived requirement would otherwise ask for less
-- but above that, mass is a free variable exactly like a
mechanical actuator's; "bigger" here means more or bigger
operators, not a fixed physiological constant. That floor also
sets the smallest platform mass that could structurally carry it
(CARGO_KG_PER_STRUCTURAL_KG again, applied to the platform
carrying its own actuator+storage instead of cargo) -- below that,
no actuator/storage choice is physically possible. Above that
lower bound, platform mass is a real THIRD search variable, not
fixed at p_rep: a bigger platform also raises the structural cap
on how much actuator+storage it can carry, so growing all three
together can score higher than minimizing platform down to what's
merely required. Searched jointly (outer coarse-to-fine scan over
platform mass, inner coarse-to-fine scan over actuator/storage at
each candidate) for whatever allocation maximizes this domain's
own weighted composite score, using the same normalize()/
composite_score() the real scoring pass uses. Not "just enough to
function" and not "best score regardless of function" -- both,
floor then optimize jointly. Returns (actuator_mass, storage_mass,
platform_mass, feasible). `feasible` is False only when no platform
mass within its own declared ceiling could structurally carry the
required floor -- power_density/range_fuel/cost_efficiency are all
per-kg ratios, so they don't naturally penalize a build whose
absolute mass tramples its own platform's declared ceiling;
callers must treat an infeasible build as a hard fail rather than
trusting the (still-computable, still ratio-plausible) score. Also
used by evaluate_allocation to compute the slider's starting
@@ -967,16 +970,15 @@ class Pipeline:
return float(dep.value)
return None
if ctx.actuator_energy_form in BIOLOGICAL_OPERATOR_MASS_KG:
power_mass = BIOLOGICAL_OPERATOR_MASS_KG[ctx.actuator_energy_form]
return ctx.a_min, ctx.s_min, power_mass, power_mass, ctx.p_rep, True
if ctx.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.
# No meaningful "bigger sail" mass slider here (area-driven,
# not budget-driven), so this stays its own case.
footprint = dep_value(ctx.actuator, "footprint", "range_min") or 0.0
actuator_mass = footprint * 0.05 # kg/m^2, thin deployable sail film
return actuator_mass, ctx.s_min, actuator_mass, 0.0, ctx.p_rep, True
return actuator_mass, ctx.s_min, ctx.p_rep, True
# Step 1: the required floor (same solve as before -- now a floor
# for the search below, not the final answer).
@@ -1019,10 +1021,16 @@ class Pipeline:
required_actuator = ctx.a_min if ctx.a_min > 0.0 else 10.0
required_storage = ctx.s_min
if ctx.actuator_energy_form in BIOLOGICAL_OPERATOR_MASS_KG:
# At least one real operator, regardless of what the bare
# performance solve above would have asked for -- see the
# BIOLOGICAL_OPERATOR_MASS_KG module comment.
required_actuator = max(required_actuator, BIOLOGICAL_OPERATOR_MASS_KG[ctx.actuator_energy_form])
if ctx.p_max is None:
# No declared mass ceiling (e.g. Spaceship) -- no bounded
# budget to search within, use the requirement floor as-is.
return required_actuator, required_storage, required_actuator, 0.0, ctx.p_rep, True
return required_actuator, required_storage, ctx.p_rep, True
a_floor = max(ctx.a_min, required_actuator)
s_floor = max(ctx.s_min, required_storage)
@@ -1048,11 +1056,11 @@ class Pipeline:
# per-kg ratios, so they don't naturally penalize a build whose
# ABSOLUTE mass tramples its own platform's declared ceiling --
# something else has to catch that).
return a_floor, s_floor, a_floor, 0.0, p_lo, False
return a_floor, s_floor, p_lo, False
def objective(platform_mass: float, actuator_mass: float, storage_mass: float) -> float:
raw = self._raw_physics_from_masses(
ctx, actuator_mass, storage_mass, actuator_mass, 0.0,
ctx, actuator_mass, storage_mass,
bounds_by_name, units_by_name, cargo_capacity_kg,
platform_mass=platform_mass,
)
@@ -1115,7 +1123,7 @@ class Pipeline:
best_score, best_p = sc, p
actuator_mass, storage_mass, _score = best_at_platform(best_p, grid=12, rounds=6)
return actuator_mass, storage_mass, actuator_mass, 0.0, best_p, True
return actuator_mass, storage_mass, best_p, True
@staticmethod
def _search_best_allocation(
@@ -1224,11 +1232,11 @@ class Pipeline:
ctx = self._physics_context(combo, bounds_by_name)
feasible = True
if ctx is not None:
actuator_mass, storage_mass, power_mass, denom_offset, 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
)
raw.update(self._raw_physics_from_masses(
ctx, actuator_mass, storage_mass, power_mass, denom_offset,
ctx, actuator_mass, storage_mass,
bounds_by_name, units_by_name, cargo_capacity_kg,
platform_mass=platform_mass,
))
@@ -1289,23 +1297,22 @@ class Pipeline:
never silently allowed below what pass 1 would have rejected.
Returns None for combos with no free actuator mass to explore
(biological actuators, radiation-pressure sails -- see
_stub_estimate's module note) or with no declared platform mass
ceiling to bound a weight-class slider.
(radiation-pressure sails -- thrust is area-driven, not a mass
choice, see _decide_masses) or with no declared platform mass
ceiling to bound a weight-class slider. Biological actuators DO
get sliders: rider/operator mass is a real, budget-competing
variable like any other actuator (see BIOLOGICAL_OPERATOR_MASS_KG).
"""
bounds_by_name = {mb.metric_name: mb for mb in domain.metric_bounds}
units_by_name = {mb.metric_name: mb.unit for mb in domain.metric_bounds}
ctx = self._physics_context(combo, bounds_by_name)
if ctx is None or ctx.p_max is None:
return None
if (
ctx.actuator_energy_form in BIOLOGICAL_OPERATOR_MASS_KG
or ctx.actuator_energy_form == "radiation_pressure"
):
if ctx.actuator_energy_form == "radiation_pressure":
return None
cargo_capacity_kg = (ctx.p_min + ctx.a_min + ctx.s_min) * CARGO_KG_PER_STRUCTURAL_KG
default_actuator, default_storage, _power_mass, _denom_offset, 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
)
p_mass = default_platform if platform_mass is None else platform_mass
@@ -1317,7 +1324,7 @@ class Pipeline:
s_mass = max(ctx.s_min, s_mass)
raw = self._raw_physics_from_masses(
ctx, a_mass, s_mass, a_mass, 0.0,
ctx, a_mass, s_mass,
bounds_by_name, units_by_name, cargo_capacity_kg,
platform_mass=p_mass,
)