| from itertools import combinations |
| from app.config import settings |
| from app.feature_engineering import combine_features, orbital_shell_key, FEATURE_COLUMNS |
| from app.graph_features import build_graph, pair_graph_features |
| from app.repository import upsert_space_object, save_pair_score, insert_pair_history, create_run, get_pair_history, list_objects |
| from app.ml import predict_local |
| from app.utils import new_id, dumps |
| from app.explanations import build_top_factors, analyst_summary, structured_explanation, recommended_action |
| def demo_objects(db,limit=200): |
| rows=list_objects(db,limit=limit) |
| 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] |
| def generate_candidate_pairs(objects): |
| grouped={} |
| for obj in objects: grouped.setdefault(orbital_shell_key(obj), []).append(obj) |
| candidates=[] |
| for group in grouped.values(): |
| if len(group)<2: continue |
| for a,b in combinations(group[:120],2): |
| candidates.append((a,b)) |
| if len(candidates)>=settings.MAX_CANDIDATE_PAIRS: return candidates |
| return candidates |
| def _trend_features(db,pair_id): |
| hist=get_pair_history(db,pair_id,limit=10) |
| if len(hist)<2: return {"recurrence_count":float(len(hist)),"trend_delta_score":0.0,"score_volatility_proxy":0.0} |
| scores=[h.final_score for h in hist]; avg=sum(scores)/len(scores); vol=sum(abs(x-avg) for x in scores)/len(scores) |
| return {"recurrence_count":float(len(hist)),"trend_delta_score":float(scores[0]-scores[-1]),"score_volatility_proxy":float(vol)} |
| def score_pair(db,a,b,graph_feats=None): |
| pair_id=f"{a['object_id']}__{b['object_id']}"; trend=_trend_features(db,pair_id); graph=graph_feats or {"graph_degree_sum":0.0,"graph_common_neighbors":0.0,"graph_jaccard":0.0,"graph_local_density":0.0} |
| features=combine_features(a,b,trend,graph); vector=[float(features.get(c,0.0)) for c in FEATURE_COLUMNS]; risk,anomaly,final=predict_local(vector) |
| label="critical" if final>=0.9 else "high" if final>=0.75 else "medium" if final>=0.45 else "low" |
| top=build_top_factors(features,anomaly,final); action=recommended_action(label); summary=analyst_summary(features,top,final); structured=structured_explanation(features,top,final,action) |
| return {"pair_id":pair_id,"risk_score":risk,"anomaly_score":anomaly,"final_score":final,"risk_label":label,"top_factors":top,"analyst_summary":summary,"structured_explanation":structured,"recommended_action":action,"features":features} |
| def scoring_cycle(db,objects,source="demo"): |
| run_id=new_id("run"); create_run(db,{"run_id":run_id,"source":source,"object_count":len(objects),"candidate_pair_count":0,"scored_pair_count":0,"completed":False}) |
| for obj in objects: upsert_space_object(db,obj) |
| db.commit(); candidates=generate_candidate_pairs(objects); graph=build_graph([(a["object_id"],b["object_id"]) for a,b in candidates]); count=0 |
| for a,b in candidates: |
| result=score_pair(db,a,b,pair_graph_features(graph,a["object_id"],b["object_id"])); hist=get_pair_history(db,result["pair_id"],limit=20); recurrence=len(hist)+1; trend_delta=result["final_score"]-hist[-1].final_score if hist else 0.0 |
| save_pair_score(db,{"pair_id":result["pair_id"],"primary_object_id":a["object_id"],"secondary_object_id":b["object_id"],"latest_run_id":run_id,"risk_score":result["risk_score"],"anomaly_score":result["anomaly_score"],"final_score":result["final_score"],"risk_label":result["risk_label"],"recurrence_count":recurrence,"trend_delta_24h":trend_delta,"shell_key":orbital_shell_key(a),"top_factors_json":dumps(result["top_factors"]),"feature_payload_json":dumps(result["features"]|{"analyst_summary":result["analyst_summary"],"structured_explanation":result["structured_explanation"]})}) |
| insert_pair_history(db,{"history_id":new_id("hist"),"pair_id":result["pair_id"],"run_id":run_id,"risk_score":result["risk_score"],"anomaly_score":result["anomaly_score"],"final_score":result["final_score"]}); count+=1 |
| from app.models import ScoringRun |
| run=db.get(ScoringRun,run_id); run.candidate_pair_count=len(candidates); run.scored_pair_count=count; run.completed=True; db.add(run); db.commit() |
| return {"run_id":run_id,"object_count":len(objects),"candidate_pair_count":len(candidates),"scored_pair_count":count} |
|
|