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:
@@ -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,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user