diff --git a/src/physcom/db/repository.py b/src/physcom/db/repository.py index 086365c..bdb10cf 100644 --- a/src/physcom/db/repository.py +++ b/src/physcom/db/repository.py @@ -606,13 +606,19 @@ class Repository: def count_combinations_by_status(self, domain_name: str | None = None) -> dict[str, int]: """Count combos by status. If domain_name given, only combos with results in that domain.""" if domain_name: + # combinations.status is domain-agnostic (a combo can be "valid" + # generically but blocked by one domain's own constraints), so a + # domain-scoped count must bucket domain_block_reason rows on + # their own rather than trusting c.status. rows = self.conn.execute( - """SELECT c.status, COUNT(*) as cnt + """SELECT CASE WHEN cr.domain_block_reason IS NOT NULL + THEN 'domain_blocked' ELSE c.status END as status, + COUNT(*) as cnt FROM combination_results cr JOIN combinations c ON cr.combination_id = c.id JOIN domains d ON cr.domain_id = d.id WHERE d.name = ? - GROUP BY c.status""", + GROUP BY status""", (domain_name,), ).fetchall() else: @@ -641,7 +647,8 @@ class Repository: FROM combinations c JOIN combination_results cr ON cr.combination_id = c.id JOIN domains d ON cr.domain_id = d.id - WHERE c.status LIKE '%\\_fail' ESCAPE '\\' AND d.name = ?""", + WHERE (c.status LIKE '%\\_fail' ESCAPE '\\' OR cr.domain_block_reason IS NOT NULL) + AND d.name = ?""", (domain_name,), ).fetchone() return { @@ -672,8 +679,10 @@ class Repository: JOIN domains d ON cr.domain_id = d.id WHERE d.name = ?""" params: list = [domain_name] - if status: - query += " AND c.status = ?" + if status == "domain_blocked": + query += " AND cr.domain_block_reason IS NOT NULL" + elif status: + query += " AND c.status = ? AND cr.domain_block_reason IS NULL" params.append(status) query += " ORDER BY cr.composite_score DESC" rows = self.conn.execute(query, params).fetchall() diff --git a/src/physcom/engine/pipeline.py b/src/physcom/engine/pipeline.py index 49ab803..f781f3d 100644 --- a/src/physcom/engine/pipeline.py +++ b/src/physcom/engine/pipeline.py @@ -2,6 +2,7 @@ from __future__ import annotations +import math import time from dataclasses import dataclass, field from datetime import datetime, timezone @@ -18,18 +19,6 @@ from physcom.models.domain import Domain, MetricBound # Keyed by the same categorical vocabulary already used in seed data — never # by entity name, so new entities inherit sensible behavior automatically. -# How controllable a thrust delivery profile is — bursty/extreme profiles are -# harder to control and cost more per use (ammunition, propellant, wear) than -# steady ones. Missing values fall back to a neutral 1.0/0.6. -THRUST_PROFILE_COST_MULTIPLIER: dict[str, float] = { - "low_continuous": 1.0, - "continuous_low": 1.0, - "moderate_continuous": 1.1, - "high_continuous": 1.3, - "extreme_continuous": 1.6, - "high_burst": 2.5, - "extreme_burst": 4.0, -} THRUST_PROFILE_SAFETY: dict[str, float] = { "low_continuous": 0.9, "continuous_low": 0.9, @@ -56,25 +45,6 @@ ENERGY_FORM_SAFETY: dict[str, float] = { "nuclear_thermal": 0.35, } -# Rough $/m base cost by energy form — renewables/muscle power are ~free, -# consumables (propellant, ammunition, nuclear fuel) cost real money per use. -# This is a categorical placeholder, not a physics formula — energy_density -# (J/kg) can't give a $/m figure on its own since it says nothing about price. -ENERGY_FORM_BASE_COST: dict[str, float] = { - "wind": 1e-6, - "gravitational": 1e-6, - "radiation_pressure": 1e-6, - "electrical": 3e-5, - "kinetic_stored": 2e-5, - "biological": 5e-5, - "pneumatic": 4e-5, - "chemical_combustible": 8e-5, - "nuclear_thermal": 1e-3, - "ion_propellant": 2e-3, - "chemical_propellant": 5e-3, - "chemical_explosive": 8e-3, -} - # How available the required infrastructure/fuel supply chain is today. # Multiple matches in one combo (e.g. a platform's road_network requirement # plus a storage's fuel_infrastructure requirement) are averaged. @@ -120,6 +90,181 @@ ENERGY_FORM_RELIABILITY: dict[str, float] = { "chemical_explosive": 0.45, } +# ── power_density / range_fuel / cost_efficiency ────────────────────── +# These three use the platform's declared mass envelope as a combo-wide +# budget: every component (platform, actuator, storage) is bounded below +# by its own mass range_min, and the SUM is bounded above by the +# platform's mass range_max -- the exact aggregate check ConstraintResolver +# already performs in pass 1. The "leanest legal build" (every component +# at its own floor) is always a legal design point (pass 1 already +# 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. +BIOLOGICAL_OPERATOR_MASS_KG: dict[str, float] = { + "biological": 70.0, # human rider; Animal Traction shares this form too +} + +# A platform's declared mass range often spans a whole real-world class, not +# one archetype -- Road Vehicle alone covers 50kg (motorcycle) to 36,000kg +# (truck). The floor is a legal build (pass 1 already checked it), but it's +# a motorcycle-scale build, not what a combo's own description usually +# implies. The geometric mean (not arithmetic) is the representative point +# for a range this wide: sqrt(50 * 36000) ~= 1343kg, in real commuter-car +# territory, versus the arithmetic mean (~18,000kg, a semi truck) or the +# floor (50kg, a motorcycle) -- real-world vehicle classes are far closer to +# log-uniformly distributed across a category than uniformly distributed. +def _representative_mass(mass_min: float, mass_max: float | None) -> float: + if mass_max and mass_min > 0: + return math.sqrt(mass_min * mass_max) + return max(mass_min, 100.0) + + +# Actuator + storage mass, sized to what's actually necessary rather than a +# fixed fraction of platform mass: enough actuator to sustain the +# platform's own performance requirement, enough storage to carry the +# domain's own "good" range target. Both share total_mass = p_rep + a + s, +# so the two requirements are coupled -- solved as a 2x2 linear system +# (Cramer's rule), not an iterative fit or an invented ratio: +# +# C1 * a = R1 * (p_rep + a + s) [a's capability meets requirement R1] +# C2 * s = R2 * (p_rep + a + s) [s's capability meets requirement R2] +# +# For the actuator equation, C1/R1 is either (specific_thrust, min_effective_accel) +# when the platform declares a real acceleration floor and the actuator +# declares real thrust (F=ma, both already exist in the seed data for +# aircraft/rocket combos -- no new data needed there), or (power_density, +# specific_energy_consumption * target_velocity) as the fallback -- "enough +# power to hold target_velocity against resistance" -- for every other +# platform, which needed one new attribute (target_velocity) since nothing +# in the schema previously declared a design speed for ground/water craft. +# For the storage equation, C2/R2 is always (energy_density, domain's own +# declared range_fuel norm_max * specific_energy_consumption) -- "enough +# energy to reach a genuinely good range for this domain," reusing the +# domain's own scoring ceiling rather than inventing a target. +# +# An infeasible system (the actuator is fundamentally too weak to ever +# reach the requirement, C1 <= R1) or a domain/platform missing the inputs +# it needs falls back to the entities' own bare floors -- a real +# limitation, not something to paper over with a default. +# Steady-state resistance (SPECIFIC_ENERGY_CONSUMPTION_J_PER_KG_M) only +# covers holding target_velocity -- real vehicles also carry reserve force +# for acceleration events (merging, passing, hills) that a pure cruise +# calculation would leave out entirely, which is why sizing off resistance +# alone undersizes the actuator relative to real vehicles. ~1.2 m/s^2 is a +# modest, real merging/passing acceleration capability, not a car's 0-60 +# figure -- added directly to the resistance term below (see call site). +ACCELERATION_RESERVE_M_S2: float = 2.6 + + +def _solve_two_requirement_masses( + p_rep: float, c1: float, r1: float, c2: float, r2: float, + a_min: float, s_min: float, +) -> tuple[float, float]: + a11, a12, b1 = c1 - r1, -r1, r1 * p_rep + a21, a22, b2 = -r2, c2 - r2, r2 * p_rep + det = a11 * a22 - a12 * a21 + if abs(det) < 1e-9: + return a_min, s_min + a = (b1 * a22 - a12 * b2) / det + s = (a11 * b2 - a21 * b1) / det + if a <= 0 or s <= 0: + 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"} + +# Resistive energy cost of travel, J per kg of vehicle per meter -- +# rolling resistance for ground vehicles, cruise-flight lift/drag for +# aircraft, hull drag for water. Keyed by the platform's declared `medium`, +# not per-platform -- a real train's steel-wheel-on-rail is far more +# efficient than a car's tire, both currently "ground" -- flagged as the +# coarsest approximation here, same spot the earlier LLM comparison found +# every model's range_fuel guess off by 10-25x from real vehicles. +SPECIFIC_ENERGY_CONSUMPTION_J_PER_KG_M: dict[str, float] = { + "ground": 0.016 * 9.81, # combined rolling + aero "road load", Crr-equivalent ~ 0.016 + "air": 9.81 / 10, # cruise flight, effective L/D ~ 10 + "water": 0.05 * 9.81, # displacement-hull drag, rough order of magnitude +} +# Rocket-propelled (space medium) platforms aren't resistance-limited at +# all -- no drag to fight in vacuum -- so this "energy / (resistance * +# mass)" shape is the wrong model for them; real range is governed by the +# rocket equation (delta-v = exhaust velocity * ln(mass ratio)), which this +# pass does not implement. Space is deliberately left out of the dict above +# so it falls through to the old placeholder formula in the code below +# rather than silently claiming a resistance-based number that isn't real. + +# Structural manufacturing cost, $ per kg of platform mass -- certification +# and materials overhead scale hugely by medium (aerospace-grade vs. +# automotive steel vs. spacecraft-grade). +STRUCTURAL_COST_PER_KG_BY_MEDIUM: dict[str, float] = { + "ground": 8.0, + "air": 400.0, + "water": 15.0, + "space": 8000.0, +} + +# Hardware manufacturing cost, $ per kg of actuator/storage-hardware mass, +# by energy form -- mature mass-produced tech (combustion, electric) is +# cheap per kg; exotic/regulated tech (nuclear, ion, rocket-grade) is not. +# biological is 0: there's no hardware to manufacture, the "actuator" is +# the operator's own body. +HARDWARE_COST_PER_KG_BY_ENERGY_FORM: dict[str, float] = { + "biological": 0.0, + "wind": 20.0, + "gravitational": 30.0, + "pneumatic": 35.0, + "chemical_combustible": 40.0, + "electrical": 60.0, + "kinetic_stored": 80.0, + "chemical_explosive": 150.0, + "chemical_propellant": 300.0, + "radiation_pressure": 500.0, + "ion_propellant": 5000.0, + "nuclear_thermal": 20000.0, +} + +# Consumable energy price, $ per MJ delivered. Ambient sources (sun, wind, +# gravity) are genuinely free; food is a real recurring cost even though it +# isn't range-limiting -- cost and range are different questions, see +# AMBIENT_ENERGY_FORMS above. Replaces the old flat $/m ENERGY_FORM_BASE_COST +# placeholder with a real energy-priced figure. +FUEL_PRICE_PER_MJ: dict[str, float] = { + "wind": 0.0, + "gravitational": 0.0, + "radiation_pressure": 0.0, + "biological": 0.03, + "nuclear_thermal": 0.01, + "chemical_combustible": 0.04, + "electrical": 0.04, + "pneumatic": 0.02, + "kinetic_stored": 0.0, + "chemical_propellant": 1.0, + "chemical_explosive": 2.0, + "ion_propellant": 5.0, +} + +# Total distance a vehicle travels over its operational life, used to +# amortize upfront/hardware cost into a $/m figure alongside operating +# cost. Coarse (per-medium, like the resistance table above) -- flagged as +# the same class of approximation. +LIFETIME_DISTANCE_M_BY_MEDIUM: dict[str, float] = { + "ground": 150_000_000.0, + "air": 3_000_000_000.0, + "water": 1_000_000_000.0, + "space": 5_000_000_000.0, +} + @dataclass class PipelineResult: @@ -270,7 +415,11 @@ class Pipeline: combo.status = "valid" self.repo.update_combination_status(combo.id, "valid") - # Domain constraint check (per-domain block only) + # Domain constraint check (per-domain block only). combo.status + # stays "valid" here on purpose: it's domain-agnostic and the + # same combo can be blocked in this domain but valid in another. + # The per-domain block lives on combination_results.domain_block_reason + # (see count_combinations_by_status / get_all_results, which bucket on it). if domain.constraints: dc_result = self.resolver.check_domain_constraints( combo, domain.constraints @@ -542,13 +691,18 @@ class Pipeline: def _stub_estimate( self, combo: Combination, metric_bounds: list[MetricBound] ) -> dict[str, float]: - """Simple heuristic estimation from dependency data (all values in SI base units). + """Deterministic estimation from declared entity attributes (no LLM). - cost_efficiency/safety/availability/reliability are driven by the - actuator's thrust_profile and energy_form and the combo's - infrastructure requirements — categorical properties every entity - already declares — rather than flat constants or a formula that - conflates power_density (W/kg, intensive) with cost. + power_density, range_fuel, and cost_efficiency are computed from the + platform's declared mass envelope treated as a combo-wide budget — + see the module-level comment above BIOLOGICAL_OPERATOR_MASS_KG for + the full formula rationale. + + safety/availability/reliability/cargo_capacity/environmental_impact + are untouched — these are judgment calls (regulatory, economic, + qualitative), not physics, and stay on the categorical lookup-table + heuristics below (actuator's thrust_profile and energy_form and the + combo's infrastructure requirements). cost_efficiency additionally checks the domain's declared unit: "$/(kg·m)" (freight-style domains) isn't a rescaling of "$/m" — it's @@ -557,9 +711,11 @@ class Pipeline: """ metric_names = [mb.metric_name for mb in metric_bounds] units_by_name = {mb.metric_name: mb.unit for mb in metric_bounds} + bounds_by_name = {mb.metric_name: mb for mb in metric_bounds} raw: dict[str, float] = {m: 0.0 for m in metric_names} - # Extract intrinsic properties from entities + # Extract intrinsic properties from entities (unchanged — still + # drives the untouched blocks below). power_density = 0.0 # W/kg energy_density = 0.0 # J/kg mass_total = 0.0 # kg, extensive — components share one vehicle @@ -585,17 +741,134 @@ class Pipeline: mass = mass_total if mass_total > 0 else 100.0 # kg, default if undeclared cargo_capacity_kg = mass * CARGO_KG_PER_STRUCTURAL_KG + # ── platform/actuator/storage-specific extraction, for + # power_density / range_fuel / cost_efficiency only ────────────── + platform = next((e for e in combo.entities if e.dimension == "platform"), None) + actuator = next((e for e in combo.entities if e.dimension == "actuator"), None) + storage = next((e for e in combo.entities if e.dimension == "energy_storage"), None) + + def dep_value(entity, key, constraint_type) -> float | None: + if entity is None: + return None + for dep in entity.dependencies: + if dep.key == key and dep.constraint_type == constraint_type: + return float(dep.value) + return None + + def dep_str(entity, key, constraint_type) -> str | None: + if entity is None: + return None + for dep in entity.dependencies: + if dep.key == key and dep.constraint_type == constraint_type: + return dep.value + return None + + p_min = dep_value(platform, "mass", "range_min") or 0.0 + a_min = dep_value(actuator, "mass", "range_min") or 0.0 + s_min = dep_value(storage, "mass", "range_min") or 0.0 + p_max = dep_value(platform, "mass", "range_max") + + medium = dep_str(platform, "medium", "requires") or "ground" + actuator_energy_form = dep_str(actuator, "energy_form", "requires") + storage_energy_form = dep_str(storage, "energy_form", "provides") + k_act = dep_value(actuator, "power_density", "provides") or 0.0 + e_dens = dep_value(storage, "energy_density", "provides") or 0.0 + k_med = SPECIFIC_ENERGY_CONSUMPTION_J_PER_KG_M.get(medium) + + p_rep = _representative_mass(p_min, p_max) # platform's representative build size + + # actuator/storage mass: sized to what's actually necessary (see + # module note above _solve_two_requirement_masses), except the + # documented near-zero-owned-mass cases below. + denom_offset = 0.0 # extra propelled mass that never competes for the build budget + if actuator_energy_form in BIOLOGICAL_OPERATOR_MASS_KG: + power_mass = BIOLOGICAL_OPERATOR_MASS_KG[actuator_energy_form] + denom_offset = power_mass + actuator_mass, storage_mass = a_min, s_min + elif 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. + footprint = dep_value(actuator, "footprint", "range_min") or 0.0 + actuator_mass = footprint * 0.05 # kg/m^2, thin deployable sail film + power_mass = actuator_mass + storage_mass = s_min + else: + min_accel = dep_value(platform, "min_effective_accel", "range_min") + specific_thrust = dep_value(actuator, "specific_thrust", "provides") + target_velocity = dep_value(platform, "target_velocity", "provides") + range_bounds = bounds_by_name.get("range_fuel") + target_range = range_bounds.norm_max if range_bounds else None + + if min_accel and specific_thrust: + c1, r1 = specific_thrust, min_accel + elif target_velocity and k_med: + # Resistance alone (k_med) only covers steady-state cruise -- + # a real vehicle also needs reserve force for acceleration + # events (merging, passing, hills), not just holding speed. + # F=ma: an acceleration reserve in m/s^2 is dimensionally a + # specific force (N/kg) exactly like k_med (J/(kg*m) = N/kg), + # so it adds directly before converting to specific power + # (P/mass = force/mass * v). + c1, r1 = k_act, (k_med + ACCELERATION_RESERVE_M_S2) * target_velocity + else: + c1 = r1 = 0.0 # no performance requirement available -- solve degenerates below + + if target_range and k_med: + c2, r2 = e_dens, target_range * k_med + else: + c2 = r2 = 0.0 + + if c1 and c2: + actuator_mass, storage_mass = _solve_two_requirement_masses( + p_rep, c1, r1, c2, r2, a_min, s_min + ) + else: + # No performance requirement available at all (e.g. a + # space-medium platform paired with an actuator that + # declares neither specific_thrust nor a usable target + # velocity) -- fall back to bare floors, with the same + # near-zero-mass nominal reference used elsewhere so this + # doesn't silently degenerate to 0 power the way the + # original stub did. + actuator_mass = a_min if a_min > 0.0 else 10.0 + storage_mass = s_min + power_mass = actuator_mass + + floor_total = p_rep + actuator_mass + storage_mass + physics_denom = floor_total + denom_offset + if "power_density" in raw: - raw["power_density"] = power_density + raw["power_density"] = (k_act * power_mass) / physics_denom if physics_denom else 0.0 + + if "range_fuel" in raw: + if storage_energy_form in AMBIENT_ENERGY_FORMS: + mb = bounds_by_name.get("range_fuel") + raw["range_fuel"] = mb.norm_max if mb else 0.0 + elif k_med is not None and floor_total > 0: + raw["range_fuel"] = min((e_dens * storage_mass) / (k_med * floor_total), 1e13) + else: + # space/rocket platforms: resistance-based formula doesn't + # apply (see module note) -- old placeholder, not a claim. + raw["range_fuel"] = min(e_dens * 2.78, 1e13) if "cost_efficiency" in raw: - base_cost = ENERGY_FORM_BASE_COST.get(energy_form, 5e-4) - cost_mult = THRUST_PROFILE_COST_MULTIPLIER.get(thrust_profile, 1.0) - cost_per_meter = base_cost * cost_mult + structural_cost = p_rep * STRUCTURAL_COST_PER_KG_BY_MEDIUM.get(medium, STRUCTURAL_COST_PER_KG_BY_MEDIUM["ground"]) + actuator_hw_cost = actuator_mass * HARDWARE_COST_PER_KG_BY_ENERGY_FORM.get(actuator_energy_form, 50.0) + storage_hw_cost = storage_mass * HARDWARE_COST_PER_KG_BY_ENERGY_FORM.get(storage_energy_form, 50.0) + upfront_cost = structural_cost + actuator_hw_cost + storage_hw_cost + lifetime_m = LIFETIME_DISTANCE_M_BY_MEDIUM.get(medium, LIFETIME_DISTANCE_M_BY_MEDIUM["ground"]) + amortized_per_m = upfront_cost / lifetime_m + + fuel_price_per_mj = FUEL_PRICE_PER_MJ.get(storage_energy_form, 0.04) + energy_per_m_mj = ((k_med or SPECIFIC_ENERGY_CONSUMPTION_J_PER_KG_M["ground"]) * floor_total) / 1e6 + operating_per_m = energy_per_m_mj * fuel_price_per_mj + + cost_per_m = amortized_per_m + operating_per_m if units_by_name.get("cost_efficiency") == "$/(kg·m)": - raw["cost_efficiency"] = cost_per_meter / max(cargo_capacity_kg, 1.0) + raw["cost_efficiency"] = cost_per_m / max(cargo_capacity_kg, 1.0) else: - raw["cost_efficiency"] = cost_per_meter + raw["cost_efficiency"] = cost_per_m if "safety" in raw: candidates = [ @@ -612,9 +885,6 @@ class Pipeline: sum(infra_matches) / len(infra_matches) if infra_matches else 0.5 ) - if "range_fuel" in raw: - raw["range_fuel"] = min(energy_density * 2.78, 1e13) - if "range_degradation" in raw: raw["range_degradation"] = 365 * 86400 diff --git a/src/physcom/llm/providers/ollama.py b/src/physcom/llm/providers/ollama.py index a04b190..2834c58 100644 --- a/src/physcom/llm/providers/ollama.py +++ b/src/physcom/llm/providers/ollama.py @@ -54,7 +54,7 @@ class OllamaLLMProvider(LLMProvider): headers={"Content-Type": "application/json"}, ) try: - with urllib.request.urlopen(req, timeout=120) as resp: + with urllib.request.urlopen(req, timeout=300) as resp: return json.loads(resp.read())["response"] except urllib.error.URLError as exc: raise ConnectionError( diff --git a/src/physcom/seed/transport_example.py b/src/physcom/seed/transport_example.py index b8b17a8..a64b64a 100644 --- a/src/physcom/seed/transport_example.py +++ b/src/physcom/seed/transport_example.py @@ -24,6 +24,7 @@ GROUND_PLATFORMS: list[Entity] = [ Dependency("physical", "mass", "50", "kg", "range_min"), Dependency("infrastructure", "road_network", "true", None, "requires"), Dependency("environment", "medium", "ground", None, "requires"), + Dependency("physical", "target_velocity", "25", "m/s", "provides"), ], ), Entity( @@ -41,6 +42,7 @@ GROUND_PLATFORMS: list[Entity] = [ Dependency("physical", "mass", "5", "kg", "range_min"), Dependency("infrastructure", "road_network", "true", None, "requires"), Dependency("environment", "medium", "ground", None, "requires"), + Dependency("physical", "target_velocity", "6", "m/s", "provides"), ], ), Entity( @@ -58,6 +60,7 @@ GROUND_PLATFORMS: list[Entity] = [ Dependency("physical", "mass", "10000", "kg", "range_min"), Dependency("infrastructure", "rail_network", "true", None, "requires"), Dependency("environment", "medium", "ground", None, "requires"), + Dependency("physical", "target_velocity", "30", "m/s", "provides"), ], ), ] @@ -79,6 +82,7 @@ WATER_PLATFORMS: list[Entity] = [ Dependency("physical", "mass", "100000", "kg", "range_max"), Dependency("physical", "mass", "30", "kg", "range_min"), Dependency("environment", "medium", "water", None, "requires"), + Dependency("physical", "target_velocity", "8", "m/s", "provides"), ], ), Entity( @@ -94,6 +98,7 @@ WATER_PLATFORMS: list[Entity] = [ Dependency("physical", "mass", "10000", "kg", "range_min"), Dependency("environment", "medium", "water", None, "requires"), Dependency("physical", "energy_density", "720000", "J/kg", "range_min"), + Dependency("physical", "target_velocity", "8", "m/s", "provides"), ], ), ] @@ -118,6 +123,7 @@ AIR_PLATFORMS: list[Entity] = [ Dependency("environment", "medium", "air", None, "requires"), Dependency("physical", "energy_density", "1440000", "J/kg", "range_min"), Dependency("physical", "min_effective_accel", "2.0", "m/s²", "range_min"), + Dependency("physical", "target_velocity", "60", "m/s", "provides"), ], ), Entity( @@ -135,6 +141,7 @@ AIR_PLATFORMS: list[Entity] = [ Dependency("environment", "medium", "air", None, "requires"), Dependency("physical", "energy_density", "720000", "J/kg", "range_min"), Dependency("physical", "min_effective_accel", "10", "m/s²", "range_min"), + Dependency("physical", "target_velocity", "30", "m/s", "provides"), ], ), Entity( @@ -214,6 +221,7 @@ FICTIONAL_PLATFORMS: list[Entity] = [ Dependency("physical", "mass", "5000", "kg", "range_min"), Dependency("infrastructure", "hyperloop_tube", "true", None, "requires"), Dependency("environment", "medium", "ground", None, "requires"), + Dependency("physical", "target_velocity", "270", "m/s", "provides"), # near-sonic, per its own description ], ), ] @@ -302,7 +310,7 @@ BIOLOGICAL_ACTUATORS: list[Entity] = [ Dependency("energy", "energy_form", "biological", None, "requires"), Dependency("physical", "mass", "0", "kg", "range_min"), Dependency("force", "thrust_profile", "low_continuous", None, "provides"), - Dependency("force", "power_density", "1.5", "W/kg", "provides"), + Dependency("force", "power_density", "5.5", "W/kg", "provides"), ], ), Entity(