from app.utils import safe_float, safe_int FEATURE_COLUMNS = [ "delta_mean_motion","delta_inclination","delta_eccentricity","delta_raan","delta_bstar", "launch_year_gap","same_object_type","same_shell","shell_density_proxy","close_approach_proxy", "persistence_proxy","recurrence_count","trend_delta_score","score_volatility_proxy", "graph_degree_sum","graph_common_neighbors","graph_jaccard","graph_local_density", ] def normalize_object(raw): name = raw.get("OBJECT_NAME") or raw.get("object_name") or "UNKNOWN" norad = raw.get("NORAD_CAT_ID") or raw.get("norad_cat_id") intl = raw.get("OBJECT_ID") or raw.get("object_id") or "" launch_year = int(intl[:4]) if len(intl) >= 4 and intl[:4].isdigit() else None return { "object_id": str(norad or name), "norad_cat_id": safe_int(norad, 0) or None, "object_name": name, "object_type": raw.get("OBJECT_TYPE") or raw.get("object_type"), "mean_motion": safe_float(raw.get("MEAN_MOTION") or raw.get("mean_motion")), "inclination": safe_float(raw.get("INCLINATION") or raw.get("inclination")), "eccentricity": safe_float(raw.get("ECCENTRICITY") or raw.get("eccentricity")), "raan": safe_float(raw.get("RA_OF_ASC_NODE") or raw.get("raan")), "bstar": safe_float(raw.get("BSTAR") or raw.get("bstar")), "launch_year": launch_year, } def orbital_shell_key(obj): mm = safe_float(obj.get("mean_motion")) inc = safe_float(obj.get("inclination")) ecc = safe_float(obj.get("eccentricity")) return f"mm:{int(mm)}|inc:{int(inc//5)*5}|ecc:{int(ecc*1000)//10}" def base_pair_features(a, b): mm1, mm2 = safe_float(a.get("mean_motion")), safe_float(b.get("mean_motion")) inc1, inc2 = safe_float(a.get("inclination")), safe_float(b.get("inclination")) ecc1, ecc2 = safe_float(a.get("eccentricity")), safe_float(b.get("eccentricity")) raan1, raan2 = safe_float(a.get("raan")), safe_float(b.get("raan")) b1, b2 = safe_float(a.get("bstar")), safe_float(b.get("bstar")) ly1, ly2 = safe_int(a.get("launch_year")), safe_int(b.get("launch_year")) same_type = 1 if (a.get("object_type") or "") == (b.get("object_type") or "") else 0 same_shell = 1 if orbital_shell_key(a) == orbital_shell_key(b) else 0 delta_mm = abs(mm1 - mm2) delta_inc = abs(inc1 - inc2) delta_ecc = abs(ecc1 - ecc2) delta_raan = abs(raan1 - raan2) delta_bstar = abs(b1 - b2) launch_gap = abs(ly1 - ly2) if ly1 and ly2 else 25 shell_density_proxy = max(0.0, 10.0 - delta_mm) + max(0.0, 8.0 - delta_inc / 2.0) close_approach_proxy = 1.0 / (1.0 + delta_mm + delta_inc / 10.0 + delta_ecc * 50.0 + delta_raan / 60.0) persistence_proxy = 1.0 if same_shell else 0.25 return { "delta_mean_motion": delta_mm, "delta_inclination": delta_inc, "delta_eccentricity": delta_ecc, "delta_raan": delta_raan, "delta_bstar": delta_bstar, "launch_year_gap": float(launch_gap), "same_object_type": float(same_type), "same_shell": float(same_shell), "shell_density_proxy": float(shell_density_proxy), "close_approach_proxy": float(close_approach_proxy), "persistence_proxy": float(persistence_proxy), } def combine_features(a, b, trend, graph): return {**base_pair_features(a, b), **trend, **graph}