seeding expansion

also: replace energy output with energy output density
This commit is contained in:
2026-03-04 13:21:20 -06:00
parent aa6eb72a74
commit e99a14d087
16 changed files with 1078 additions and 117 deletions

View File

@@ -153,6 +153,35 @@ class Repository:
self.conn.commit()
return dep
def replace_entity_dependencies(self, entity_id: int, deps: list[Dependency]) -> None:
"""Delete all existing dependencies for an entity and insert new ones."""
self.conn.execute("DELETE FROM dependencies WHERE entity_id = ?", (entity_id,))
for dep in deps:
cur = self.conn.execute(
"""INSERT INTO dependencies
(entity_id, category, key, value, unit, constraint_type)
VALUES (?, ?, ?, ?, ?, ?)""",
(entity_id, dep.category, dep.key, dep.value, dep.unit, dep.constraint_type),
)
dep.id = cur.lastrowid
self.conn.commit()
def get_entity_by_name(self, dimension: str, name: str) -> Entity | None:
row = self.conn.execute(
"""SELECT e.id, e.name, e.description, d.name as dimension, e.dimension_id
FROM entities e JOIN dimensions d ON e.dimension_id = d.id
WHERE d.name = ? AND e.name = ?""",
(dimension, name),
).fetchone()
if not row:
return None
deps = self._load_dependencies(row["id"])
return Entity(
id=row["id"], name=row["name"], description=row["description"] or "",
dimension=row["dimension"], dimension_id=row["dimension_id"],
dependencies=deps,
)
def update_dependency(self, dep_id: int, dep: Dependency) -> None:
self.conn.execute(
"""UPDATE dependencies
@@ -772,3 +801,20 @@ class Repository:
(combo_id, domain_id),
).fetchone()
return dict(row) if row else None
# ── Admin ────────────────────────────────────────────────────
def clear_all(self) -> None:
"""Delete all data from every table in FK-safe order."""
self.conn.execute("DELETE FROM pipeline_runs")
self.conn.execute("DELETE FROM combination_results")
self.conn.execute("DELETE FROM combination_scores")
self.conn.execute("DELETE FROM combination_entities")
self.conn.execute("DELETE FROM combinations")
self.conn.execute("DELETE FROM dependencies")
self.conn.execute("DELETE FROM entities")
self.conn.execute("DELETE FROM domain_metric_weights")
self.conn.execute("DELETE FROM domains")
self.conn.execute("DELETE FROM metrics")
self.conn.execute("DELETE FROM dimensions")
self.conn.commit()

View File

@@ -41,7 +41,7 @@ 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_force_scale(combination, result)
self._check_power_density(combination, result)
self._check_energy_density(combination, result)
self._check_unmet_requirements(all_deps, result)
@@ -125,34 +125,32 @@ class ConstraintResolver:
f"but {max_name} limits {key} <= {max_val}"
)
def _check_force_scale(
def _check_power_density(
self, combination: Combination, result: ConstraintResult
) -> None:
"""Rule 4: If power source output << platform requirement → warn/block."""
force_provided: list[tuple[str, float]] = []
force_required: list[tuple[str, float]] = []
"""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 == "force_output_watts" and dep.constraint_type == "provides":
force_provided.append((entity.name, float(dep.value)))
elif dep.key == "force_required_watts" and dep.constraint_type == "range_min":
force_required.append((entity.name, float(dep.value)))
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_watts in force_required:
for prov_name, prov_watts in force_provided:
if prov_watts < req_watts * 0.01:
# Off by more than 100x — hard block
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_watts}W but "
f"{req_name} requires {req_watts}W "
f"(force deficit > 100x)"
f"{prov_name} provides {prov_density} W/kg but "
f"{req_name} requires {req_density} W/kg "
f"(power density deficit > 100x)"
)
elif prov_watts < req_watts:
# Under-powered but not impossibly so — warn
elif prov_density < req_density:
result.warnings.append(
f"{prov_name} provides {prov_watts}W but "
f"{req_name} requires {req_watts}W "
f"{prov_name} provides {prov_density} W/kg but "
f"{req_name} requires {req_density} W/kg "
f"(under-powered)"
)

View File

@@ -417,22 +417,25 @@ class Pipeline:
"""Simple heuristic estimation from dependency data."""
raw: dict[str, float] = {m: 0.0 for m in metric_names}
# Extract force output from power source
force_watts = 0.0
# Extract intrinsic properties from entities
power_density = 0.0 # W/kg
energy_density = 0.0 # Wh/kg
mass_kg = 100.0 # default
for entity in combo.entities:
for dep in entity.dependencies:
if dep.key == "force_output_watts" and dep.constraint_type == "provides":
force_watts = max(force_watts, float(dep.value))
if dep.key == "min_mass_kg" and dep.constraint_type == "range_min":
if dep.key == "power_density_w_kg" 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":
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))
# Rough speed estimate: F=ma -> v proportional to power/mass
if "speed" in raw and mass_kg > 0:
raw["speed"] = min(force_watts / mass_kg * 0.5, 300000)
# Rough speed estimate: higher power density → faster
if "speed" in raw:
raw["speed"] = min(power_density * 0.5, 300000)
if "cost_efficiency" in raw:
raw["cost_efficiency"] = max(0.01, 2.0 - force_watts / 100000)
raw["cost_efficiency"] = max(0.01, 2.0 - power_density / 1000)
if "safety" in raw:
raw["safety"] = 0.5
@@ -441,9 +444,21 @@ class Pipeline:
raw["availability"] = 0.5
if "range_fuel" in raw:
raw["range_fuel"] = min(force_watts * 0.01, 1e10)
raw["range_fuel"] = min(energy_density * 10, 1e10)
if "range_degradation" in raw:
raw["range_degradation"] = 365
if "cargo_capacity" in raw:
raw["cargo_capacity"] = mass_kg * 0.5
if "cargo_capacity_kg" in raw:
raw["cargo_capacity_kg"] = mass_kg * 0.3
if "environmental_impact" in raw:
raw["environmental_impact"] = max(0.0, power_density * 0.2)
if "reliability" in raw:
raw["reliability"] = 0.5
return raw

