From 6cdd308583b9a6555eba878214ab95a0f08e1a31 Mon Sep 17 00:00:00 2001
From: Andrew Simonson
Date: Sat, 15 Aug 2026 19:17:59 -0500
Subject: [PATCH] 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
---
src/physcom/engine/pipeline.py | 121 +++++++++---------
.../templates/results/_explore_result.html | 5 +-
src/physcom_web/templates/results/detail.html | 6 +-
3 files changed, 71 insertions(+), 61 deletions(-)
diff --git a/src/physcom/engine/pipeline.py b/src/physcom/engine/pipeline.py
index c702771..bd2ca47 100644
--- a/src/physcom/engine/pipeline.py
+++ b/src/physcom/engine/pipeline.py
@@ -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,
)
diff --git a/src/physcom_web/templates/results/_explore_result.html b/src/physcom_web/templates/results/_explore_result.html
index 177e329..dad062f 100644
--- a/src/physcom_web/templates/results/_explore_result.html
+++ b/src/physcom_web/templates/results/_explore_result.html
@@ -1,7 +1,8 @@
{% if explore_result is none %}
No free mass allocation to explore for this combination — its
-actuator's mass isn't a design choice (a physiological or footprint-derived
-quantity), or the platform has no declared mass ceiling to bound the sliders.
+actuator's mass isn't a design choice (a footprint-derived quantity, like a
+radiation-pressure sail), or the platform has no declared mass ceiling to
+bound the sliders.