Delete app
Browse files- app/__init__.py +0 -0
- app/analytics.py +0 -46
- app/config.py +0 -14
- app/database.py +0 -17
- app/explanations.py +0 -43
- app/feature_engineering.py +0 -88
- app/graph_features.py +0 -26
- app/main.py +0 -27
- app/ml.py +0 -46
- app/models.py +0 -55
- app/repository.py +0 -56
- app/routers/__init__.py +0 -0
- app/routers/admin.py +0 -53
- app/routers/analytics.py +0 -18
- app/routers/health.py +0 -8
- app/routers/objects.py +0 -22
- app/routers/pairs.py +0 -63
- app/scripts/bootstrap_demo.py +0 -28
- app/scripts/seed_synthetic.py +0 -36
- app/scripts/train_baseline.py +0 -35
- app/services.py +0 -109
- app/utils.py +0 -19
app/__init__.py
DELETED
|
File without changes
|
app/analytics.py
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
from collections import defaultdict
|
| 2 |
-
from app.repository import list_high_risk_pairs, latest_runs
|
| 3 |
-
|
| 4 |
-
def shell_analytics(db, limit: int = 10):
|
| 5 |
-
rows = list_high_risk_pairs(db, limit=500)
|
| 6 |
-
buckets = defaultdict(lambda: {"shell_key": "", "pair_count": 0, "avg_final_score": 0.0, "max_final_score": 0.0, "high_or_critical": 0})
|
| 7 |
-
for r in rows:
|
| 8 |
-
key = r.shell_key or "unknown"
|
| 9 |
-
b = buckets[key]
|
| 10 |
-
b["shell_key"] = key
|
| 11 |
-
b["pair_count"] += 1
|
| 12 |
-
b["avg_final_score"] += r.final_score
|
| 13 |
-
b["max_final_score"] = max(b["max_final_score"], r.final_score)
|
| 14 |
-
if r.risk_label in ("high", "critical"):
|
| 15 |
-
b["high_or_critical"] += 1
|
| 16 |
-
out = []
|
| 17 |
-
for b in buckets.values():
|
| 18 |
-
b["avg_final_score"] = b["avg_final_score"] / max(1, b["pair_count"])
|
| 19 |
-
out.append(b)
|
| 20 |
-
out.sort(key=lambda x: x["avg_final_score"], reverse=True)
|
| 21 |
-
return out[:limit]
|
| 22 |
-
|
| 23 |
-
def diagnostics_summary(db):
|
| 24 |
-
rows = list_high_risk_pairs(db, limit=500)
|
| 25 |
-
runs = latest_runs(db, limit=10)
|
| 26 |
-
labels = {"low": 0, "medium": 0, "high": 0, "critical": 0}
|
| 27 |
-
if not rows:
|
| 28 |
-
return {
|
| 29 |
-
"pair_count": 0,
|
| 30 |
-
"avg_final_score": 0.0,
|
| 31 |
-
"max_final_score": 0.0,
|
| 32 |
-
"high_plus_critical": 0,
|
| 33 |
-
"label_distribution": labels,
|
| 34 |
-
"recent_runs": len(runs),
|
| 35 |
-
}
|
| 36 |
-
scores = [r.final_score for r in rows]
|
| 37 |
-
for r in rows:
|
| 38 |
-
labels[r.risk_label] = labels.get(r.risk_label, 0) + 1
|
| 39 |
-
return {
|
| 40 |
-
"pair_count": len(rows),
|
| 41 |
-
"avg_final_score": sum(scores) / len(scores),
|
| 42 |
-
"max_final_score": max(scores),
|
| 43 |
-
"high_plus_critical": labels["high"] + labels["critical"],
|
| 44 |
-
"label_distribution": labels,
|
| 45 |
-
"recent_runs": len(runs),
|
| 46 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/config.py
DELETED
|
@@ -1,14 +0,0 @@
|
|
| 1 |
-
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 2 |
-
|
| 3 |
-
class Settings(BaseSettings):
|
| 4 |
-
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
| 5 |
-
APP_NAME: str = "Space Risk Intelligence API"
|
| 6 |
-
APP_ENV: str = "dev"
|
| 7 |
-
DATABASE_URL: str = "sqlite:///./space_risk.db"
|
| 8 |
-
CELESTRAK_URL: str = "https://celestrak.org/NORAD/elements/gp.php?GROUP=active&FORMAT=json"
|
| 9 |
-
ALLOWED_ORIGINS: str = "http://localhost:5173,http://localhost:3000"
|
| 10 |
-
TOP_K_ALERTS: int = 25
|
| 11 |
-
MAX_OBJECTS_PER_RUN: int = 600
|
| 12 |
-
MAX_CANDIDATE_PAIRS: int = 2500
|
| 13 |
-
|
| 14 |
-
settings = Settings()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/database.py
DELETED
|
@@ -1,17 +0,0 @@
|
|
| 1 |
-
from sqlalchemy import create_engine
|
| 2 |
-
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
| 3 |
-
from app.config import settings
|
| 4 |
-
|
| 5 |
-
connect_args = {"check_same_thread": False} if settings.DATABASE_URL.startswith("sqlite") else {}
|
| 6 |
-
engine = create_engine(settings.DATABASE_URL, connect_args=connect_args)
|
| 7 |
-
SessionLocal = sessionmaker(bind=engine, autocommit=False, autoflush=False)
|
| 8 |
-
|
| 9 |
-
class Base(DeclarativeBase):
|
| 10 |
-
pass
|
| 11 |
-
|
| 12 |
-
def get_db():
|
| 13 |
-
db = SessionLocal()
|
| 14 |
-
try:
|
| 15 |
-
yield db
|
| 16 |
-
finally:
|
| 17 |
-
db.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/explanations.py
DELETED
|
@@ -1,43 +0,0 @@
|
|
| 1 |
-
def build_top_factors(features, anomaly_score, final_score):
|
| 2 |
-
factors = []
|
| 3 |
-
if features.get("close_approach_proxy", 0) > 0.5:
|
| 4 |
-
factors.append("small orbital separation proxy")
|
| 5 |
-
if features.get("same_shell", 0) >= 1:
|
| 6 |
-
factors.append("same orbital shell")
|
| 7 |
-
if features.get("graph_local_density", 0) > 0.2:
|
| 8 |
-
factors.append("dense interaction neighborhood")
|
| 9 |
-
if features.get("recurrence_count", 0) >= 3:
|
| 10 |
-
factors.append("repeated appearance across scoring windows")
|
| 11 |
-
if features.get("trend_delta_score", 0) > 0.1:
|
| 12 |
-
factors.append("risk trend increasing over time")
|
| 13 |
-
if anomaly_score > 0.6:
|
| 14 |
-
factors.append("unusual conjunction pattern")
|
| 15 |
-
if final_score > 0.9:
|
| 16 |
-
factors.append("high blended system score")
|
| 17 |
-
return factors[:5] or ["general risk elevation from orbital similarity"]
|
| 18 |
-
|
| 19 |
-
def analyst_summary(features, top_factors, final_score):
|
| 20 |
-
text = "This pair is prioritized because " + ", ".join(top_factors[:3]) + "." if top_factors else "This pair is prioritized because multiple similarity signals are elevated."
|
| 21 |
-
if features.get("recurrence_count", 0) >= 3:
|
| 22 |
-
text += " The pair has appeared repeatedly in recent scoring windows."
|
| 23 |
-
if features.get("graph_local_density", 0) > 0.2:
|
| 24 |
-
text += " The surrounding interaction neighborhood is congested."
|
| 25 |
-
if final_score > 0.9:
|
| 26 |
-
text += " This pair should be reviewed first."
|
| 27 |
-
return text
|
| 28 |
-
|
| 29 |
-
def structured_explanation(features, top_factors, final_score, action):
|
| 30 |
-
return {
|
| 31 |
-
"why_risky": top_factors[:3],
|
| 32 |
-
"what_changed": "Risk increased recently." if features.get("trend_delta_score", 0) > 0.1 else "No major recent change detected.",
|
| 33 |
-
"context": "Pair sits in a dense local interaction neighborhood." if features.get("graph_local_density", 0) > 0.2 else "Pair context is not highly congested.",
|
| 34 |
-
"recommended_action": action,
|
| 35 |
-
"final_score": final_score,
|
| 36 |
-
}
|
| 37 |
-
|
| 38 |
-
def recommended_action(label):
|
| 39 |
-
return {
|
| 40 |
-
"critical": "immediate analyst review",
|
| 41 |
-
"high": "prioritize analyst review",
|
| 42 |
-
"medium": "monitor and rescore on next cycle",
|
| 43 |
-
}.get(label, "low priority monitoring")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/feature_engineering.py
DELETED
|
@@ -1,88 +0,0 @@
|
|
| 1 |
-
from app.utils import safe_float, safe_int
|
| 2 |
-
|
| 3 |
-
FEATURE_COLUMNS = [
|
| 4 |
-
"delta_mean_motion","delta_inclination","delta_eccentricity","delta_raan","delta_bstar",
|
| 5 |
-
"launch_year_gap","same_object_type","same_shell","shell_density_proxy","close_approach_proxy",
|
| 6 |
-
"persistence_proxy","recurrence_count","trend_delta_score","score_volatility_proxy",
|
| 7 |
-
"graph_degree_sum","graph_common_neighbors","graph_jaccard","graph_local_density",
|
| 8 |
-
]
|
| 9 |
-
|
| 10 |
-
FEATURE_LABELS = {
|
| 11 |
-
"delta_mean_motion": "Mean motion difference",
|
| 12 |
-
"delta_inclination": "Inclination difference",
|
| 13 |
-
"delta_eccentricity": "Eccentricity difference",
|
| 14 |
-
"delta_raan": "RAAN difference",
|
| 15 |
-
"delta_bstar": "BSTAR difference",
|
| 16 |
-
"launch_year_gap": "Launch year gap",
|
| 17 |
-
"same_object_type": "Same object type",
|
| 18 |
-
"same_shell": "Same orbital shell",
|
| 19 |
-
"shell_density_proxy": "Shell density proxy",
|
| 20 |
-
"close_approach_proxy": "Close approach proxy",
|
| 21 |
-
"persistence_proxy": "Persistence proxy",
|
| 22 |
-
"recurrence_count": "Recurrence count",
|
| 23 |
-
"trend_delta_score": "Trend delta",
|
| 24 |
-
"score_volatility_proxy": "Score volatility",
|
| 25 |
-
"graph_degree_sum": "Graph degree sum",
|
| 26 |
-
"graph_common_neighbors": "Common neighbors",
|
| 27 |
-
"graph_jaccard": "Graph jaccard",
|
| 28 |
-
"graph_local_density": "Graph local density",
|
| 29 |
-
}
|
| 30 |
-
|
| 31 |
-
def normalize_object(raw):
|
| 32 |
-
name = raw.get("OBJECT_NAME") or raw.get("object_name") or "UNKNOWN"
|
| 33 |
-
norad = raw.get("NORAD_CAT_ID") or raw.get("norad_cat_id")
|
| 34 |
-
intl = raw.get("OBJECT_ID") or raw.get("object_id") or ""
|
| 35 |
-
launch_year = int(intl[:4]) if len(intl) >= 4 and intl[:4].isdigit() else None
|
| 36 |
-
return {
|
| 37 |
-
"object_id": str(norad or name),
|
| 38 |
-
"norad_cat_id": safe_int(norad, 0) or None,
|
| 39 |
-
"object_name": name,
|
| 40 |
-
"object_type": raw.get("OBJECT_TYPE") or raw.get("object_type"),
|
| 41 |
-
"mean_motion": safe_float(raw.get("MEAN_MOTION") or raw.get("mean_motion")),
|
| 42 |
-
"inclination": safe_float(raw.get("INCLINATION") or raw.get("inclination")),
|
| 43 |
-
"eccentricity": safe_float(raw.get("ECCENTRICITY") or raw.get("eccentricity")),
|
| 44 |
-
"raan": safe_float(raw.get("RA_OF_ASC_NODE") or raw.get("raan")),
|
| 45 |
-
"bstar": safe_float(raw.get("BSTAR") or raw.get("bstar")),
|
| 46 |
-
"launch_year": launch_year,
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
def orbital_shell_key(obj):
|
| 50 |
-
mm = safe_float(obj.get("mean_motion"))
|
| 51 |
-
inc = safe_float(obj.get("inclination"))
|
| 52 |
-
ecc = safe_float(obj.get("eccentricity"))
|
| 53 |
-
return f"mm:{int(mm)}|inc:{int(inc//5)*5}|ecc:{int(ecc*1000)//10}"
|
| 54 |
-
|
| 55 |
-
def base_pair_features(a, b):
|
| 56 |
-
mm1, mm2 = safe_float(a.get("mean_motion")), safe_float(b.get("mean_motion"))
|
| 57 |
-
inc1, inc2 = safe_float(a.get("inclination")), safe_float(b.get("inclination"))
|
| 58 |
-
ecc1, ecc2 = safe_float(a.get("eccentricity")), safe_float(b.get("eccentricity"))
|
| 59 |
-
raan1, raan2 = safe_float(a.get("raan")), safe_float(b.get("raan"))
|
| 60 |
-
b1, b2 = safe_float(a.get("bstar")), safe_float(b.get("bstar"))
|
| 61 |
-
ly1, ly2 = safe_int(a.get("launch_year")), safe_int(b.get("launch_year"))
|
| 62 |
-
same_type = 1 if (a.get("object_type") or "") == (b.get("object_type") or "") else 0
|
| 63 |
-
same_shell = 1 if orbital_shell_key(a) == orbital_shell_key(b) else 0
|
| 64 |
-
delta_mm = abs(mm1 - mm2)
|
| 65 |
-
delta_inc = abs(inc1 - inc2)
|
| 66 |
-
delta_ecc = abs(ecc1 - ecc2)
|
| 67 |
-
delta_raan = abs(raan1 - raan2)
|
| 68 |
-
delta_bstar = abs(b1 - b2)
|
| 69 |
-
launch_gap = abs(ly1 - ly2) if ly1 and ly2 else 25
|
| 70 |
-
shell_density_proxy = max(0.0, 10.0 - delta_mm) + max(0.0, 8.0 - delta_inc / 2.0)
|
| 71 |
-
close_approach_proxy = 1.0 / (1.0 + delta_mm + delta_inc / 10.0 + delta_ecc * 50.0 + delta_raan / 60.0)
|
| 72 |
-
persistence_proxy = 1.0 if same_shell else 0.25
|
| 73 |
-
return {
|
| 74 |
-
"delta_mean_motion": delta_mm,
|
| 75 |
-
"delta_inclination": delta_inc,
|
| 76 |
-
"delta_eccentricity": delta_ecc,
|
| 77 |
-
"delta_raan": delta_raan,
|
| 78 |
-
"delta_bstar": delta_bstar,
|
| 79 |
-
"launch_year_gap": float(launch_gap),
|
| 80 |
-
"same_object_type": float(same_type),
|
| 81 |
-
"same_shell": float(same_shell),
|
| 82 |
-
"shell_density_proxy": float(shell_density_proxy),
|
| 83 |
-
"close_approach_proxy": float(close_approach_proxy),
|
| 84 |
-
"persistence_proxy": float(persistence_proxy),
|
| 85 |
-
}
|
| 86 |
-
|
| 87 |
-
def combine_features(a, b, trend, graph):
|
| 88 |
-
return {**base_pair_features(a, b), **trend, **graph}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/graph_features.py
DELETED
|
@@ -1,26 +0,0 @@
|
|
| 1 |
-
import networkx as nx
|
| 2 |
-
|
| 3 |
-
def build_graph(candidate_pairs):
|
| 4 |
-
g = nx.Graph()
|
| 5 |
-
for a, b in candidate_pairs:
|
| 6 |
-
g.add_edge(a, b)
|
| 7 |
-
return g
|
| 8 |
-
|
| 9 |
-
def pair_graph_features(g, a, b):
|
| 10 |
-
degree_sum = float(g.degree(a) + g.degree(b))
|
| 11 |
-
common = len(list(nx.common_neighbors(g, a, b))) if a in g and b in g else 0
|
| 12 |
-
na = set(g.neighbors(a)) if a in g else set()
|
| 13 |
-
nb = set(g.neighbors(b)) if b in g else set()
|
| 14 |
-
union = len(na | nb)
|
| 15 |
-
inter = len(na & nb)
|
| 16 |
-
jaccard = float(inter / union) if union else 0.0
|
| 17 |
-
nodes = set([a, b]) | na | nb
|
| 18 |
-
sub = g.subgraph(nodes)
|
| 19 |
-
possible = max(1, len(nodes) * (len(nodes) - 1) / 2)
|
| 20 |
-
density = float(sub.number_of_edges() / possible)
|
| 21 |
-
return {
|
| 22 |
-
"graph_degree_sum": degree_sum,
|
| 23 |
-
"graph_common_neighbors": float(common),
|
| 24 |
-
"graph_jaccard": jaccard,
|
| 25 |
-
"graph_local_density": density,
|
| 26 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/main.py
DELETED
|
@@ -1,27 +0,0 @@
|
|
| 1 |
-
from fastapi import FastAPI
|
| 2 |
-
from fastapi.middleware.cors import CORSMiddleware
|
| 3 |
-
from app.config import settings
|
| 4 |
-
from app.database import Base, engine
|
| 5 |
-
from app.routers.health import router as health_router
|
| 6 |
-
from app.routers.objects import router as objects_router
|
| 7 |
-
from app.routers.pairs import router as pairs_router
|
| 8 |
-
from app.routers.admin import router as admin_router
|
| 9 |
-
from app.routers.analytics import router as analytics_router
|
| 10 |
-
from app.scripts.bootstrap_demo import bootstrap_if_needed
|
| 11 |
-
|
| 12 |
-
Base.metadata.create_all(bind=engine)
|
| 13 |
-
bootstrap_if_needed()
|
| 14 |
-
|
| 15 |
-
app = FastAPI(title=settings.APP_NAME, docs_url="/docs", redoc_url="/redoc", openapi_url="/openapi.json")
|
| 16 |
-
origins = [x.strip() for x in settings.ALLOWED_ORIGINS.split(",") if x.strip()]
|
| 17 |
-
app.add_middleware(CORSMiddleware, allow_origins=origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
| 18 |
-
|
| 19 |
-
@app.get("/")
|
| 20 |
-
def root():
|
| 21 |
-
return {"status": "ok", "message": "Space Risk Intelligence API is running", "docs": "/docs", "health": "/health"}
|
| 22 |
-
|
| 23 |
-
app.include_router(health_router)
|
| 24 |
-
app.include_router(objects_router)
|
| 25 |
-
app.include_router(pairs_router)
|
| 26 |
-
app.include_router(admin_router)
|
| 27 |
-
app.include_router(analytics_router)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/ml.py
DELETED
|
@@ -1,46 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
from pathlib import Path
|
| 3 |
-
import joblib
|
| 4 |
-
import numpy as np
|
| 5 |
-
from sklearn.ensemble import IsolationForest
|
| 6 |
-
from xgboost import XGBClassifier
|
| 7 |
-
from app.feature_engineering import FEATURE_COLUMNS, FEATURE_LABELS
|
| 8 |
-
|
| 9 |
-
BASE_DIR = Path(__file__).resolve().parents[1]
|
| 10 |
-
MODEL_DIR = BASE_DIR / "models"
|
| 11 |
-
MODEL_DIR.mkdir(exist_ok=True)
|
| 12 |
-
BASELINE_PATH = MODEL_DIR / "baseline_model.joblib"
|
| 13 |
-
ANOMALY_PATH = MODEL_DIR / "anomaly_model.joblib"
|
| 14 |
-
FEATURE_COLUMNS_PATH = MODEL_DIR / "feature_columns.json"
|
| 15 |
-
|
| 16 |
-
def train_models(X, y):
|
| 17 |
-
clf = XGBClassifier(
|
| 18 |
-
n_estimators=140, max_depth=5, learning_rate=0.06,
|
| 19 |
-
subsample=0.9, colsample_bytree=0.9, eval_metric="logloss",
|
| 20 |
-
random_state=42
|
| 21 |
-
)
|
| 22 |
-
clf.fit(X, y)
|
| 23 |
-
anomaly = IsolationForest(n_estimators=180, contamination=0.08, random_state=42)
|
| 24 |
-
anomaly.fit(X)
|
| 25 |
-
joblib.dump(clf, BASELINE_PATH)
|
| 26 |
-
joblib.dump(anomaly, ANOMALY_PATH)
|
| 27 |
-
FEATURE_COLUMNS_PATH.write_text(json.dumps(FEATURE_COLUMNS), encoding="utf-8")
|
| 28 |
-
return str(BASELINE_PATH)
|
| 29 |
-
|
| 30 |
-
def predict_local(feature_vector):
|
| 31 |
-
clf = joblib.load(BASELINE_PATH)
|
| 32 |
-
anomaly = joblib.load(ANOMALY_PATH)
|
| 33 |
-
x = np.array([feature_vector])
|
| 34 |
-
risk = float(clf.predict_proba(x)[0][1])
|
| 35 |
-
raw = float(anomaly.decision_function(x)[0])
|
| 36 |
-
anomaly_score = float(max(0.0, min(1.0, 1.0 - ((raw + 0.5) / 1.0))))
|
| 37 |
-
final = 0.72 * risk + 0.18 * anomaly_score + 0.10 * min(1.0, feature_vector[10] if len(feature_vector) > 10 else 0.0)
|
| 38 |
-
return risk, anomaly_score, float(max(0.0, min(1.0, final)))
|
| 39 |
-
|
| 40 |
-
def feature_importance():
|
| 41 |
-
clf = joblib.load(BASELINE_PATH)
|
| 42 |
-
vals = clf.feature_importances_.tolist()
|
| 43 |
-
out = []
|
| 44 |
-
for name, value in sorted(zip(FEATURE_COLUMNS, vals), key=lambda x: x[1], reverse=True):
|
| 45 |
-
out.append({"feature": name, "label": FEATURE_LABELS.get(name, name), "importance": float(value)})
|
| 46 |
-
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/models.py
DELETED
|
@@ -1,55 +0,0 @@
|
|
| 1 |
-
from datetime import datetime
|
| 2 |
-
from sqlalchemy import String, Float, Integer, DateTime, Text, Boolean
|
| 3 |
-
from sqlalchemy.orm import Mapped, mapped_column
|
| 4 |
-
from app.database import Base
|
| 5 |
-
|
| 6 |
-
class SpaceObject(Base):
|
| 7 |
-
__tablename__ = "space_objects"
|
| 8 |
-
object_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
| 9 |
-
norad_cat_id: Mapped[int | None] = mapped_column(Integer, index=True, nullable=True)
|
| 10 |
-
object_name: Mapped[str] = mapped_column(String(255), index=True)
|
| 11 |
-
object_type: Mapped[str | None] = mapped_column(String(64), nullable=True)
|
| 12 |
-
mean_motion: Mapped[float | None] = mapped_column(Float, nullable=True)
|
| 13 |
-
inclination: Mapped[float | None] = mapped_column(Float, nullable=True)
|
| 14 |
-
eccentricity: Mapped[float | None] = mapped_column(Float, nullable=True)
|
| 15 |
-
raan: Mapped[float | None] = mapped_column(Float, nullable=True)
|
| 16 |
-
bstar: Mapped[float | None] = mapped_column(Float, nullable=True)
|
| 17 |
-
launch_year: Mapped[int | None] = mapped_column(Integer, nullable=True)
|
| 18 |
-
inserted_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
| 19 |
-
|
| 20 |
-
class PairScore(Base):
|
| 21 |
-
__tablename__ = "pair_scores"
|
| 22 |
-
pair_id: Mapped[str] = mapped_column(String(128), primary_key=True)
|
| 23 |
-
primary_object_id: Mapped[str] = mapped_column(String(64), index=True)
|
| 24 |
-
secondary_object_id: Mapped[str] = mapped_column(String(64), index=True)
|
| 25 |
-
latest_run_id: Mapped[str] = mapped_column(String(64), index=True)
|
| 26 |
-
risk_score: Mapped[float] = mapped_column(Float, index=True)
|
| 27 |
-
anomaly_score: Mapped[float] = mapped_column(Float, index=True)
|
| 28 |
-
final_score: Mapped[float] = mapped_column(Float, index=True)
|
| 29 |
-
risk_label: Mapped[str] = mapped_column(String(32), index=True)
|
| 30 |
-
recurrence_count: Mapped[int] = mapped_column(Integer, default=1)
|
| 31 |
-
trend_delta_24h: Mapped[float | None] = mapped_column(Float, nullable=True)
|
| 32 |
-
shell_key: Mapped[str | None] = mapped_column(String(128), index=True, nullable=True)
|
| 33 |
-
top_factors_json: Mapped[str] = mapped_column(Text)
|
| 34 |
-
feature_payload_json: Mapped[str] = mapped_column(Text)
|
| 35 |
-
updated_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
| 36 |
-
|
| 37 |
-
class PairScoreHistory(Base):
|
| 38 |
-
__tablename__ = "pair_score_history"
|
| 39 |
-
history_id: Mapped[str] = mapped_column(String(128), primary_key=True)
|
| 40 |
-
pair_id: Mapped[str] = mapped_column(String(128), index=True)
|
| 41 |
-
run_id: Mapped[str] = mapped_column(String(64), index=True)
|
| 42 |
-
risk_score: Mapped[float] = mapped_column(Float)
|
| 43 |
-
anomaly_score: Mapped[float] = mapped_column(Float)
|
| 44 |
-
final_score: Mapped[float] = mapped_column(Float)
|
| 45 |
-
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
| 46 |
-
|
| 47 |
-
class ScoringRun(Base):
|
| 48 |
-
__tablename__ = "scoring_runs"
|
| 49 |
-
run_id: Mapped[str] = mapped_column(String(64), primary_key=True)
|
| 50 |
-
source: Mapped[str] = mapped_column(String(64))
|
| 51 |
-
object_count: Mapped[int] = mapped_column(Integer, default=0)
|
| 52 |
-
candidate_pair_count: Mapped[int] = mapped_column(Integer, default=0)
|
| 53 |
-
scored_pair_count: Mapped[int] = mapped_column(Integer, default=0)
|
| 54 |
-
completed: Mapped[bool] = mapped_column(Boolean, default=False)
|
| 55 |
-
created_at: Mapped[datetime] = mapped_column(DateTime, default=datetime.utcnow)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/repository.py
DELETED
|
@@ -1,56 +0,0 @@
|
|
| 1 |
-
from sqlalchemy.orm import Session
|
| 2 |
-
from sqlalchemy import select, desc, or_
|
| 3 |
-
from app.models import SpaceObject, PairScore, PairScoreHistory, ScoringRun
|
| 4 |
-
|
| 5 |
-
def upsert_space_object(db: Session, payload):
|
| 6 |
-
obj = db.get(SpaceObject, payload["object_id"])
|
| 7 |
-
if obj:
|
| 8 |
-
for k, v in payload.items():
|
| 9 |
-
setattr(obj, k, v)
|
| 10 |
-
db.add(obj)
|
| 11 |
-
else:
|
| 12 |
-
db.add(SpaceObject(**payload))
|
| 13 |
-
|
| 14 |
-
def list_objects(db: Session, limit=100):
|
| 15 |
-
return db.scalars(select(SpaceObject).limit(limit)).all()
|
| 16 |
-
|
| 17 |
-
def get_object(db: Session, object_id):
|
| 18 |
-
return db.get(SpaceObject, object_id)
|
| 19 |
-
|
| 20 |
-
def save_pair_score(db: Session, payload):
|
| 21 |
-
row = db.get(PairScore, payload["pair_id"])
|
| 22 |
-
if row:
|
| 23 |
-
for k, v in payload.items():
|
| 24 |
-
setattr(row, k, v)
|
| 25 |
-
db.add(row)
|
| 26 |
-
else:
|
| 27 |
-
db.add(PairScore(**payload))
|
| 28 |
-
|
| 29 |
-
def insert_pair_history(db: Session, payload):
|
| 30 |
-
db.add(PairScoreHistory(**payload))
|
| 31 |
-
|
| 32 |
-
def list_high_risk_pairs(db: Session, limit=50):
|
| 33 |
-
return db.scalars(select(PairScore).order_by(desc(PairScore.final_score)).limit(limit)).all()
|
| 34 |
-
|
| 35 |
-
def get_pair(db: Session, pair_id):
|
| 36 |
-
return db.get(PairScore, pair_id)
|
| 37 |
-
|
| 38 |
-
def get_pair_history(db: Session, pair_id, limit=20):
|
| 39 |
-
return db.scalars(select(PairScoreHistory).where(PairScoreHistory.pair_id == pair_id).order_by(desc(PairScoreHistory.created_at)).limit(limit)).all()
|
| 40 |
-
|
| 41 |
-
def create_run(db: Session, payload):
|
| 42 |
-
db.add(ScoringRun(**payload))
|
| 43 |
-
|
| 44 |
-
def latest_runs(db: Session, limit=10):
|
| 45 |
-
return db.scalars(select(ScoringRun).order_by(desc(ScoringRun.created_at)).limit(limit)).all()
|
| 46 |
-
|
| 47 |
-
def get_run(db: Session, run_id):
|
| 48 |
-
return db.get(ScoringRun, run_id)
|
| 49 |
-
|
| 50 |
-
def object_pairs(db: Session, object_id: str, limit=25):
|
| 51 |
-
stmt = select(PairScore).where(or_(PairScore.primary_object_id == object_id, PairScore.secondary_object_id == object_id)).order_by(desc(PairScore.final_score)).limit(limit)
|
| 52 |
-
return db.scalars(stmt).all()
|
| 53 |
-
|
| 54 |
-
def pairs_in_same_shell(db: Session, shell_key: str, exclude_pair_id: str, limit=20):
|
| 55 |
-
stmt = select(PairScore).where(PairScore.shell_key == shell_key, PairScore.pair_id != exclude_pair_id).order_by(desc(PairScore.final_score)).limit(limit)
|
| 56 |
-
return db.scalars(stmt).all()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/routers/__init__.py
DELETED
|
File without changes
|
app/routers/admin.py
DELETED
|
@@ -1,53 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
from fastapi import APIRouter, Depends
|
| 3 |
-
from app.database import get_db
|
| 4 |
-
from app.feature_engineering import normalize_object
|
| 5 |
-
from app.ingestion import fetch_celestrak_json
|
| 6 |
-
from app.repository import list_high_risk_pairs, latest_runs, get_run
|
| 7 |
-
from app.services import scoring_cycle, demo_objects
|
| 8 |
-
from app.scripts.train_baseline import run_training
|
| 9 |
-
|
| 10 |
-
router = APIRouter(prefix="/api/v1", tags=["admin"])
|
| 11 |
-
|
| 12 |
-
@router.post("/ingest/celestrak")
|
| 13 |
-
def ingest_celestrak(db=Depends(get_db)):
|
| 14 |
-
return {"status": "ok", **scoring_cycle(db, [normalize_object(x) for x in fetch_celestrak_json()], source="celestrak")}
|
| 15 |
-
|
| 16 |
-
@router.post("/score/demo-cycle")
|
| 17 |
-
def score_demo_cycle(db=Depends(get_db)):
|
| 18 |
-
return {"status": "ok", **scoring_cycle(db, demo_objects(db), source="demo")}
|
| 19 |
-
|
| 20 |
-
@router.post("/train/baseline")
|
| 21 |
-
def train_baseline():
|
| 22 |
-
model_path, rows, metrics = run_training()
|
| 23 |
-
return {"status": "ok", "model_path": model_path, "rows_used": rows, "metrics": metrics}
|
| 24 |
-
|
| 25 |
-
@router.get("/alerts/live")
|
| 26 |
-
def alerts_live(limit: int = 25, db=Depends(get_db)):
|
| 27 |
-
rows = list_high_risk_pairs(db, limit)
|
| 28 |
-
return [{"pair_id": r.pair_id, "final_score": r.final_score, "risk_label": r.risk_label, "top_factors": json.loads(r.top_factors_json)} for r in rows]
|
| 29 |
-
|
| 30 |
-
@router.get("/runs")
|
| 31 |
-
def runs(db=Depends(get_db)):
|
| 32 |
-
return [{"run_id": r.run_id, "source": r.source, "object_count": r.object_count, "candidate_pair_count": r.candidate_pair_count, "scored_pair_count": r.scored_pair_count, "completed": r.completed, "created_at": r.created_at} for r in latest_runs(db, 10)]
|
| 33 |
-
|
| 34 |
-
@router.get("/runs/{run_id}")
|
| 35 |
-
def run_detail(run_id: str, db=Depends(get_db)):
|
| 36 |
-
r = get_run(db, run_id)
|
| 37 |
-
if not r:
|
| 38 |
-
return {"detail": "Run not found"}
|
| 39 |
-
rows = [x for x in list_high_risk_pairs(db, 200) if x.latest_run_id == run_id]
|
| 40 |
-
labels = {"low": 0, "medium": 0, "high": 0, "critical": 0}
|
| 41 |
-
for x in rows:
|
| 42 |
-
labels[x.risk_label] = labels.get(x.risk_label, 0) + 1
|
| 43 |
-
return {
|
| 44 |
-
"run_id": r.run_id,
|
| 45 |
-
"source": r.source,
|
| 46 |
-
"object_count": r.object_count,
|
| 47 |
-
"candidate_pair_count": r.candidate_pair_count,
|
| 48 |
-
"scored_pair_count": r.scored_pair_count,
|
| 49 |
-
"completed": r.completed,
|
| 50 |
-
"created_at": r.created_at,
|
| 51 |
-
"label_distribution": labels,
|
| 52 |
-
"top_pairs": [{"pair_id": x.pair_id, "final_score": x.final_score, "risk_label": x.risk_label} for x in sorted(rows, key=lambda z: z.final_score, reverse=True)[:20]],
|
| 53 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/routers/analytics.py
DELETED
|
@@ -1,18 +0,0 @@
|
|
| 1 |
-
from fastapi import APIRouter, Depends
|
| 2 |
-
from app.database import get_db
|
| 3 |
-
from app.analytics import shell_analytics, diagnostics_summary
|
| 4 |
-
from app.ml import feature_importance
|
| 5 |
-
|
| 6 |
-
router = APIRouter(prefix="/api/v1", tags=["analytics"])
|
| 7 |
-
|
| 8 |
-
@router.get("/analytics/shells")
|
| 9 |
-
def get_shell_analytics(limit: int = 10, db=Depends(get_db)):
|
| 10 |
-
return shell_analytics(db, limit)
|
| 11 |
-
|
| 12 |
-
@router.get("/diagnostics/summary")
|
| 13 |
-
def get_diagnostics_summary(db=Depends(get_db)):
|
| 14 |
-
return diagnostics_summary(db)
|
| 15 |
-
|
| 16 |
-
@router.get("/model/feature-importance")
|
| 17 |
-
def get_feature_importance():
|
| 18 |
-
return feature_importance()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/routers/health.py
DELETED
|
@@ -1,8 +0,0 @@
|
|
| 1 |
-
from fastapi import APIRouter
|
| 2 |
-
from app.config import settings
|
| 3 |
-
|
| 4 |
-
router = APIRouter()
|
| 5 |
-
|
| 6 |
-
@router.get("/health")
|
| 7 |
-
def health():
|
| 8 |
-
return {"status": "ok", "app": settings.APP_NAME}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/routers/objects.py
DELETED
|
@@ -1,22 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
from fastapi import APIRouter, Depends, HTTPException
|
| 3 |
-
from app.database import get_db
|
| 4 |
-
from app.repository import list_objects, get_object, object_pairs
|
| 5 |
-
|
| 6 |
-
router = APIRouter(prefix="/api/v1/objects", tags=["objects"])
|
| 7 |
-
|
| 8 |
-
@router.get("")
|
| 9 |
-
def get_objects(limit: int = 100, db=Depends(get_db)):
|
| 10 |
-
return list_objects(db, limit)
|
| 11 |
-
|
| 12 |
-
@router.get("/{object_id}")
|
| 13 |
-
def get_object_detail(object_id: str, db=Depends(get_db)):
|
| 14 |
-
obj = get_object(db, object_id)
|
| 15 |
-
if not obj:
|
| 16 |
-
raise HTTPException(status_code=404, detail="Object not found")
|
| 17 |
-
return obj
|
| 18 |
-
|
| 19 |
-
@router.get("/{object_id}/pairs")
|
| 20 |
-
def get_object_related_pairs(object_id: str, limit: int = 25, db=Depends(get_db)):
|
| 21 |
-
rows = object_pairs(db, object_id, limit)
|
| 22 |
-
return [{"pair_id": r.pair_id, "final_score": r.final_score, "risk_label": r.risk_label, "top_factors": json.loads(r.top_factors_json)} for r in rows]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/routers/pairs.py
DELETED
|
@@ -1,63 +0,0 @@
|
|
| 1 |
-
import json
|
| 2 |
-
from fastapi import APIRouter, Depends, HTTPException
|
| 3 |
-
from app.database import get_db
|
| 4 |
-
from app.feature_engineering import normalize_object
|
| 5 |
-
from app.services import score_pair
|
| 6 |
-
from app.repository import list_high_risk_pairs, get_pair, get_pair_history, pairs_in_same_shell, object_pairs
|
| 7 |
-
|
| 8 |
-
router = APIRouter(prefix="/api/v1", tags=["pairs"])
|
| 9 |
-
|
| 10 |
-
@router.post("/score/pair")
|
| 11 |
-
def score_pair_route(payload: dict, db=Depends(get_db)):
|
| 12 |
-
return score_pair(db, normalize_object(payload["primary"]), normalize_object(payload["secondary"]))
|
| 13 |
-
|
| 14 |
-
@router.get("/pairs/high-risk")
|
| 15 |
-
def high_risk(limit: int = 50, db=Depends(get_db)):
|
| 16 |
-
rows = list_high_risk_pairs(db, limit)
|
| 17 |
-
return [{"pair_id": r.pair_id, "risk_score": r.risk_score, "anomaly_score": r.anomaly_score, "final_score": r.final_score, "risk_label": r.risk_label, "recurrence_count": r.recurrence_count, "trend_delta_24h": r.trend_delta_24h, "shell_key": r.shell_key, "top_factors": json.loads(r.top_factors_json)} for r in rows]
|
| 18 |
-
|
| 19 |
-
@router.get("/pairs/{pair_id}")
|
| 20 |
-
def pair_detail(pair_id: str, db=Depends(get_db)):
|
| 21 |
-
row = get_pair(db, pair_id)
|
| 22 |
-
if not row:
|
| 23 |
-
raise HTTPException(status_code=404, detail="Pair not found")
|
| 24 |
-
payload = json.loads(row.feature_payload_json)
|
| 25 |
-
return {
|
| 26 |
-
"pair_id": row.pair_id,
|
| 27 |
-
"primary_object_id": row.primary_object_id,
|
| 28 |
-
"secondary_object_id": row.secondary_object_id,
|
| 29 |
-
"risk_score": row.risk_score,
|
| 30 |
-
"anomaly_score": row.anomaly_score,
|
| 31 |
-
"final_score": row.final_score,
|
| 32 |
-
"risk_label": row.risk_label,
|
| 33 |
-
"recurrence_count": row.recurrence_count,
|
| 34 |
-
"trend_delta_24h": row.trend_delta_24h,
|
| 35 |
-
"shell_key": row.shell_key,
|
| 36 |
-
"top_factors": json.loads(row.top_factors_json),
|
| 37 |
-
"features": payload,
|
| 38 |
-
"analyst_summary": payload.get("analyst_summary", ""),
|
| 39 |
-
"structured_explanation": payload.get("structured_explanation", {}),
|
| 40 |
-
}
|
| 41 |
-
|
| 42 |
-
@router.get("/pairs/{pair_id}/history")
|
| 43 |
-
def pair_history(pair_id: str, limit: int = 20, db=Depends(get_db)):
|
| 44 |
-
rows = get_pair_history(db, pair_id, limit)
|
| 45 |
-
return [{"history_id": r.history_id, "run_id": r.run_id, "risk_score": r.risk_score, "anomaly_score": r.anomaly_score, "final_score": r.final_score, "created_at": r.created_at} for r in rows]
|
| 46 |
-
|
| 47 |
-
@router.get("/pairs/{pair_id}/neighbors")
|
| 48 |
-
def pair_neighbors(pair_id: str, limit: int = 20, db=Depends(get_db)):
|
| 49 |
-
row = get_pair(db, pair_id)
|
| 50 |
-
if not row:
|
| 51 |
-
raise HTTPException(status_code=404, detail="Pair not found")
|
| 52 |
-
shell_rows = pairs_in_same_shell(db, row.shell_key or "unknown", row.pair_id, limit)
|
| 53 |
-
object_related = object_pairs(db, row.primary_object_id, limit) + object_pairs(db, row.secondary_object_id, limit)
|
| 54 |
-
seen = {pair_id}
|
| 55 |
-
related = []
|
| 56 |
-
for r in shell_rows + object_related:
|
| 57 |
-
if r.pair_id in seen:
|
| 58 |
-
continue
|
| 59 |
-
seen.add(r.pair_id)
|
| 60 |
-
related.append({"pair_id": r.pair_id, "final_score": r.final_score, "risk_label": r.risk_label, "top_factors": json.loads(r.top_factors_json)})
|
| 61 |
-
if len(related) >= limit:
|
| 62 |
-
break
|
| 63 |
-
return related
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/scripts/bootstrap_demo.py
DELETED
|
@@ -1,28 +0,0 @@
|
|
| 1 |
-
from pathlib import Path
|
| 2 |
-
from app.database import Base, engine, SessionLocal
|
| 3 |
-
from app.repository import list_objects, latest_runs
|
| 4 |
-
from app.scripts.seed_synthetic import main as seed_main
|
| 5 |
-
from app.scripts.train_baseline import run_training
|
| 6 |
-
from app.services import scoring_cycle, demo_objects
|
| 7 |
-
|
| 8 |
-
def bootstrap_if_needed():
|
| 9 |
-
Base.metadata.create_all(bind=engine)
|
| 10 |
-
model_path = Path(__file__).resolve().parents[2] / "models" / "baseline_model.joblib"
|
| 11 |
-
db = SessionLocal()
|
| 12 |
-
try:
|
| 13 |
-
has_objects = bool(list_objects(db, 5))
|
| 14 |
-
finally:
|
| 15 |
-
db.close()
|
| 16 |
-
if not has_objects:
|
| 17 |
-
seed_main()
|
| 18 |
-
if not model_path.exists():
|
| 19 |
-
run_training()
|
| 20 |
-
db = SessionLocal()
|
| 21 |
-
try:
|
| 22 |
-
if not latest_runs(db, 1):
|
| 23 |
-
scoring_cycle(db, demo_objects(db), source="bootstrap-demo")
|
| 24 |
-
finally:
|
| 25 |
-
db.close()
|
| 26 |
-
|
| 27 |
-
if __name__ == "__main__":
|
| 28 |
-
bootstrap_if_needed()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/scripts/seed_synthetic.py
DELETED
|
@@ -1,36 +0,0 @@
|
|
| 1 |
-
import random
|
| 2 |
-
from app.database import Base, engine, SessionLocal
|
| 3 |
-
from app.repository import upsert_space_object
|
| 4 |
-
|
| 5 |
-
def synthetic_objects(n=300):
|
| 6 |
-
items = []
|
| 7 |
-
for i in range(n):
|
| 8 |
-
shell = random.choice([(15.2, 53), (14.9, 98), (13.8, 74), (2.0, 0), (12.5, 55)])
|
| 9 |
-
mm, inc = shell
|
| 10 |
-
items.append({
|
| 11 |
-
"object_id": f"OBJ-{i+1}",
|
| 12 |
-
"norad_cat_id": 10000 + i,
|
| 13 |
-
"object_name": f"SIM_OBJECT_{i+1}",
|
| 14 |
-
"object_type": random.choice(["PAYLOAD", "DEBRIS", "ROCKET BODY"]),
|
| 15 |
-
"mean_motion": round(random.gauss(mm, 0.12), 5),
|
| 16 |
-
"inclination": round(random.gauss(inc, 1.4), 5),
|
| 17 |
-
"eccentricity": round(abs(random.gauss(0.001, 0.002)), 6),
|
| 18 |
-
"raan": round(random.uniform(0, 360), 4),
|
| 19 |
-
"bstar": round(random.uniform(0.00001, 0.005), 8),
|
| 20 |
-
"launch_year": random.randint(1998, 2025),
|
| 21 |
-
})
|
| 22 |
-
return items
|
| 23 |
-
|
| 24 |
-
def main():
|
| 25 |
-
Base.metadata.create_all(bind=engine)
|
| 26 |
-
db = SessionLocal()
|
| 27 |
-
try:
|
| 28 |
-
for item in synthetic_objects():
|
| 29 |
-
upsert_space_object(db, item)
|
| 30 |
-
db.commit()
|
| 31 |
-
print("Synthetic objects loaded.")
|
| 32 |
-
finally:
|
| 33 |
-
db.close()
|
| 34 |
-
|
| 35 |
-
if __name__ == "__main__":
|
| 36 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/scripts/train_baseline.py
DELETED
|
@@ -1,35 +0,0 @@
|
|
| 1 |
-
import json, random
|
| 2 |
-
import numpy as np
|
| 3 |
-
import pandas as pd
|
| 4 |
-
from sklearn.metrics import roc_auc_score
|
| 5 |
-
from app.database import SessionLocal
|
| 6 |
-
from app.repository import list_objects
|
| 7 |
-
from app.feature_engineering import FEATURE_COLUMNS, combine_features
|
| 8 |
-
from app.graph_features import build_graph, pair_graph_features
|
| 9 |
-
from app.ml import train_models
|
| 10 |
-
|
| 11 |
-
def run_training():
|
| 12 |
-
db = SessionLocal()
|
| 13 |
-
try:
|
| 14 |
-
objs = list_objects(db, 5000)
|
| 15 |
-
objects = [{"object_id": o.object_id, "object_type": o.object_type, "mean_motion": o.mean_motion, "inclination": o.inclination, "eccentricity": o.eccentricity, "raan": o.raan, "bstar": o.bstar, "launch_year": o.launch_year} for o in objs]
|
| 16 |
-
finally:
|
| 17 |
-
db.close()
|
| 18 |
-
pairs = [tuple(random.sample(objects, 2)) for _ in range(4000)]
|
| 19 |
-
g = build_graph([(a["object_id"], b["object_id"]) for a, b in pairs[:1000]])
|
| 20 |
-
rows, raw_scores = [], []
|
| 21 |
-
for a, b in pairs:
|
| 22 |
-
trend = {"recurrence_count": float(random.choice([0,1,2,3,4])), "trend_delta_score": float(random.uniform(-0.1, 0.3)), "score_volatility_proxy": float(random.uniform(0, 0.2))}
|
| 23 |
-
f = combine_features(a, b, trend, pair_graph_features(g, a["object_id"], b["object_id"]))
|
| 24 |
-
score = 0.30*f["close_approach_proxy"] + 0.16*f["same_shell"] + 0.10*min(1.0,f["shell_density_proxy"]/12.0) + 0.10*min(1.0,f["graph_local_density"]*2.0) + 0.09*min(1.0,f["graph_jaccard"]) + 0.10*min(1.0,f["recurrence_count"]/5.0) + 0.08*max(0.0,f["trend_delta_score"]) + np.random.normal(0,0.05)
|
| 25 |
-
y = 1 if score > 0.48 else 0
|
| 26 |
-
rows.append({**f, "label": y})
|
| 27 |
-
raw_scores.append(score)
|
| 28 |
-
df = pd.DataFrame(rows)
|
| 29 |
-
path = train_models(df[FEATURE_COLUMNS].values, df["label"].values)
|
| 30 |
-
auc = float(roc_auc_score(df["label"].values, np.array(raw_scores)))
|
| 31 |
-
return path, len(df), {"pseudo_auc": round(auc, 4), "rows": int(len(df))}
|
| 32 |
-
|
| 33 |
-
if __name__ == "__main__":
|
| 34 |
-
p, r, m = run_training()
|
| 35 |
-
print(json.dumps({"model_path": p, "rows": r, "metrics": m}, indent=2))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/services.py
DELETED
|
@@ -1,109 +0,0 @@
|
|
| 1 |
-
from itertools import combinations
|
| 2 |
-
from app.config import settings
|
| 3 |
-
from app.feature_engineering import normalize_object, combine_features, orbital_shell_key, FEATURE_COLUMNS
|
| 4 |
-
from app.graph_features import build_graph, pair_graph_features
|
| 5 |
-
from app.repository import upsert_space_object, save_pair_score, insert_pair_history, create_run, get_pair_history, list_objects
|
| 6 |
-
from app.ml import predict_local
|
| 7 |
-
from app.utils import new_id, dumps
|
| 8 |
-
from app.explanations import build_top_factors, analyst_summary, structured_explanation, recommended_action
|
| 9 |
-
|
| 10 |
-
def demo_objects(db, limit=200):
|
| 11 |
-
rows = list_objects(db, limit=limit)
|
| 12 |
-
return [{"object_id": r.object_id, "object_name": r.object_name, "object_type": r.object_type, "mean_motion": r.mean_motion, "inclination": r.inclination, "eccentricity": r.eccentricity, "raan": r.raan, "bstar": r.bstar, "launch_year": r.launch_year} for r in rows]
|
| 13 |
-
|
| 14 |
-
def generate_candidate_pairs(objects):
|
| 15 |
-
grouped = {}
|
| 16 |
-
for obj in objects:
|
| 17 |
-
key = orbital_shell_key(obj)
|
| 18 |
-
grouped.setdefault(key, []).append(obj)
|
| 19 |
-
candidates = []
|
| 20 |
-
for group in grouped.values():
|
| 21 |
-
if len(group) < 2:
|
| 22 |
-
continue
|
| 23 |
-
for a, b in combinations(group[:120], 2):
|
| 24 |
-
candidates.append((a, b))
|
| 25 |
-
if len(candidates) >= settings.MAX_CANDIDATE_PAIRS:
|
| 26 |
-
return candidates
|
| 27 |
-
return candidates
|
| 28 |
-
|
| 29 |
-
def _trend_features(db, pair_id):
|
| 30 |
-
hist = get_pair_history(db, pair_id, limit=10)
|
| 31 |
-
if len(hist) < 2:
|
| 32 |
-
return {"recurrence_count": float(len(hist)), "trend_delta_score": 0.0, "score_volatility_proxy": 0.0}
|
| 33 |
-
scores = [h.final_score for h in hist]
|
| 34 |
-
avg = sum(scores) / len(scores)
|
| 35 |
-
vol = sum(abs(x - avg) for x in scores) / len(scores)
|
| 36 |
-
return {"recurrence_count": float(len(hist)), "trend_delta_score": float(scores[0] - scores[-1]), "score_volatility_proxy": float(vol)}
|
| 37 |
-
|
| 38 |
-
def score_pair(db, a, b, graph_feats=None):
|
| 39 |
-
pair_id = f"{a['object_id']}__{b['object_id']}"
|
| 40 |
-
trend = _trend_features(db, pair_id)
|
| 41 |
-
graph = graph_feats or {"graph_degree_sum": 0.0, "graph_common_neighbors": 0.0, "graph_jaccard": 0.0, "graph_local_density": 0.0}
|
| 42 |
-
features = combine_features(a, b, trend, graph)
|
| 43 |
-
vector = [float(features.get(c, 0.0)) for c in FEATURE_COLUMNS]
|
| 44 |
-
risk, anomaly, final = predict_local(vector)
|
| 45 |
-
label = "critical" if final >= 0.9 else "high" if final >= 0.75 else "medium" if final >= 0.45 else "low"
|
| 46 |
-
top = build_top_factors(features, anomaly, final)
|
| 47 |
-
action = recommended_action(label)
|
| 48 |
-
summary = analyst_summary(features, top, final)
|
| 49 |
-
structured = structured_explanation(features, top, final, action)
|
| 50 |
-
return {
|
| 51 |
-
"pair_id": pair_id,
|
| 52 |
-
"risk_score": risk,
|
| 53 |
-
"anomaly_score": anomaly,
|
| 54 |
-
"final_score": final,
|
| 55 |
-
"risk_label": label,
|
| 56 |
-
"top_factors": top,
|
| 57 |
-
"analyst_summary": summary,
|
| 58 |
-
"structured_explanation": structured,
|
| 59 |
-
"recommended_action": action,
|
| 60 |
-
"features": features,
|
| 61 |
-
}
|
| 62 |
-
|
| 63 |
-
def scoring_cycle(db, objects, source="demo"):
|
| 64 |
-
run_id = new_id("run")
|
| 65 |
-
create_run(db, {"run_id": run_id, "source": source, "object_count": len(objects), "candidate_pair_count": 0, "scored_pair_count": 0, "completed": False})
|
| 66 |
-
for obj in objects:
|
| 67 |
-
upsert_space_object(db, obj)
|
| 68 |
-
db.commit()
|
| 69 |
-
candidates = generate_candidate_pairs(objects)
|
| 70 |
-
graph = build_graph([(a["object_id"], b["object_id"]) for a, b in candidates])
|
| 71 |
-
count = 0
|
| 72 |
-
for a, b in candidates:
|
| 73 |
-
graph_feats = pair_graph_features(graph, a["object_id"], b["object_id"])
|
| 74 |
-
result = score_pair(db, a, b, graph_feats)
|
| 75 |
-
hist = get_pair_history(db, result["pair_id"], limit=20)
|
| 76 |
-
recurrence = len(hist) + 1
|
| 77 |
-
trend_delta = result["final_score"] - hist[-1].final_score if hist else 0.0
|
| 78 |
-
save_pair_score(db, {
|
| 79 |
-
"pair_id": result["pair_id"],
|
| 80 |
-
"primary_object_id": a["object_id"],
|
| 81 |
-
"secondary_object_id": b["object_id"],
|
| 82 |
-
"latest_run_id": run_id,
|
| 83 |
-
"risk_score": result["risk_score"],
|
| 84 |
-
"anomaly_score": result["anomaly_score"],
|
| 85 |
-
"final_score": result["final_score"],
|
| 86 |
-
"risk_label": result["risk_label"],
|
| 87 |
-
"recurrence_count": recurrence,
|
| 88 |
-
"trend_delta_24h": trend_delta,
|
| 89 |
-
"shell_key": orbital_shell_key(a),
|
| 90 |
-
"top_factors_json": dumps(result["top_factors"]),
|
| 91 |
-
"feature_payload_json": dumps(result["features"] | {"analyst_summary": result["analyst_summary"], "structured_explanation": result["structured_explanation"]}),
|
| 92 |
-
})
|
| 93 |
-
insert_pair_history(db, {
|
| 94 |
-
"history_id": new_id("hist"),
|
| 95 |
-
"pair_id": result["pair_id"],
|
| 96 |
-
"run_id": run_id,
|
| 97 |
-
"risk_score": result["risk_score"],
|
| 98 |
-
"anomaly_score": result["anomaly_score"],
|
| 99 |
-
"final_score": result["final_score"],
|
| 100 |
-
})
|
| 101 |
-
count += 1
|
| 102 |
-
from app.models import ScoringRun
|
| 103 |
-
run = db.get(ScoringRun, run_id)
|
| 104 |
-
run.candidate_pair_count = len(candidates)
|
| 105 |
-
run.scored_pair_count = count
|
| 106 |
-
run.completed = True
|
| 107 |
-
db.add(run)
|
| 108 |
-
db.commit()
|
| 109 |
-
return {"run_id": run_id, "object_count": len(objects), "candidate_pair_count": len(candidates), "scored_pair_count": count}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
app/utils.py
DELETED
|
@@ -1,19 +0,0 @@
|
|
| 1 |
-
import json, uuid
|
| 2 |
-
|
| 3 |
-
def safe_float(v, d=0.0):
|
| 4 |
-
try:
|
| 5 |
-
return d if v in (None, "") else float(v)
|
| 6 |
-
except Exception:
|
| 7 |
-
return d
|
| 8 |
-
|
| 9 |
-
def safe_int(v, d=0):
|
| 10 |
-
try:
|
| 11 |
-
return d if v in (None, "") else int(v)
|
| 12 |
-
except Exception:
|
| 13 |
-
return d
|
| 14 |
-
|
| 15 |
-
def dumps(x):
|
| 16 |
-
return json.dumps(x, default=str)
|
| 17 |
-
|
| 18 |
-
def new_id(prefix):
|
| 19 |
-
return f"{prefix}_{uuid.uuid4().hex[:16]}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|