File diff suppressed because it is too large Load Diff

View File

@@ -109,11 +109,13 @@ def create_app() -> Flask:
from physcom_web.routes.domains import bp as domains_bp
from physcom_web.routes.pipeline import bp as pipeline_bp
from physcom_web.routes.results import bp as results_bp
from physcom_web.routes.admin import bp as admin_bp
app.register_blueprint(entities_bp)
app.register_blueprint(domains_bp)
app.register_blueprint(pipeline_bp)
app.register_blueprint(results_bp)
app.register_blueprint(admin_bp)
@app.route("/")
def index():

View File

@@ -0,0 +1,51 @@
"""Admin panel routes."""
from __future__ import annotations
from flask import Blueprint, flash, redirect, render_template, url_for
from physcom.seed.transport_example import load_transport_seed
from physcom_web.app import get_repo
bp = Blueprint("admin", __name__, url_prefix="/admin")
@bp.route("/")
def admin_index():
repo = get_repo()
entities = repo.list_entities()
domains = repo.list_domains()
status_counts = repo.count_combinations_by_status()
runs = repo.list_pipeline_runs()
stats = {
"entities": len(entities),
"domains": len(domains),
"combinations": sum(status_counts.values()),
"pipeline_runs": len(runs),
}
return render_template("admin/index.html", stats=stats)
@bp.route("/reseed", methods=["POST"])
def reseed():
repo = get_repo()
counts = load_transport_seed(repo)
total = counts["platforms"] + counts["power_sources"]
flash(
f"Reseed complete — added {total} entities, {counts['domains']} domains.",
"success",
)
return redirect(url_for("admin.admin_index"))
@bp.route("/wipe-and-reseed", methods=["POST"])
def wipe_and_reseed():
repo = get_repo()
repo.clear_all()
counts = load_transport_seed(repo)
total = counts["platforms"] + counts["power_sources"]
flash(
f"Wiped all data and reseeded — {total} entities, {counts['domains']} domains.",
"success",
)
return redirect(url_for("admin.admin_index"))

View File

@@ -647,5 +647,11 @@ select option {
margin-top: 0.25rem;
}
/* ── Admin warning box ──────────────────────────────────── */
.warning-box {
border-color: rgba(184,147,92,0.4);
background: rgba(184,147,92,0.06);
}
/* ── Google Fonts import ─────────────────────────────────── */
@import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600&family=Inter:wght@400;500;600&family=JetBrains+Mono:wght@400;500&display=swap');

View File

@@ -0,0 +1,51 @@
{% extends "base.html" %}
{% block title %}Admin — PhysCom{% endblock %}
{% block content %}
<h1>Admin Panel</h1>
<div class="stats-row">
<div class="stat-card">
<div class="stat-value">{{ stats.entities }}</div>
<div class="stat-label">Entities</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ stats.domains }}</div>
<div class="stat-label">Domains</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ stats.combinations }}</div>
<div class="stat-label">Combinations</div>
</div>
<div class="stat-card">
<div class="stat-value">{{ stats.pipeline_runs }}</div>
<div class="stat-label">Pipeline Runs</div>
</div>
</div>
<h2>Seed Data</h2>
<div class="card-grid">
<div class="card">
<h3>Reseed (Additive)</h3>
<p class="subtitle">
Add any missing seed entities and domains. Existing user-created data is
preserved — only entries that don't already exist are inserted.
</p>
<form method="post" action="{{ url_for('admin.reseed') }}" style="margin-top: 0.75rem">
<button type="submit" class="btn btn-primary">Reseed</button>
</form>
</div>
<div class="card warning-box">
<h3>Wipe &amp; Reseed</h3>
<p class="subtitle">
Delete <strong>all</strong> data — entities, domains, combinations,
pipeline runs — then reload seed data from scratch.
</p>
<form method="post" action="{{ url_for('admin.wipe_and_reseed') }}" style="margin-top: 0.75rem"
onsubmit="return confirm('This will permanently delete ALL data and reseed from scratch. Continue?')">
<button type="submit" class="btn btn-danger">Wipe &amp; Reseed</button>
</form>
</div>
</div>
{% endblock %}

View File

@@ -19,6 +19,7 @@
<li><a href="{{ url_for('domains.domain_list') }}">Domains</a></li>
<li><a href="{{ url_for('pipeline.pipeline_form') }}">Pipeline</a></li>
<li><a href="{{ url_for('results.results_index') }}">Results</a></li>
<li><a href="{{ url_for('admin.admin_index') }}">Admin</a></li>
</ul>
</nav>