Adisri99 commited on
Commit
90f9113
·
verified ·
1 Parent(s): e8cca46

Delete app

Browse files
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]}"