From 00cc8dd9eff0af1652652aae33fb89aec42426ae Mon Sep 17 00:00:00 2001 From: Andrew Simonson Date: Wed, 4 Mar 2026 16:30:09 -0600 Subject: [PATCH] I love how stupid this project is si units and redefining speed metric as thrust/weight ratio --- .claude/settings.local.json | 5 +- ...1-speed-metric-to-power-density-scoring.md | 50 +++ LOGIC DOCS/README.md | 5 + src/physcom/engine/constraint_resolver.py | 42 +-- src/physcom/engine/pipeline.py | 31 +- src/physcom/llm/prompts.py | 2 +- src/physcom/seed/transport_example.py | 311 ++++++++---------- src/physcom/units.py | 110 +++++++ src/physcom_web/app.py | 2 + .../templates/domains/_metrics_table.html | 4 +- src/physcom_web/templates/domains/list.html | 4 +- .../templates/entities/_dep_table.html | 2 +- src/physcom_web/templates/results/detail.html | 6 +- tests/conftest.py | 58 ++-- tests/test_constraint_resolver.py | 67 +--- tests/test_pipeline.py | 6 +- tests/test_pipeline_async.py | 6 +- tests/test_scorer.py | 22 +- tests/test_units.py | 102 ++++++ 19 files changed, 494 insertions(+), 341 deletions(-) create mode 100644 LOGIC DOCS/001-speed-metric-to-power-density-scoring.md create mode 100644 LOGIC DOCS/README.md create mode 100644 src/physcom/units.py create mode 100644 tests/test_units.py diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 07f16a9..6f17283 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -6,7 +6,10 @@ "Bash(python -m pytest *)", "Bash(python -m physcom *)", "Bash(python -m physcom_web *)", - "Bash(python -c *)" + "Bash(python -c *)", + "WebFetch(domain:127.0.0.1)", + "WebFetch(domain:localhost)", + "Bash(curl:*)" ] } } diff --git a/LOGIC DOCS/001-speed-metric-to-power-density-scoring.md b/LOGIC DOCS/001-speed-metric-to-power-density-scoring.md new file mode 100644 index 0000000..0f927d0 --- /dev/null +++ b/LOGIC DOCS/001-speed-metric-to-power-density-scoring.md @@ -0,0 +1,50 @@ +# Replace "speed" metric with power density scoring + +## The problem + +After standardizing the database to SI base units, the stub estimator's speed formula was exposed as fundamentally broken: + +```python +raw["speed"] = min(power_density * 0.14, 3e8) # m/s +``` + +An Electric Motor (2000 W/kg) on a Light Personal Vehicle produced 280 m/s (1008 km/h). The old formula had the same issue (1000 km/h) — it was just less visible in km/h with the `|si` filter masking the absurdity. + +The root cause: **top speed cannot be derived from power density alone**. Speed depends on drag, rolling resistance, medium, vehicle geometry, and a dozen other factors the stub doesn't know. A linear power-to-speed model can never work across domains spanning e-bikes to spaceships. + +## Candidate replacement: thrust-to-weight ratio (N/kg) + +N/kg is dimensionally m/s² — a well-understood physical quantity. The actuator already declares `power_density` (W/kg), so the data exists. But three concerns emerged: + +1. **Redundant with Pass 1** — The constraint system already checks actuator `power_density` against platform `power_density_required`. Combos that reach scoring have already passed this gate, so scoring on the same axis re-evaluates something the pipeline already filtered. + +2. **Actuator-dominated** — Only the actuator declares `power_density`. Every combo sharing the same actuator scores identically, so the metric doesn't capture entity interplay. + +3. **Higher isn't always better** — A Rocket Nozzle (10,000 W/kg) on a Light Personal Vehicle has extreme thrust-to-weight, but that's not desirable for urban commuting. The current scorer only does monotonic normalization (higher is better, or lower is better), not "closer to optimal band." + +## The insight: `power_density_required` is a scoring concern, not a constraint + +Examining what `power_density_required` actually represents on platforms: + +- Road Vehicle: "I need >= 5 W/kg" +- Fixed-Wing Aircraft: "I need >= 100 W/kg" +- Spaceship: "I need >= 300 W/kg" + +These thresholds are arbitrary performance gradients — a car with 4 W/kg still moves, just poorly. There's no hard physics cliff. Compare with genuinely binary constraints: + +- **medium mutex** — a boat physically cannot operate in space +- **energy_form requires/provides** — an electric motor cannot burn gasoline +- **mass range** — a nuclear reactor won't fit on a bicycle frame + +Power density fit is "how well does it work," not "can it work at all." That's scoring territory. + +The one edge case — aircraft needing thrust-to-weight > 1 to leave the ground — is already implicitly handled by atmosphere/medium constraints and energy density minimums. + +## Decision + +Remove `power_density_required` from platform entity constraints and introduce it as a domain-level scoring metric instead. This: + +- Removes ~15 arbitrary constraint thresholds from seed data +- Lets more combos through to scoring for nuanced evaluation +- Creates a metric that varies by combo (actuator provides vs. platform context) +- Replaces the broken "speed" heuristic with something directly computable from entity data diff --git a/LOGIC DOCS/README.md b/LOGIC DOCS/README.md new file mode 100644 index 0000000..a44794f --- /dev/null +++ b/LOGIC DOCS/README.md @@ -0,0 +1,5 @@ +# Logic Docs + +A series of thought experiments and design decisions made while developing PhysCom. Each document captures the reasoning behind a specific architectural or modeling choice — the "why" behind the code. + +These are living records of the design process, not formal specifications. They preserve context that would otherwise be lost between development sessions. diff --git a/src/physcom/engine/constraint_resolver.py b/src/physcom/engine/constraint_resolver.py index d8a950d..ee19f6b 100644 --- a/src/physcom/engine/constraint_resolver.py +++ b/src/physcom/engine/constraint_resolver.py @@ -41,7 +41,6 @@ class ConstraintResolver: self._check_requires_vs_excludes(all_deps, result) self._check_mutual_exclusion(all_deps, result) self._check_range_incompatibility(all_deps, result) - self._check_power_density(combination, result) self._check_energy_density(combination, result) self._check_unmet_requirements(all_deps, result) @@ -125,35 +124,6 @@ class ConstraintResolver: f"but {max_name} limits {key} <= {max_val}" ) - def _check_power_density( - self, combination: Combination, result: ConstraintResult - ) -> None: - """Rule 4: If power source W/kg << platform required W/kg → warn/block.""" - density_provided: list[tuple[str, float]] = [] - density_required: list[tuple[str, float]] = [] - - for entity in combination.entities: - for dep in entity.dependencies: - if dep.key == "power_density_w_kg" and dep.constraint_type == "provides": - density_provided.append((entity.name, float(dep.value))) - elif dep.key == "power_density_required_w_kg" and dep.constraint_type == "range_min": - density_required.append((entity.name, float(dep.value))) - - for req_name, req_density in density_required: - for prov_name, prov_density in density_provided: - if prov_density < req_density * 0.01: - result.violations.append( - f"{prov_name} provides {prov_density} W/kg but " - f"{req_name} requires {req_density} W/kg " - f"(power density deficit > 100x)" - ) - elif prov_density < req_density: - result.warnings.append( - f"{prov_name} provides {prov_density} W/kg but " - f"{req_name} requires {req_density} W/kg " - f"(under-powered)" - ) - def _check_energy_density( self, combination: Combination, result: ConstraintResult ) -> None: @@ -166,9 +136,9 @@ class ConstraintResolver: for entity in combination.entities: for dep in entity.dependencies: - if dep.key == "energy_density_wh_kg" and dep.constraint_type == "provides": + if dep.key == "energy_density" and dep.constraint_type == "provides": density_provided.append((entity.name, float(dep.value))) - elif dep.key == "energy_density_wh_kg" and dep.constraint_type == "range_min": + elif dep.key == "energy_density" and dep.constraint_type == "range_min": density_required.append((entity.name, float(dep.value))) for req_name, req_density in density_required: @@ -177,14 +147,14 @@ class ConstraintResolver: for prov_name, prov_density in density_provided: if prov_density < req_density * 0.25: result.violations.append( - f"{prov_name} provides {prov_density:.0f} Wh/kg but " - f"{req_name} requires {req_density:.0f} Wh/kg " + f"{prov_name} provides {prov_density:.0f} J/kg but " + f"{req_name} requires {req_density:.0f} J/kg " f"(energy density deficit > 4x)" ) elif prov_density < req_density: result.warnings.append( - f"{prov_name} provides {prov_density:.0f} Wh/kg but " - f"{req_name} requires {req_density:.0f} Wh/kg " + f"{prov_name} provides {prov_density:.0f} J/kg but " + f"{req_name} requires {req_density:.0f} J/kg " f"(under-density)" ) diff --git a/src/physcom/engine/pipeline.py b/src/physcom/engine/pipeline.py index a201dde..1b20627 100644 --- a/src/physcom/engine/pipeline.py +++ b/src/physcom/engine/pipeline.py @@ -414,28 +414,27 @@ class Pipeline: def _stub_estimate( self, combo: Combination, metric_names: list[str] ) -> dict[str, float]: - """Simple heuristic estimation from dependency data.""" + """Simple heuristic estimation from dependency data (all values in SI base units).""" raw: dict[str, float] = {m: 0.0 for m in metric_names} # Extract intrinsic properties from entities power_density = 0.0 # W/kg - energy_density = 0.0 # Wh/kg - mass_kg = 100.0 # default + energy_density = 0.0 # J/kg + mass = 100.0 # kg, default for entity in combo.entities: for dep in entity.dependencies: - if dep.key == "power_density_w_kg" and dep.constraint_type == "provides": + if dep.key == "power_density" and dep.constraint_type == "provides": power_density = max(power_density, float(dep.value)) - if dep.key == "energy_density_wh_kg" and dep.constraint_type == "provides": + if dep.key == "energy_density" and dep.constraint_type == "provides": energy_density = max(energy_density, float(dep.value)) - if dep.key == "mass_kg" and dep.constraint_type == "range_min": - mass_kg = max(mass_kg, float(dep.value)) + if dep.key == "mass" and dep.constraint_type == "range_min": + mass = max(mass, float(dep.value)) - # Rough speed estimate: higher power density → faster - if "speed" in raw: - raw["speed"] = min(power_density * 0.5, 300000) + if "power_density" in raw: + raw["power_density"] = power_density if "cost_efficiency" in raw: - raw["cost_efficiency"] = max(0.01, 2.0 - power_density / 1000) + raw["cost_efficiency"] = max(1e-5, 2e-3 - power_density / 1e6) if "safety" in raw: raw["safety"] = 0.5 @@ -444,19 +443,19 @@ class Pipeline: raw["availability"] = 0.5 if "range_fuel" in raw: - raw["range_fuel"] = min(energy_density * 10, 1e10) + raw["range_fuel"] = min(energy_density * 2.78, 1e13) if "range_degradation" in raw: - raw["range_degradation"] = 365 + raw["range_degradation"] = 365 * 86400 if "cargo_capacity" in raw: - raw["cargo_capacity"] = mass_kg * 0.5 + raw["cargo_capacity"] = mass * 500 if "cargo_capacity_kg" in raw: - raw["cargo_capacity_kg"] = mass_kg * 0.3 + raw["cargo_capacity_kg"] = mass * 0.3 if "environmental_impact" in raw: - raw["environmental_impact"] = max(0.0, power_density * 0.2) + raw["environmental_impact"] = max(0.0, power_density * 2e-7) if "reliability" in raw: raw["reliability"] = 0.5 diff --git a/src/physcom/llm/prompts.py b/src/physcom/llm/prompts.py index b7e03de..ec6ca73 100644 --- a/src/physcom/llm/prompts.py +++ b/src/physcom/llm/prompts.py @@ -14,7 +14,7 @@ estimate the requested metrics using order-of-magnitude physics reasoning. - Use real-world physics to estimate each metric. - If the concept is implausible, still provide your best estimate. - Return ONLY valid JSON mapping metric names to numeric values. -- Example: {{"speed": 45.0, "cost_efficiency": 0.15, "safety": 0.7}} +- Example: {{"power_density": 500.0, "cost_efficiency": 0.15, "safety": 0.7}} """ PLAUSIBILITY_REVIEW_PROMPT = """\ diff --git a/src/physcom/seed/transport_example.py b/src/physcom/seed/transport_example.py index 1a0ace3..71e8f61 100644 --- a/src/physcom/seed/transport_example.py +++ b/src/physcom/seed/transport_example.py @@ -16,11 +16,10 @@ GROUND_PLATFORMS: list[Entity] = [ dependencies=[ Dependency("environment", "ground_surface", "true", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "50", "m²", "range_max"), - Dependency("physical", "footprint_m2", "0.5", "m²", "range_min"), - Dependency("physical", "mass_kg", "36000", "kg", "range_max"), - Dependency("physical", "mass_kg", "50", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "5", "W/kg", "range_min"), + Dependency("physical", "footprint", "50", "m²", "range_max"), + Dependency("physical", "footprint", "0.5", "m²", "range_min"), + Dependency("physical", "mass", "36000", "kg", "range_max"), + Dependency("physical", "mass", "50", "kg", "range_min"), Dependency("infrastructure", "road_network", "true", None, "requires"), Dependency("environment", "medium", "ground", None, "requires"), ], @@ -32,30 +31,14 @@ GROUND_PLATFORMS: list[Entity] = [ dependencies=[ Dependency("environment", "ground_surface", "true", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "3", "m²", "range_max"), - Dependency("physical", "footprint_m2", "0.3", "m²", "range_min"), - Dependency("physical", "mass_kg", "60", "kg", "range_max"), - Dependency("physical", "mass_kg", "5", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "1", "W/kg", "range_min"), + Dependency("physical", "footprint", "3", "m²", "range_max"), + Dependency("physical", "footprint", "0.3", "m²", "range_min"), + Dependency("physical", "mass", "60", "kg", "range_max"), + Dependency("physical", "mass", "5", "kg", "range_min"), Dependency("infrastructure", "road_network", "true", None, "requires"), Dependency("environment", "medium", "ground", None, "requires"), ], ), - Entity( - name="Walking", - dimension="platform", - description="Bipedal locomotion", - dependencies=[ - Dependency("environment", "ground_surface", "true", None, "requires"), - Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "0.8", "m²", "range_max"), - Dependency("physical", "footprint_m2", "0.3", "m²", "range_min"), - Dependency("physical", "mass_kg", "150", "kg", "range_max"), - Dependency("force", "power_density_required_w_kg", "1", "W/kg", "range_min"), - Dependency("infrastructure", "fuel_infrastructure", "none", None, "requires"), - Dependency("environment", "medium", "ground", None, "requires"), - ], - ), Entity( name="Rail Vehicle", dimension="platform", @@ -63,30 +46,14 @@ GROUND_PLATFORMS: list[Entity] = [ dependencies=[ Dependency("environment", "ground_surface", "true", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "200", "m²", "range_max"), - Dependency("physical", "footprint_m2", "20", "m²", "range_min"), - Dependency("physical", "mass_kg", "40000", "kg", "range_max"), - Dependency("physical", "mass_kg", "10000", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "10", "W/kg", "range_min"), + Dependency("physical", "footprint", "200", "m²", "range_max"), + Dependency("physical", "footprint", "20", "m²", "range_min"), + Dependency("physical", "mass", "40000", "kg", "range_max"), + Dependency("physical", "mass", "10000", "kg", "range_min"), Dependency("infrastructure", "rail_network", "true", None, "requires"), Dependency("environment", "medium", "ground", None, "requires"), ], ), - Entity( - name="Animal Transport", - dimension="platform", - description="Animal-powered riding or pulling — horses, dog sleds", - dependencies=[ - Dependency("environment", "ground_surface", "true", None, "requires"), - Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "5", "m²", "range_max"), - Dependency("physical", "footprint_m2", "1", "m²", "range_min"), - Dependency("physical", "mass_kg", "700", "kg", "range_max"), - Dependency("physical", "mass_kg", "100", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "1", "W/kg", "range_min"), - Dependency("environment", "medium", "ground", None, "requires"), - ], - ), ] @@ -100,11 +67,10 @@ WATER_PLATFORMS: list[Entity] = [ dependencies=[ Dependency("environment", "water_surface", "true", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "2000", "m²", "range_max"), - Dependency("physical", "footprint_m2", "2", "m²", "range_min"), - Dependency("physical", "mass_kg", "100000", "kg", "range_max"), - Dependency("physical", "mass_kg", "30", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "2", "W/kg", "range_min"), + Dependency("physical", "footprint", "2000", "m²", "range_max"), + Dependency("physical", "footprint", "2", "m²", "range_min"), + Dependency("physical", "mass", "100000", "kg", "range_max"), + Dependency("physical", "mass", "30", "kg", "range_min"), Dependency("environment", "medium", "water", None, "requires"), ], ), @@ -115,12 +81,11 @@ WATER_PLATFORMS: list[Entity] = [ dependencies=[ Dependency("environment", "water_surface", "true", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "200", "m²", "range_max"), - Dependency("physical", "footprint_m2", "20", "m²", "range_min"), - Dependency("physical", "mass_kg", "10000", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "20", "W/kg", "range_min"), + Dependency("physical", "footprint", "200", "m²", "range_max"), + Dependency("physical", "footprint", "20", "m²", "range_min"), + Dependency("physical", "mass", "10000", "kg", "range_min"), Dependency("environment", "medium", "water", None, "requires"), - Dependency("physical", "energy_density_wh_kg", "200", "Wh/kg", "range_min"), + Dependency("physical", "energy_density", "720000", "J/kg", "range_min"), ], ), ] @@ -136,14 +101,13 @@ AIR_PLATFORMS: list[Entity] = [ dependencies=[ Dependency("environment", "atmosphere", "standard", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "500", "m²", "range_max"), - Dependency("physical", "footprint_m2", "10", "m²", "range_min"), - Dependency("physical", "mass_kg", "100000", "kg", "range_max"), - Dependency("physical", "mass_kg", "500", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "100", "W/kg", "range_min"), + Dependency("physical", "footprint", "500", "m²", "range_max"), + Dependency("physical", "footprint", "10", "m²", "range_min"), + Dependency("physical", "mass", "100000", "kg", "range_max"), + Dependency("physical", "mass", "500", "kg", "range_min"), Dependency("infrastructure", "runway", "true", None, "requires"), Dependency("environment", "medium", "air", None, "requires"), - Dependency("physical", "energy_density_wh_kg", "400", "Wh/kg", "range_min"), + Dependency("physical", "energy_density", "1440000", "J/kg", "range_min"), ], ), Entity( @@ -153,13 +117,12 @@ AIR_PLATFORMS: list[Entity] = [ dependencies=[ Dependency("environment", "atmosphere", "standard", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "20", "m²", "range_max"), - Dependency("physical", "footprint_m2", "0.5", "m²", "range_min"), - Dependency("physical", "mass_kg", "5000", "kg", "range_max"), - Dependency("physical", "mass_kg", "1", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "80", "W/kg", "range_min"), + Dependency("physical", "footprint", "20", "m²", "range_max"), + Dependency("physical", "footprint", "0.5", "m²", "range_min"), + Dependency("physical", "mass", "5000", "kg", "range_max"), + Dependency("physical", "mass", "1", "kg", "range_min"), Dependency("environment", "medium", "air", None, "requires"), - Dependency("physical", "energy_density_wh_kg", "200", "Wh/kg", "range_min"), + Dependency("physical", "energy_density", "720000", "J/kg", "range_min"), ], ), Entity( @@ -169,11 +132,10 @@ AIR_PLATFORMS: list[Entity] = [ dependencies=[ Dependency("environment", "atmosphere", "standard", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "1000", "m²", "range_max"), - Dependency("physical", "footprint_m2", "50", "m²", "range_min"), - Dependency("physical", "mass_kg", "20000", "kg", "range_max"), - Dependency("physical", "mass_kg", "200", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "5", "W/kg", "range_min"), + Dependency("physical", "footprint", "1000", "m²", "range_max"), + Dependency("physical", "footprint", "50", "m²", "range_min"), + Dependency("physical", "mass", "20000", "kg", "range_max"), + Dependency("physical", "mass", "200", "kg", "range_min"), Dependency("environment", "medium", "air", None, "requires"), ], ), @@ -184,11 +146,10 @@ AIR_PLATFORMS: list[Entity] = [ dependencies=[ Dependency("environment", "atmosphere", "standard", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "20", "m²", "range_max"), - Dependency("physical", "footprint_m2", "5", "m²", "range_min"), - Dependency("physical", "mass_kg", "600", "kg", "range_max"), - Dependency("physical", "mass_kg", "5", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "1", "W/kg", "range_min"), + Dependency("physical", "footprint", "20", "m²", "range_max"), + Dependency("physical", "footprint", "5", "m²", "range_min"), + Dependency("physical", "mass", "600", "kg", "range_max"), + Dependency("physical", "mass", "5", "kg", "range_min"), Dependency("infrastructure", "tow_or_winch", "true", None, "requires"), Dependency("environment", "medium", "air", None, "requires"), ], @@ -205,13 +166,12 @@ SPACE_PLATFORMS: list[Entity] = [ description="Vehicle designed for space travel", dependencies=[ Dependency("environment", "atmosphere", "vacuum_or_thin", None, "requires"), - Dependency("physical", "footprint_m2", "500", "m²", "range_max"), - Dependency("physical", "footprint_m2", "10", "m²", "range_min"), - Dependency("physical", "mass_kg", "5000", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "300", "W/kg", "range_min"), + Dependency("physical", "footprint", "500", "m²", "range_max"), + Dependency("physical", "footprint", "10", "m²", "range_min"), + Dependency("physical", "mass", "5000", "kg", "range_min"), Dependency("infrastructure", "launch_facility", "true", None, "requires"), Dependency("environment", "medium", "space", None, "requires"), - Dependency("physical", "energy_density_wh_kg", "2000", "Wh/kg", "range_min"), + Dependency("physical", "energy_density", "7200000", "J/kg", "range_min"), ], ), ] @@ -226,11 +186,10 @@ MULTI_PLATFORMS: list[Entity] = [ description="Vehicle capable of operation on land, water, or both", dependencies=[ Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "100", "m²", "range_max"), - Dependency("physical", "footprint_m2", "5", "m²", "range_min"), - Dependency("physical", "mass_kg", "10000", "kg", "range_max"), - Dependency("physical", "mass_kg", "1500", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "20", "W/kg", "range_min"), + Dependency("physical", "footprint", "100", "m²", "range_max"), + Dependency("physical", "footprint", "5", "m²", "range_min"), + Dependency("physical", "mass", "10000", "kg", "range_max"), + Dependency("physical", "mass", "1500", "kg", "range_min"), ], ), ] @@ -244,10 +203,9 @@ FICTIONAL_PLATFORMS: list[Entity] = [ dimension="platform", description="Hypothetical matter transmission device", dependencies=[ - Dependency("physical", "footprint_m2", "10", "m²", "range_max"), - Dependency("physical", "footprint_m2", "1", "m²", "range_min"), - Dependency("physical", "mass_kg", "0", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "1000", "W/kg", "range_min"), + Dependency("physical", "footprint", "10", "m²", "range_max"), + Dependency("physical", "footprint", "1", "m²", "range_min"), + Dependency("physical", "mass", "0", "kg", "range_min"), Dependency("infrastructure", "teleport_network", "true", None, "requires"), ], ), @@ -258,11 +216,10 @@ FICTIONAL_PLATFORMS: list[Entity] = [ dependencies=[ Dependency("environment", "ground_surface", "true", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), - Dependency("physical", "footprint_m2", "50", "m²", "range_max"), - Dependency("physical", "footprint_m2", "5", "m²", "range_min"), - Dependency("physical", "mass_kg", "20000", "kg", "range_max"), - Dependency("physical", "mass_kg", "5000", "kg", "range_min"), - Dependency("force", "power_density_required_w_kg", "80", "W/kg", "range_min"), + Dependency("physical", "footprint", "50", "m²", "range_max"), + Dependency("physical", "footprint", "5", "m²", "range_min"), + Dependency("physical", "mass", "20000", "kg", "range_max"), + Dependency("physical", "mass", "5000", "kg", "range_min"), Dependency("infrastructure", "hyperloop_tube", "true", None, "requires"), Dependency("environment", "medium", "ground", None, "requires"), ], @@ -292,9 +249,9 @@ COMBUSTION_ACTUATORS: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "chemical_combustible", None, "requires"), Dependency("environment", "atmosphere", "standard", None, "requires"), - Dependency("physical", "mass_kg", "45", "kg", "range_min"), + Dependency("physical", "mass", "45", "kg", "range_min"), Dependency("force", "thrust_profile", "high_continuous", None, "provides"), - Dependency("force", "power_density_w_kg", "1000", "W/kg", "provides"), + Dependency("force", "power_density", "1000", "W/kg", "provides"), ], ), Entity( @@ -304,9 +261,9 @@ COMBUSTION_ACTUATORS: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "chemical_combustible", None, "requires"), Dependency("environment", "atmosphere", "standard", None, "requires"), - Dependency("physical", "mass_kg", "200", "kg", "range_min"), + Dependency("physical", "mass", "200", "kg", "range_min"), Dependency("force", "thrust_profile", "extreme_continuous", None, "provides"), - Dependency("force", "power_density_w_kg", "5000", "W/kg", "provides"), + Dependency("force", "power_density", "5000", "W/kg", "provides"), ], ), Entity( @@ -316,9 +273,9 @@ COMBUSTION_ACTUATORS: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "chemical_combustible", None, "requires"), Dependency("environment", "atmosphere", "standard", None, "requires"), - Dependency("physical", "mass_kg", "300", "kg", "range_min"), + Dependency("physical", "mass", "300", "kg", "range_min"), Dependency("force", "thrust_profile", "high_continuous", None, "provides"), - Dependency("force", "power_density_w_kg", "30", "W/kg", "provides"), + Dependency("force", "power_density", "30", "W/kg", "provides"), ], ), ] @@ -333,9 +290,9 @@ ELECTRIC_ACTUATORS: list[Entity] = [ description="Rotary motor converting electrical energy to mechanical motion", dependencies=[ Dependency("energy", "energy_form", "electrical", None, "requires"), - Dependency("physical", "mass_kg", "5", "kg", "range_min"), + Dependency("physical", "mass", "5", "kg", "range_min"), Dependency("force", "thrust_profile", "moderate_continuous", None, "provides"), - Dependency("force", "power_density_w_kg", "2000", "W/kg", "provides"), + Dependency("force", "power_density", "2000", "W/kg", "provides"), ], ), ] @@ -350,9 +307,9 @@ BIOLOGICAL_ACTUATORS: list[Entity] = [ description="Human-powered via pedalling, pushing, or rowing", dependencies=[ Dependency("energy", "energy_form", "biological", None, "requires"), - Dependency("physical", "mass_kg", "0", "kg", "range_min"), + Dependency("physical", "mass", "0", "kg", "range_min"), Dependency("force", "thrust_profile", "low_continuous", None, "provides"), - Dependency("force", "power_density_w_kg", "1.5", "W/kg", "provides"), + Dependency("force", "power_density", "1.5", "W/kg", "provides"), ], ), Entity( @@ -362,9 +319,9 @@ BIOLOGICAL_ACTUATORS: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "biological", None, "requires"), Dependency("environment", "ground_surface", "true", None, "requires"), - Dependency("physical", "mass_kg", "0", "kg", "range_min"), + Dependency("physical", "mass", "0", "kg", "range_min"), Dependency("force", "thrust_profile", "low_continuous", None, "provides"), - Dependency("force", "power_density_w_kg", "2", "W/kg", "provides"), + Dependency("force", "power_density", "2", "W/kg", "provides"), ], ), ] @@ -381,9 +338,9 @@ RENEWABLE_ACTUATORS: list[Entity] = [ Dependency("energy", "energy_form", "radiation_pressure", None, "requires"), Dependency("environment", "atmosphere", "vacuum_or_thin", None, "requires"), Dependency("environment", "star_proximity", "true", None, "requires"), - Dependency("physical", "surface_area_m2", "100", "m^2", "range_min"), + Dependency("physical", "surface_area", "100", "m²", "range_min"), Dependency("force", "thrust_profile", "continuous_low", None, "provides"), - Dependency("force", "power_density_w_kg", "0.01", "W/kg", "provides"), + Dependency("force", "power_density", "0.01", "W/kg", "provides"), Dependency("environment", "medium", "space", None, "requires"), ], ), @@ -394,9 +351,9 @@ RENEWABLE_ACTUATORS: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "wind", None, "requires"), Dependency("environment", "atmosphere", "standard", None, "requires"), - Dependency("physical", "mass_kg", "15", "kg", "range_min"), + Dependency("physical", "mass", "15", "kg", "range_min"), Dependency("force", "thrust_profile", "low_continuous", None, "provides"), - Dependency("force", "power_density_w_kg", "10", "W/kg", "provides"), + Dependency("force", "power_density", "10", "W/kg", "provides"), ], ), ] @@ -411,9 +368,9 @@ ROCKET_ACTUATORS: list[Entity] = [ description="Thrust from expanding combustion gases through a nozzle", dependencies=[ Dependency("energy", "energy_form", "chemical_propellant", None, "requires"), - Dependency("physical", "mass_kg", "150", "kg", "range_min"), + Dependency("physical", "mass", "150", "kg", "range_min"), Dependency("force", "thrust_profile", "extreme_burst", None, "provides"), - Dependency("force", "power_density_w_kg", "10000", "W/kg", "provides"), + Dependency("force", "power_density", "10000", "W/kg", "provides"), ], ), Entity( @@ -424,9 +381,9 @@ ROCKET_ACTUATORS: list[Entity] = [ Dependency("energy", "energy_form", "ion_propellant", None, "requires"), Dependency("environment", "atmosphere", "vacuum_or_thin", None, "requires"), Dependency("environment", "medium", "space", None, "requires"), - Dependency("physical", "mass_kg", "8", "kg", "range_min"), + Dependency("physical", "mass", "8", "kg", "range_min"), Dependency("force", "thrust_profile", "continuous_low", None, "provides"), - Dependency("force", "power_density_w_kg", "30", "W/kg", "provides"), + Dependency("force", "power_density", "30", "W/kg", "provides"), ], ), Entity( @@ -435,9 +392,9 @@ ROCKET_ACTUATORS: list[Entity] = [ description="Nuclear fission reactor heating propellant for thrust", dependencies=[ Dependency("energy", "energy_form", "nuclear_thermal", None, "requires"), - Dependency("physical", "mass_kg", "1500", "kg", "range_min"), + Dependency("physical", "mass", "1500", "kg", "range_min"), Dependency("force", "thrust_profile", "extreme_continuous", None, "provides"), - Dependency("force", "power_density_w_kg", "50", "W/kg", "provides"), + Dependency("force", "power_density", "50", "W/kg", "provides"), Dependency("material", "radiation_shielding", "true", None, "requires"), ], ), @@ -453,9 +410,9 @@ EXOTIC_ACTUATORS: list[Entity] = [ description="Propulsion via sequential cannon blasts", dependencies=[ Dependency("energy", "energy_form", "chemical_explosive", None, "requires"), - Dependency("physical", "mass_kg", "80", "kg", "range_min"), + Dependency("physical", "mass", "80", "kg", "range_min"), Dependency("force", "thrust_profile", "high_burst", None, "provides"), - Dependency("force", "power_density_w_kg", "3000", "W/kg", "provides"), + Dependency("force", "power_density", "3000", "W/kg", "provides"), ], ), Entity( @@ -464,9 +421,9 @@ EXOTIC_ACTUATORS: list[Entity] = [ description="Motor powered by expanding compressed air", dependencies=[ Dependency("energy", "energy_form", "pneumatic", None, "requires"), - Dependency("physical", "mass_kg", "10", "kg", "range_min"), + Dependency("physical", "mass", "10", "kg", "range_min"), Dependency("force", "thrust_profile", "moderate_continuous", None, "provides"), - Dependency("force", "power_density_w_kg", "100", "W/kg", "provides"), + Dependency("force", "power_density", "100", "W/kg", "provides"), ], ), Entity( @@ -475,9 +432,9 @@ EXOTIC_ACTUATORS: list[Entity] = [ description="Mechanical drive transferring kinetic energy from a spinning rotor", dependencies=[ Dependency("energy", "energy_form", "kinetic_stored", None, "requires"), - Dependency("physical", "mass_kg", "20", "kg", "range_min"), + Dependency("physical", "mass", "20", "kg", "range_min"), Dependency("force", "thrust_profile", "high_burst", None, "provides"), - Dependency("force", "power_density_w_kg", "2000", "W/kg", "provides"), + Dependency("force", "power_density", "2000", "W/kg", "provides"), ], ), Entity( @@ -488,9 +445,9 @@ EXOTIC_ACTUATORS: list[Entity] = [ Dependency("energy", "energy_form", "gravitational", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), Dependency("environment", "ground_surface", "true", None, "requires"), - Dependency("physical", "mass_kg", "0", "kg", "range_min"), + Dependency("physical", "mass", "0", "kg", "range_min"), Dependency("force", "thrust_profile", "low_continuous", None, "provides"), - Dependency("force", "power_density_w_kg", "10", "W/kg", "provides"), + Dependency("force", "power_density", "10", "W/kg", "provides"), ], ), ] @@ -518,8 +475,8 @@ COMBUSTIBLE_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "chemical_combustible", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "fuel_station", None, "requires"), - Dependency("physical", "mass_kg", "10", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "1300", "Wh/kg", "provides"), + Dependency("physical", "mass", "10", "kg", "range_min"), + Dependency("physical", "energy_density", "4680000", "J/kg", "provides"), ], ), Entity( @@ -529,8 +486,8 @@ COMBUSTIBLE_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "chemical_combustible", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "hydrogen_station", None, "requires"), - Dependency("physical", "mass_kg", "5", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "600", "Wh/kg", "provides"), + Dependency("physical", "mass", "5", "kg", "range_min"), + Dependency("physical", "energy_density", "2160000", "J/kg", "provides"), ], ), Entity( @@ -540,8 +497,8 @@ COMBUSTIBLE_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "chemical_combustible", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "cng_station", None, "requires"), - Dependency("physical", "mass_kg", "15", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "800", "Wh/kg", "provides"), + Dependency("physical", "mass", "15", "kg", "range_min"), + Dependency("physical", "energy_density", "2880000", "J/kg", "provides"), ], ), Entity( @@ -551,8 +508,8 @@ COMBUSTIBLE_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "chemical_combustible", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "coal_supply", None, "requires"), - Dependency("physical", "mass_kg", "100", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "400", "Wh/kg", "provides"), + Dependency("physical", "mass", "100", "kg", "range_min"), + Dependency("physical", "energy_density", "1440000", "J/kg", "provides"), ], ), Entity( @@ -562,8 +519,8 @@ COMBUSTIBLE_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "chemical_combustible", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "jet_fuel", None, "requires"), - Dependency("physical", "mass_kg", "50", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "1200", "Wh/kg", "provides"), + Dependency("physical", "mass", "50", "kg", "range_min"), + Dependency("physical", "energy_density", "4320000", "J/kg", "provides"), ], ), ] @@ -579,8 +536,8 @@ ELECTRICAL_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "electrical", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "charging_station", None, "requires"), - Dependency("physical", "mass_kg", "9", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "300", "Wh/kg", "provides"), + Dependency("physical", "mass", "9", "kg", "range_min"), + Dependency("physical", "energy_density", "1080000", "J/kg", "provides"), ], ), Entity( @@ -590,8 +547,8 @@ ELECTRICAL_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "electrical", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "charging_station", None, "requires"), - Dependency("physical", "mass_kg", "15", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "10", "Wh/kg", "provides"), + Dependency("physical", "mass", "15", "kg", "range_min"), + Dependency("physical", "energy_density", "36000", "J/kg", "provides"), ], ), Entity( @@ -602,8 +559,8 @@ ELECTRICAL_STORAGE: list[Entity] = [ Dependency("energy", "energy_form", "electrical", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "none", None, "requires"), Dependency("environment", "star_proximity", "true", None, "requires"), - Dependency("physical", "mass_kg", "10", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "50", "Wh/kg", "provides"), + Dependency("physical", "mass", "10", "kg", "range_min"), + Dependency("physical", "energy_density", "180000", "J/kg", "provides"), ], ), ] @@ -619,8 +576,8 @@ BIOLOGICAL_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "biological", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "none", None, "requires"), - Dependency("physical", "mass_kg", "0", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "200", "Wh/kg", "provides"), + Dependency("physical", "mass", "0", "kg", "range_min"), + Dependency("physical", "energy_density", "720000", "J/kg", "provides"), ], ), ] @@ -637,7 +594,7 @@ AMBIENT_STORAGE: list[Entity] = [ Dependency("energy", "energy_form", "radiation_pressure", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "none", None, "requires"), Dependency("environment", "star_proximity", "true", None, "requires"), - Dependency("physical", "mass_kg", "0", "kg", "range_min"), + Dependency("physical", "mass", "0", "kg", "range_min"), ], ), Entity( @@ -648,7 +605,7 @@ AMBIENT_STORAGE: list[Entity] = [ Dependency("energy", "energy_form", "wind", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "none", None, "requires"), Dependency("environment", "atmosphere", "standard", None, "requires"), - Dependency("physical", "mass_kg", "0", "kg", "range_min"), + Dependency("physical", "mass", "0", "kg", "range_min"), ], ), ] @@ -664,8 +621,8 @@ PROPELLANT_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "chemical_propellant", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "solid_propellant", None, "requires"), - Dependency("physical", "mass_kg", "50", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "500", "Wh/kg", "provides"), + Dependency("physical", "mass", "50", "kg", "range_min"), + Dependency("physical", "energy_density", "1800000", "J/kg", "provides"), ], ), Entity( @@ -675,8 +632,8 @@ PROPELLANT_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "ion_propellant", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "xenon_propellant", None, "requires"), - Dependency("physical", "mass_kg", "2", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "3000", "Wh/kg", "provides"), + Dependency("physical", "mass", "2", "kg", "range_min"), + Dependency("physical", "energy_density", "10800000", "J/kg", "provides"), ], ), Entity( @@ -686,8 +643,8 @@ PROPELLANT_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "nuclear_thermal", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "nuclear_fuel", None, "requires"), - Dependency("physical", "mass_kg", "500", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "500000", "Wh/kg", "provides"), + Dependency("physical", "mass", "500", "kg", "range_min"), + Dependency("physical", "energy_density", "1800000000", "J/kg", "provides"), Dependency("material", "radiation_shielding", "true", None, "requires"), ], ), @@ -704,8 +661,8 @@ EXOTIC_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "chemical_explosive", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "ammunition", None, "requires"), - Dependency("physical", "mass_kg", "20", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "200", "Wh/kg", "provides"), + Dependency("physical", "mass", "20", "kg", "range_min"), + Dependency("physical", "energy_density", "720000", "J/kg", "provides"), ], ), Entity( @@ -715,8 +672,8 @@ EXOTIC_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "pneumatic", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "compressed_air_station", None, "requires"), - Dependency("physical", "mass_kg", "10", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "30", "Wh/kg", "provides"), + Dependency("physical", "mass", "10", "kg", "range_min"), + Dependency("physical", "energy_density", "108000", "J/kg", "provides"), ], ), Entity( @@ -726,8 +683,8 @@ EXOTIC_STORAGE: list[Entity] = [ dependencies=[ Dependency("energy", "energy_form", "kinetic_stored", None, "provides"), Dependency("infrastructure", "fuel_infrastructure", "charging_station", None, "requires"), - Dependency("physical", "mass_kg", "30", "kg", "range_min"), - Dependency("physical", "energy_density_wh_kg", "50", "Wh/kg", "provides"), + Dependency("physical", "mass", "30", "kg", "range_min"), + Dependency("physical", "energy_density", "180000", "J/kg", "provides"), ], ), Entity( @@ -739,7 +696,7 @@ EXOTIC_STORAGE: list[Entity] = [ Dependency("infrastructure", "fuel_infrastructure", "none", None, "requires"), Dependency("environment", "gravity", "true", None, "requires"), Dependency("environment", "ground_surface", "true", None, "requires"), - Dependency("physical", "mass_kg", "0", "kg", "range_min"), + Dependency("physical", "mass", "0", "kg", "range_min"), ], ), ] @@ -763,11 +720,11 @@ URBAN_COMMUTING = Domain( name="urban_commuting", description="Daily travel within a city, 1-50km range", metric_bounds=[ - MetricBound("speed", weight=0.25, norm_min=5, norm_max=120, unit="km/h"), - MetricBound("cost_efficiency", weight=0.25, norm_min=0.01, norm_max=2.0, unit="$/km", lower_is_better=True), + 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("safety", weight=0.25, norm_min=0.0, norm_max=1.0, unit="0-1"), MetricBound("availability", weight=0.15, norm_min=0.0, norm_max=1.0, unit="0-1"), - MetricBound("range_fuel", weight=0.10, norm_min=5, norm_max=500, unit="km"), + MetricBound("range_fuel", weight=0.10, norm_min=5000, norm_max=500000, unit="m"), ], ) @@ -775,11 +732,11 @@ INTERPLANETARY = Domain( name="interplanetary_travel", description="Travel between planets within a solar system", metric_bounds=[ - MetricBound("speed", weight=0.30, norm_min=1000, norm_max=300000, unit="km/s"), - MetricBound("range_fuel", weight=0.30, norm_min=1e6, norm_max=1e10, unit="km"), + MetricBound("power_density", weight=0.30, norm_min=10, norm_max=10000, unit="W/kg"), + MetricBound("range_fuel", weight=0.30, norm_min=1e9, norm_max=1e13, unit="m"), MetricBound("safety", weight=0.20, norm_min=0.0, norm_max=1.0, unit="0-1"), - MetricBound("cost_efficiency", weight=0.10, norm_min=1e3, norm_max=1e9, unit="$/km", lower_is_better=True), - MetricBound("range_degradation", weight=0.10, norm_min=100, norm_max=36500, unit="days"), + MetricBound("cost_efficiency", weight=0.10, norm_min=1.0, norm_max=1e6, unit="$/m", lower_is_better=True), + MetricBound("range_degradation", weight=0.10, norm_min=8640000, norm_max=3.1536e9, unit="s"), ], ) @@ -787,11 +744,11 @@ MARITIME_SHIPPING = Domain( name="maritime_shipping", description="Ocean cargo transport between ports, 100-40000km range", metric_bounds=[ - MetricBound("speed", weight=0.15, norm_min=5, norm_max=60, unit="km/h"), - MetricBound("cargo_capacity", weight=0.25, norm_min=1, norm_max=200000, unit="tons"), - MetricBound("cost_efficiency", weight=0.25, norm_min=0.001, norm_max=1.0, unit="$/ton-km", lower_is_better=True), + MetricBound("power_density", weight=0.15, norm_min=1, norm_max=1000, unit="W/kg"), + MetricBound("cargo_capacity", weight=0.25, norm_min=1000, norm_max=2e8, unit="kg"), + MetricBound("cost_efficiency", weight=0.25, norm_min=1e-9, norm_max=1e-6, unit="$/(kg\u00b7m)", lower_is_better=True), MetricBound("safety", weight=0.20, norm_min=0.0, norm_max=1.0, unit="0-1"), - MetricBound("range_fuel", weight=0.15, norm_min=100, norm_max=40000, unit="km"), + MetricBound("range_fuel", weight=0.15, norm_min=100000, norm_max=40000000, unit="m"), ], ) @@ -799,11 +756,11 @@ LAST_MILE_DELIVERY = Domain( name="last_mile_delivery", description="Short-range package delivery within neighborhoods, 0.5-15km", metric_bounds=[ - MetricBound("speed", weight=0.25, norm_min=2, norm_max=60, unit="km/h"), - MetricBound("cost_efficiency", weight=0.30, norm_min=0.01, norm_max=5.0, unit="$/km", lower_is_better=True), + MetricBound("power_density", weight=0.25, norm_min=1, norm_max=500, unit="W/kg"), + MetricBound("cost_efficiency", weight=0.30, norm_min=1e-5, norm_max=5e-3, unit="$/m", lower_is_better=True), MetricBound("cargo_capacity_kg", weight=0.20, norm_min=1, norm_max=500, unit="kg"), MetricBound("safety", weight=0.15, norm_min=0.0, norm_max=1.0, unit="0-1"), - MetricBound("environmental_impact", weight=0.10, norm_min=0, norm_max=500, unit="g CO2/km", lower_is_better=True), + MetricBound("environmental_impact", weight=0.10, norm_min=0, norm_max=5e-4, unit="kg/m", lower_is_better=True), ], ) @@ -811,10 +768,10 @@ CROSS_COUNTRY_FREIGHT = Domain( name="cross_country_freight", description="Long-distance overland cargo transport, 200-5000km", metric_bounds=[ - MetricBound("speed", weight=0.20, norm_min=20, norm_max=200, unit="km/h"), - MetricBound("cargo_capacity", weight=0.25, norm_min=0.5, norm_max=100, unit="tons"), - MetricBound("cost_efficiency", weight=0.25, norm_min=0.01, norm_max=5.0, unit="$/ton-km", lower_is_better=True), - MetricBound("range_fuel", weight=0.20, norm_min=100, norm_max=5000, unit="km"), + MetricBound("power_density", weight=0.20, norm_min=5, norm_max=5000, unit="W/kg"), + MetricBound("cargo_capacity", weight=0.25, norm_min=500, norm_max=100000, unit="kg"), + MetricBound("cost_efficiency", weight=0.25, norm_min=1e-8, norm_max=5e-6, unit="$/(kg\u00b7m)", lower_is_better=True), + MetricBound("range_fuel", weight=0.20, norm_min=100000, norm_max=5000000, unit="m"), MetricBound("reliability", weight=0.10, norm_min=0.0, norm_max=1.0, unit="0-1"), ], ) diff --git a/src/physcom/units.py b/src/physcom/units.py new file mode 100644 index 0000000..402edb2 --- /dev/null +++ b/src/physcom/units.py @@ -0,0 +1,110 @@ +"""Unit-aware formatting: SI base units in DB → human-friendly display.""" + +from __future__ import annotations + +# Each rule list is sorted descending by threshold. +# format_quantity walks the list and picks the first entry where abs(value) >= threshold. +# A threshold of 0 is the fallback. +DISPLAY_RULES: dict[str, list[tuple[float, str, float]]] = { + # (threshold_in_si, display_unit, divisor_to_display) + "m": [ + (9.461e15, "ly", 9.461e15), + (1.496e11, "AU", 1.496e11), + (1e3, "km", 1e3), + (1, "m", 1), + (1e-2, "cm", 1e-2), + (0, "mm", 1e-3), + ], + "m/s": [ + (1e3, "km/s", 1e3), + (0.3, "km/h", 1 / 3.6), + (0, "m/s", 1), + ], + "m\u00b2": [ + (1e6, "km\u00b2", 1e6), + (1, "m\u00b2", 1), + (0, "cm\u00b2", 1e-4), + ], + "kg": [ + (1e3, "t", 1e3), + (1, "kg", 1), + (0, "g", 1e-3), + ], + "W/kg": [ + (1e3, "kW/kg", 1e3), + (0, "W/kg", 1), + ], + "J/kg": [ + (3.6e6, "kWh/kg", 3.6e6), + (3600, "Wh/kg", 3600), + (1e3, "kJ/kg", 1e3), + (0, "J/kg", 1), + ], + "s": [ + (3.156e7, "yr", 3.156e7), + (86400, "d", 86400), + (3600, "h", 3600), + (60, "min", 60), + (0, "s", 1), + ], + # Always display in human-friendly derived unit + "$/m": [ + (0, "$/km", 1e-3), + ], + "$/(kg\u00b7m)": [ + (0, "$/t\u00b7km", 1e-6), + ], + "kg/m": [ + (0, "g/km", 1e-6), + ], +} + + +def format_quantity(value: object, si_unit: str | None = None) -> str: + """Format a numeric value with its SI unit into a human-readable string. + + Returns e.g. "120 km/h" for (33.33, "m/s") or "1.0 ly" for (9.461e15, "m"). + """ + if value is None: + return "\u2014" + + # Coerce to float + if isinstance(value, str): + try: + num = float(value) + except (ValueError, TypeError): + return value + elif isinstance(value, (int, float)): + num = float(value) + else: + return str(value) + + import math + if math.isnan(num) or math.isinf(num): + return str(value) + + if not si_unit or si_unit == "0-1": + return _fmt_number(num) + + rules = DISPLAY_RULES.get(si_unit) + if rules is None: + # No display rules — just show number + unit + return f"{_fmt_number(num)} {si_unit}" + + abs_val = abs(num) + for threshold, display_unit, divisor in rules: + if abs_val >= threshold: + display_val = num / divisor + return f"{_fmt_number(display_val)} {display_unit}" + + # Fallback (shouldn't reach here if rules end with threshold=0) + return f"{_fmt_number(num)} {si_unit}" + + +def _fmt_number(num: float) -> str: + """Format a number concisely: drop trailing zeros, cap at 4 significant figures.""" + if num == 0: + return "0" + if num == int(num) and abs(num) < 1e6: + return str(int(num)) + return f"{num:.4g}" diff --git a/src/physcom_web/app.py b/src/physcom_web/app.py index c14e3bb..fa51182 100644 --- a/src/physcom_web/app.py +++ b/src/physcom_web/app.py @@ -11,6 +11,7 @@ from flask import Flask, g from physcom.db.schema import init_db from physcom.db.repository import Repository +from physcom.units import format_quantity DEFAULT_DB = Path("data/physcom.db") @@ -101,6 +102,7 @@ def create_app() -> Flask: app.secret_key = _load_or_generate_secret_key() app.jinja_env.filters["si"] = _si_format + app.jinja_env.filters["qty"] = format_quantity app.teardown_appcontext(close_db) diff --git a/src/physcom_web/templates/domains/_metrics_table.html b/src/physcom_web/templates/domains/_metrics_table.html index 6d830b2..4d52e26 100644 --- a/src/physcom_web/templates/domains/_metrics_table.html +++ b/src/physcom_web/templates/domains/_metrics_table.html @@ -16,8 +16,8 @@ {{ mb.metric_name }} {{ mb.unit or '—' }} {{ mb.weight }} - {{ mb.norm_min|si }} - {{ mb.norm_max|si }} + {{ mb.norm_min|qty(mb.unit) }} + {{ mb.norm_max|qty(mb.unit) }} {{ '↓ lower' if mb.lower_is_better else '↑ higher' }}