From d1f14dbf14c7699f9da35c3ea579efcda392023d Mon Sep 17 00:00:00 2001
From: Andrew Simonson
Date: Sat, 15 Aug 2026 19:37:38 -0500
Subject: [PATCH] remove food from ambient energy forms, size solar sails to
actual power needs
Biological Feed (food) was treated as an "ambient" energy source, so
range_fuel always reported the domain's ceiling regardless of how much
food was carried -- stopping to eat is a resupply, the same category as
refuelling a tank, not a genuinely external/inexhaustible source like
sun or wind. Removed "biological" from AMBIENT_ENERGY_FORMS; food now
uses the normal storage-mass-limited range formula like any fuel.
Solar Sail was still special-cased to a fixed footprint-derived mass
(100m^2 -> 5kg) regardless of what a domain's power/velocity target
actually needed -- the same "fixed reference instead of a requirement
floor" bug biological actuators had before last session's fix. Folded
it into the same general requirement-floor + joint-optimizer path:
declared footprint becomes a FLOOR (SAIL_AREAL_DENSITY_KG_PER_M2), not
a fixed value, so sail size scales with what's actually needed --
"enough panels to supply enough power for actuator impulse" is now
enforced the same way structural/mass-ceiling requirements already are,
instead of relying on a product-spec constant that happened to work or
not. No actuator type is special-cased for mass sizing anymore.
Confirmed the fix surfaces an honest result rather than hiding one: a
solar sail's declared 0.01 W/kg specific power can never reach
interplanetary_travel's 10 W/kg power_density floor at any sail size
(the ratio is capped by the sail's own power_density regardless of
scale), so it correctly still scores 0 there -- a real technology/domain
mismatch, not a sizing bug.
Co-Authored-By: Claude Sonnet 5
---
src/physcom/engine/pipeline.py | 76 +++++++++++--------
.../templates/results/_explore_result.html | 4 +-
src/physcom_web/templates/results/detail.html | 2 +-
3 files changed, 46 insertions(+), 36 deletions(-)
diff --git a/src/physcom/engine/pipeline.py b/src/physcom/engine/pipeline.py
index bd2ca47..77558be 100644
--- a/src/physcom/engine/pipeline.py
+++ b/src/physcom/engine/pipeline.py
@@ -106,6 +106,16 @@ 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.
+# Radiation-pressure actuators (solar sails) don't declare a "mass" at
+# all -- thrust scales with sail area, not carried mass -- so their
+# effective mass is derived from declared footprint via a thin deployable
+# sail film's areal density. Used the same way as BIOLOGICAL_OPERATOR_MASS_KG
+# below: converts the entity's declared footprint FLOOR into a mass floor,
+# not a fixed value -- above it, effective mass is a free, budget-competing
+# variable like any other actuator (bigger sail = more collected power),
+# sized by the same joint optimizer, not a one-off product spec.
+SAIL_AREAL_DENSITY_KG_PER_M2: float = 0.05
+
# 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
@@ -187,13 +197,16 @@ def _solve_two_requirement_masses(
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"}
+# Ambient energy forms (sun, wind, gravity) 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. Food is deliberately NOT here:
+# stopping to eat is a resupply, the same category as refuelling a tank,
+# not a genuinely external/inexhaustible power source -- Biological Feed
+# uses the normal storage-mass-limited range_fuel formula.
+AMBIENT_ENERGY_FORMS: set[str] = {"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
@@ -931,13 +944,16 @@ class Pipeline:
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. 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
+ engine might score. Biological actuators (a rider's own body) and
+ radiation-pressure actuators (a solar sail) get the same treatment
+ with one addition: BIOLOGICAL_OPERATOR_MASS_KG / a footprint-derived
+ floor (see SAIL_AREAL_DENSITY_KG_PER_M2) sets a floor under the
+ floor -- at least one real operator, or the sail's own declared
+ minimum footprint, 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" means more
+ or bigger operators, or a bigger sail, not a fixed 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,
@@ -970,16 +986,6 @@ class Pipeline:
return float(dep.value)
return None
- 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, ctx.p_rep, True
-
# Step 1: the required floor (same solve as before -- now a floor
# for the search below, not the final answer).
min_accel = dep_value(ctx.platform, "min_effective_accel", "range_min")
@@ -1026,6 +1032,13 @@ class Pipeline:
# 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])
+ elif ctx.actuator_energy_form == "radiation_pressure":
+ # At least the entity's own declared minimum sail footprint,
+ # regardless of what the bare performance solve above would
+ # have asked for -- see the SAIL_AREAL_DENSITY_KG_PER_M2
+ # module comment.
+ footprint_floor = dep_value(ctx.actuator, "footprint", "range_min") or 0.0
+ required_actuator = max(required_actuator, footprint_floor * SAIL_AREAL_DENSITY_KG_PER_M2)
if ctx.p_max is None:
# No declared mass ceiling (e.g. Spaceship) -- no bounded
@@ -1296,20 +1309,19 @@ class Pipeline:
minimum (platform is also ceiling-clamped to its declared max) --
never silently allowed below what pass 1 would have rejected.
- Returns None for combos with no free actuator mass to explore
- (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).
+ Returns None only for combos with no declared platform mass
+ ceiling to bound a weight-class slider (e.g. Spaceship). Every
+ actuator type gets sliders, including biological (rider/operator
+ mass, see BIOLOGICAL_OPERATOR_MASS_KG) and radiation-pressure
+ (effective sail mass derived from footprint, see
+ SAIL_AREAL_DENSITY_KG_PER_M2) -- both are real, budget-competing
+ variables like any mechanical actuator's mass.
"""
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 == "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, default_platform, _feasible = self._decide_masses(
diff --git a/src/physcom_web/templates/results/_explore_result.html b/src/physcom_web/templates/results/_explore_result.html
index dad062f..8278b7d 100644
--- a/src/physcom_web/templates/results/_explore_result.html
+++ b/src/physcom_web/templates/results/_explore_result.html
@@ -1,8 +1,6 @@
{% if explore_result is none %}
No free mass allocation to explore for this combination — its
-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.
+platform has no declared mass ceiling to bound the sliders.