orbx 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- orbx/Configs.py +7 -0
- orbx/Models.py +27 -0
- orbx/__init__.py +5 -0
- orbx/clustering/Cluster.py +48 -0
- orbx/clustering/Core.py +44 -0
- orbx/clustering/Schema.py +41 -0
- orbx/clustering/__init__.py +3 -0
- orbx/clustering/algorithm_wrappers/DBSCANWrapper.py +216 -0
- orbx/clustering/algorithm_wrappers/HDBSCANWrapper.py +37 -0
- orbx/clustering/algorithm_wrappers/OPTICSWrapper.py +96 -0
- orbx/clustering/algorithm_wrappers/cluster_wrapper.py +12 -0
- orbx/clustering/data_handling/DataHandler.py +144 -0
- orbx/density/Density.py +74 -0
- orbx/density/__init__.py +3 -0
- orbx/density/tools/DMT.py +49 -0
- orbx/density/tools/distance_matrix.py +147 -0
- orbx/synthetic_orbits/DMT.py +49 -0
- orbx/synthetic_orbits/__init__.py +3 -0
- orbx/synthetic_orbits/data_handling/build_czml.py +176 -0
- orbx/synthetic_orbits/data_handling/ionop_czml.py +95 -0
- orbx/synthetic_orbits/orbit_finder/DMT.py +57 -0
- orbx/synthetic_orbits/orbit_finder/frechet_orbit_finder.py +215 -0
- orbx/synthetic_orbits/orbit_finder/get_optimum_orbit.py +524 -0
- orbx/synthetic_orbits/orbit_finder/max_separation_orbit_finder.py +173 -0
- orbx/synthetic_orbits/orbit_finder/optimum_orbit_tle.py +161 -0
- orbx/synthetic_orbits/orbit_finder/process_input.py +89 -0
- orbx/synthetic_orbits/orbit_finder/query_spacetrack.py +45 -0
- orbx/synthetic_orbits/process_input.py +53 -0
- orbx/synthetic_orbits/synthetic_orbit.py +108 -0
- orbx/tools/DMT.py +49 -0
- orbx/tools/distance_matrix.py +147 -0
- orbx-1.0.0.dist-info/METADATA +233 -0
- orbx-1.0.0.dist-info/RECORD +36 -0
- orbx-1.0.0.dist-info/WHEEL +5 -0
- orbx-1.0.0.dist-info/licenses/LICENSE +201 -0
- orbx-1.0.0.dist-info/top_level.txt +1 -0
orbx/density/Density.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import contextlib
|
|
2
|
+
import io
|
|
3
|
+
from orbx.clustering.Schema import Schema
|
|
4
|
+
from orbx.synthetic_orbits.orbit_finder.DMT import VectorizedKeplerianOrbit
|
|
5
|
+
import pandas as pd
|
|
6
|
+
|
|
7
|
+
def _cluster_density(df_cluster: pd.DataFrame, verbose: bool = False) -> float:
|
|
8
|
+
"""Calculate variance-style density for one cluster DataFrame."""
|
|
9
|
+
cluster_size = len(df_cluster)
|
|
10
|
+
if cluster_size < 2:
|
|
11
|
+
return 0.0
|
|
12
|
+
|
|
13
|
+
# Import here is because of Orekit dependency
|
|
14
|
+
# Keeping it at top level requires an orekit installation even if you don't use
|
|
15
|
+
# it directly.
|
|
16
|
+
from orbx.synthetic_orbits.orbit_finder.frechet_orbit_finder import frechet_orbit
|
|
17
|
+
|
|
18
|
+
if verbose:
|
|
19
|
+
frechet_orbit_result = frechet_orbit(df_cluster)
|
|
20
|
+
else:
|
|
21
|
+
# Suppress verbose optimizer logs unless explicitly requested.
|
|
22
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
23
|
+
frechet_orbit_result = frechet_orbit(df_cluster)
|
|
24
|
+
if frechet_orbit_result is None:
|
|
25
|
+
raise ValueError("Unable to compute Fréchet mean orbit for cluster.")
|
|
26
|
+
|
|
27
|
+
if isinstance(frechet_orbit_result, pd.DataFrame):
|
|
28
|
+
frechet_mean_orbit = frechet_orbit_result.iloc[-1]
|
|
29
|
+
else:
|
|
30
|
+
frechet_mean_orbit = frechet_orbit_result
|
|
31
|
+
|
|
32
|
+
if "line1" not in df_cluster.columns or "line2" not in df_cluster.columns:
|
|
33
|
+
raise ValueError("df_cluster must include 'line1' and 'line2' columns.")
|
|
34
|
+
if "line1" not in frechet_mean_orbit or "line2" not in frechet_mean_orbit:
|
|
35
|
+
raise ValueError("Fréchet mean orbit must include 'line1' and 'line2'.")
|
|
36
|
+
|
|
37
|
+
mean_orbit = VectorizedKeplerianOrbit(
|
|
38
|
+
pd.Series([frechet_mean_orbit["line1"]]).values,
|
|
39
|
+
pd.Series([frechet_mean_orbit["line2"]]).values,
|
|
40
|
+
)
|
|
41
|
+
cluster_orbits = VectorizedKeplerianOrbit(
|
|
42
|
+
df_cluster["line1"].values,
|
|
43
|
+
df_cluster["line2"].values,
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
distances = VectorizedKeplerianOrbit.DistanceMetric(mean_orbit, cluster_orbits).ravel()
|
|
47
|
+
total_squared_distance = float((distances**2).sum())
|
|
48
|
+
|
|
49
|
+
variance = total_squared_distance / (cluster_size - 1)
|
|
50
|
+
return float(variance)
|
|
51
|
+
|
|
52
|
+
def density(df: pd.DataFrame, label_column: str = "label", verbose: bool = False) -> pd.DataFrame:
|
|
53
|
+
"""
|
|
54
|
+
Compute density per label group and return a summary DataFrame.
|
|
55
|
+
|
|
56
|
+
Arguments
|
|
57
|
+
_______________
|
|
58
|
+
df : pd.DataFrame
|
|
59
|
+
Each row should contain the satellite TLE and the cluster label it belongs to (at a minimum). Your df should contain
|
|
60
|
+
a "line1", "line2", and "label" in each row, corresponding to a satellite. If missing label,
|
|
61
|
+
use cluster() to assign a label.
|
|
62
|
+
label_column : str
|
|
63
|
+
Name of the cluster label column. Defaults to "label".
|
|
64
|
+
verbose : bool
|
|
65
|
+
If True, print optimizer diagnostics while computing Fréchet means.
|
|
66
|
+
"""
|
|
67
|
+
Schema().validate(df)
|
|
68
|
+
|
|
69
|
+
results = []
|
|
70
|
+
for label, group in df.groupby(label_column, dropna=False):
|
|
71
|
+
cluster_density = _cluster_density(group, verbose=verbose)
|
|
72
|
+
results.append({"label": label, "density": cluster_density})
|
|
73
|
+
|
|
74
|
+
return pd.DataFrame(results)
|
orbx/density/__init__.py
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from sgp4.api import Satrec
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class VectorizedKeplerianOrbit:
|
|
5
|
+
def __init__(self, line1_array, line2_array=None):
|
|
6
|
+
if line2_array is not None:
|
|
7
|
+
satellites = np.vectorize(Satrec.twoline2rv)(line1_array, line2_array)
|
|
8
|
+
radius_earth_km = satellites[0].radiusearthkm
|
|
9
|
+
|
|
10
|
+
self.a = np.array([sat.a * radius_earth_km for sat in satellites])
|
|
11
|
+
self.e = np.array([sat.ecco for sat in satellites])
|
|
12
|
+
self.i = np.array([sat.inclo for sat in satellites])
|
|
13
|
+
self.omega = np.array([sat.argpo for sat in satellites])
|
|
14
|
+
self.raan = np.array([sat.nodeo for sat in satellites])
|
|
15
|
+
self.q = np.array([sat.altp * radius_earth_km for sat in satellites])
|
|
16
|
+
self.p = self.a * (1 - self.e ** 2)
|
|
17
|
+
else:
|
|
18
|
+
self.a, self.e, self.i, self.omega, self.raan, self.q, self.p = line1_array
|
|
19
|
+
|
|
20
|
+
def __getitem__(self, key):
|
|
21
|
+
return VectorizedKeplerianOrbit(
|
|
22
|
+
np.array([self.a[key], self.e[key], self.i[key], self.omega[key], self.raan[key], self.q[key], self.p[key]])
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def DistanceMetric(orbit1, orbit2):
|
|
27
|
+
w1 = orbit1.omega[:, np.newaxis]
|
|
28
|
+
w2 = orbit2.omega[np.newaxis, :]
|
|
29
|
+
|
|
30
|
+
c1 = np.cos(orbit1.i)[:, np.newaxis]
|
|
31
|
+
c2 = np.cos(orbit2.i)[np.newaxis, :]
|
|
32
|
+
|
|
33
|
+
s1 = np.sin(orbit1.i)[:, np.newaxis]
|
|
34
|
+
s2 = np.sin(orbit2.i)[np.newaxis, :]
|
|
35
|
+
|
|
36
|
+
delta = orbit1.raan[:, np.newaxis] - orbit2.raan[np.newaxis, :]
|
|
37
|
+
cosI = c1 * c2 + s1 * s2 * np.cos(delta)
|
|
38
|
+
|
|
39
|
+
cosP = s1 * s2 * np.sin(w1) * np.sin(w2) + \
|
|
40
|
+
(np.cos(w1) * np.cos(w2) + c1 * c2 * np.sin(w1) * np.sin(w2)) * np.cos(delta) + \
|
|
41
|
+
(c2 * np.cos(w1) * np.sin(w2) - c1 * np.sin(w1) * np.cos(w2)) * np.sin(delta)
|
|
42
|
+
|
|
43
|
+
q = (1 + orbit1.e[:, np.newaxis]**2) * orbit1.p[:, np.newaxis] + \
|
|
44
|
+
(1 + orbit2.e[np.newaxis, :]**2) * orbit2.p[np.newaxis, :] - \
|
|
45
|
+
2 * np.sqrt(orbit1.p[:, np.newaxis] * orbit2.p[np.newaxis, :]) * \
|
|
46
|
+
(cosI + orbit1.e[:, np.newaxis] * orbit2.e[np.newaxis, :] * cosP)
|
|
47
|
+
|
|
48
|
+
return q
|
|
49
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
from orbx.tools.DMT import VectorizedKeplerianOrbit
|
|
2
|
+
import pickle
|
|
3
|
+
from math import pi
|
|
4
|
+
import pandas as pd
|
|
5
|
+
import numpy as np
|
|
6
|
+
|
|
7
|
+
"""
|
|
8
|
+
This file is used to process the given elset data into a distance matrix and save
|
|
9
|
+
to disk.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
def get_distance_matrix(df, save=False):
|
|
13
|
+
|
|
14
|
+
line1 = df['line1'].values
|
|
15
|
+
line2 = df['line2'].values
|
|
16
|
+
|
|
17
|
+
print("Calculating orbits")
|
|
18
|
+
orbits = VectorizedKeplerianOrbit(line1, line2)
|
|
19
|
+
|
|
20
|
+
print("Calculating distances")
|
|
21
|
+
distance_matrix = VectorizedKeplerianOrbit.DistanceMetric(orbits, orbits)
|
|
22
|
+
if save:
|
|
23
|
+
print("Saving distance matrix to disk...")
|
|
24
|
+
with open(f'data/distance_matrix.pkl', 'wb') as f:
|
|
25
|
+
pickle.dump(distance_matrix, f)
|
|
26
|
+
|
|
27
|
+
key = get_key(df, save=save)
|
|
28
|
+
|
|
29
|
+
if _validate_matrix(distance_matrix):
|
|
30
|
+
return distance_matrix, key
|
|
31
|
+
|
|
32
|
+
def _validate_matrix(distance_matrix):
|
|
33
|
+
"""
|
|
34
|
+
Validate and sanitize distance matrix.
|
|
35
|
+
Modifies the matrix in place to fix common issues.
|
|
36
|
+
"""
|
|
37
|
+
# check if distance matrix is square
|
|
38
|
+
if distance_matrix.shape[0] != distance_matrix.shape[1]:
|
|
39
|
+
raise ValueError("Distance matrix is not square")
|
|
40
|
+
|
|
41
|
+
# Sanitize diagonal first (ensure it's zero)
|
|
42
|
+
np.fill_diagonal(distance_matrix, 0)
|
|
43
|
+
|
|
44
|
+
# Sanitize any negative values (numerical errors)
|
|
45
|
+
if np.any(distance_matrix < 0):
|
|
46
|
+
num_negatives = np.sum(distance_matrix < 0)
|
|
47
|
+
min_negative = distance_matrix[distance_matrix < 0].min()
|
|
48
|
+
print(f" Found {num_negatives} negative values (min: {min_negative:.2e}), setting to zero")
|
|
49
|
+
distance_matrix[distance_matrix < 0] = 0
|
|
50
|
+
|
|
51
|
+
# check if distance matrix is symmetric
|
|
52
|
+
if not np.allclose(distance_matrix, distance_matrix.T):
|
|
53
|
+
print(" WARNING: Matrix not symmetric. Symmetrizing...")
|
|
54
|
+
distance_matrix[:] = (distance_matrix + distance_matrix.T) / 2
|
|
55
|
+
|
|
56
|
+
print("Distance matrix is valid")
|
|
57
|
+
return True
|
|
58
|
+
|
|
59
|
+
def get_key(df, save=False):
|
|
60
|
+
"""These dictionaries map satellite numbers to their
|
|
61
|
+
index in the distance matrix and vice versa"""
|
|
62
|
+
|
|
63
|
+
df = df['satNo'].unique()
|
|
64
|
+
|
|
65
|
+
satNo_idx_dict = {}
|
|
66
|
+
idx_satNo_dict = {}
|
|
67
|
+
|
|
68
|
+
for i, satNo in enumerate(df):
|
|
69
|
+
idx_satNo_dict[i] = satNo
|
|
70
|
+
satNo_idx_dict[satNo] = i
|
|
71
|
+
|
|
72
|
+
if save:
|
|
73
|
+
# save both as pkl
|
|
74
|
+
with open(f'data/satNo_idx_dict.pkl', 'wb') as f:
|
|
75
|
+
pickle.dump(satNo_idx_dict, f)
|
|
76
|
+
|
|
77
|
+
with open(f'data/idx_satNo_dict.pkl', 'wb') as f:
|
|
78
|
+
pickle.dump(idx_satNo_dict, f)
|
|
79
|
+
|
|
80
|
+
# return both dictionaries
|
|
81
|
+
return {'satNo_idx_dict': satNo_idx_dict, 'idx_satNo_dict': idx_satNo_dict}
|
|
82
|
+
|
|
83
|
+
if __name__ == '__main__':
|
|
84
|
+
inclination_range = (0, 180) # degrees
|
|
85
|
+
apogee_range = (0, 2000) # kilometers
|
|
86
|
+
# get all satellites from celestrak data and create distance matrix
|
|
87
|
+
celestrak_tles_path = 'data/3le'
|
|
88
|
+
data = []
|
|
89
|
+
|
|
90
|
+
with open(celestrak_tles_path, 'r') as f:
|
|
91
|
+
lines = f.readlines()
|
|
92
|
+
|
|
93
|
+
earth_radius = 6371000 # meters
|
|
94
|
+
GM_earth = 3.986004418e14 # m^3/s^2
|
|
95
|
+
seconds_in_day = 86400 # seconds per day
|
|
96
|
+
|
|
97
|
+
for i in range(0, len(lines), 3):
|
|
98
|
+
if i + 2 < len(lines):
|
|
99
|
+
name = lines[i].strip()
|
|
100
|
+
line1 = lines[i + 1].strip()
|
|
101
|
+
line2 = lines[i + 2].strip()
|
|
102
|
+
|
|
103
|
+
if not (line1.startswith('1 ') and line2.startswith('2 ')):
|
|
104
|
+
continue
|
|
105
|
+
|
|
106
|
+
sat_no = line1[2:7].strip()
|
|
107
|
+
|
|
108
|
+
inclination = float(line2[8:16].strip())
|
|
109
|
+
|
|
110
|
+
# Extract mean motion (revolutions per day)
|
|
111
|
+
mean_motion = float(line2[52:63].strip())
|
|
112
|
+
|
|
113
|
+
# Extract eccentricity (columns 27-33 in standard TLE format)
|
|
114
|
+
eccentricity = float("0." + line2[26:33].strip())
|
|
115
|
+
|
|
116
|
+
# Calculate angular velocity n in radians per second
|
|
117
|
+
n = mean_motion * 2 * pi / seconds_in_day
|
|
118
|
+
|
|
119
|
+
# Calculate semi-major axis a in meters
|
|
120
|
+
a = (GM_earth / (n ** 2)) ** (1/3)
|
|
121
|
+
|
|
122
|
+
# Calculate apogee in kilometers
|
|
123
|
+
apogee = (a * (1 + eccentricity) - earth_radius) / 1000
|
|
124
|
+
|
|
125
|
+
data.append({
|
|
126
|
+
'satNo': sat_no,
|
|
127
|
+
'line1': line1,
|
|
128
|
+
'line2': line2,
|
|
129
|
+
'inclination': inclination,
|
|
130
|
+
'apogee': apogee
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
elset_df = pd.DataFrame(data)
|
|
134
|
+
|
|
135
|
+
elset_df['satNo'] = (
|
|
136
|
+
elset_df['satNo']
|
|
137
|
+
.astype(str)
|
|
138
|
+
.str.replace(r"\.0$", "", regex=True)
|
|
139
|
+
.str.zfill(5)
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
elset_df = elset_df[
|
|
143
|
+
(elset_df['inclination'] >= inclination_range[0]) & (elset_df['inclination'] <= inclination_range[1]) &
|
|
144
|
+
(elset_df['apogee'] >= apogee_range[0]) & (elset_df['apogee'] <= apogee_range[1])
|
|
145
|
+
].copy()
|
|
146
|
+
|
|
147
|
+
distance_matrix, key = get_distance_matrix(elset_df, save=True)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
from sgp4.api import Satrec
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class VectorizedKeplerianOrbit:
|
|
5
|
+
def __init__(self, line1_array, line2_array=None):
|
|
6
|
+
if line2_array is not None:
|
|
7
|
+
satellites = np.vectorize(Satrec.twoline2rv)(line1_array, line2_array)
|
|
8
|
+
radius_earth_km = satellites[0].radiusearthkm
|
|
9
|
+
|
|
10
|
+
self.a = np.array([sat.a * radius_earth_km for sat in satellites])
|
|
11
|
+
self.e = np.array([sat.ecco for sat in satellites])
|
|
12
|
+
self.i = np.array([sat.inclo for sat in satellites])
|
|
13
|
+
self.omega = np.array([sat.argpo for sat in satellites])
|
|
14
|
+
self.raan = np.array([sat.nodeo for sat in satellites])
|
|
15
|
+
self.q = np.array([sat.altp * radius_earth_km for sat in satellites])
|
|
16
|
+
self.p = self.a * (1 - self.e ** 2)
|
|
17
|
+
else:
|
|
18
|
+
self.a, self.e, self.i, self.omega, self.raan, self.q, self.p = line1_array
|
|
19
|
+
|
|
20
|
+
def __getitem__(self, key):
|
|
21
|
+
return VectorizedKeplerianOrbit(
|
|
22
|
+
np.array([self.a[key], self.e[key], self.i[key], self.omega[key], self.raan[key], self.q[key], self.p[key]])
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
@staticmethod
|
|
26
|
+
def DistanceMetric(orbit1, orbit2):
|
|
27
|
+
w1 = orbit1.omega[:, np.newaxis]
|
|
28
|
+
w2 = orbit2.omega[np.newaxis, :]
|
|
29
|
+
|
|
30
|
+
c1 = np.cos(orbit1.i)[:, np.newaxis]
|
|
31
|
+
c2 = np.cos(orbit2.i)[np.newaxis, :]
|
|
32
|
+
|
|
33
|
+
s1 = np.sin(orbit1.i)[:, np.newaxis]
|
|
34
|
+
s2 = np.sin(orbit2.i)[np.newaxis, :]
|
|
35
|
+
|
|
36
|
+
delta = orbit1.raan[:, np.newaxis] - orbit2.raan[np.newaxis, :]
|
|
37
|
+
cosI = c1 * c2 + s1 * s2 * np.cos(delta)
|
|
38
|
+
|
|
39
|
+
cosP = s1 * s2 * np.sin(w1) * np.sin(w2) + \
|
|
40
|
+
(np.cos(w1) * np.cos(w2) + c1 * c2 * np.sin(w1) * np.sin(w2)) * np.cos(delta) + \
|
|
41
|
+
(c2 * np.cos(w1) * np.sin(w2) - c1 * np.sin(w1) * np.cos(w2)) * np.sin(delta)
|
|
42
|
+
|
|
43
|
+
q = (1 + orbit1.e[:, np.newaxis]**2) * orbit1.p[:, np.newaxis] + \
|
|
44
|
+
(1 + orbit2.e[np.newaxis, :]**2) * orbit2.p[np.newaxis, :] - \
|
|
45
|
+
2 * np.sqrt(orbit1.p[:, np.newaxis] * orbit2.p[np.newaxis, :]) * \
|
|
46
|
+
(cosI + orbit1.e[:, np.newaxis] * orbit2.e[np.newaxis, :] * cosP)
|
|
47
|
+
|
|
48
|
+
return q
|
|
49
|
+
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
from sgp4.api import Satrec, jday
|
|
2
|
+
import json
|
|
3
|
+
import datetime as dt
|
|
4
|
+
import numpy as np
|
|
5
|
+
from random import random
|
|
6
|
+
from skyfield.api import load
|
|
7
|
+
from orbx.synthetic.orbit_finder.get_optimum_orbit import calculate_average_epoch
|
|
8
|
+
|
|
9
|
+
ts = load.timescale()
|
|
10
|
+
|
|
11
|
+
def getPos(dSeconds, satrec, julianDate):
|
|
12
|
+
|
|
13
|
+
fraction = dSeconds / 86400
|
|
14
|
+
|
|
15
|
+
days = 0
|
|
16
|
+
if fraction > 1:
|
|
17
|
+
days = int(fraction)
|
|
18
|
+
fraction -= days
|
|
19
|
+
|
|
20
|
+
return satrec.sgp4(julianDate + days, fraction)[1]
|
|
21
|
+
|
|
22
|
+
def get_posvcs(TLE_LINE1, TLE_LINE2, epochStr, only_one_period=True):
|
|
23
|
+
# Initialize satellite with skyfield
|
|
24
|
+
satrec = Satrec.twoline2rv(TLE_LINE1, TLE_LINE2)
|
|
25
|
+
|
|
26
|
+
# Convert epochStr to skyfield Time
|
|
27
|
+
epoch_dt = dt.datetime.strptime(epochStr, '%Y-%m-%dT%H:%M:%S.%fZ')
|
|
28
|
+
year = epoch_dt.year
|
|
29
|
+
month = epoch_dt.month
|
|
30
|
+
day = epoch_dt.day
|
|
31
|
+
hour = epoch_dt.hour
|
|
32
|
+
minute = epoch_dt.minute
|
|
33
|
+
second = epoch_dt.second + epoch_dt.microsecond / 1e6 # Include microseconds
|
|
34
|
+
|
|
35
|
+
# Convert epochStr to Julian date
|
|
36
|
+
jd_epochStr, fr_epochStr = jday(year, month, day, hour, minute, second)
|
|
37
|
+
|
|
38
|
+
mean_motion = satrec.no_kozai # radians per minute
|
|
39
|
+
periodInMinutes = np.pi * 2 / mean_motion
|
|
40
|
+
periodInSeconds = int(periodInMinutes * 60)
|
|
41
|
+
|
|
42
|
+
if only_one_period:
|
|
43
|
+
time_limit = periodInSeconds
|
|
44
|
+
else:
|
|
45
|
+
time_limit = 86400
|
|
46
|
+
|
|
47
|
+
stepSeconds = 600
|
|
48
|
+
|
|
49
|
+
positions = []
|
|
50
|
+
coord_list = []
|
|
51
|
+
|
|
52
|
+
getLat = lambda position: np.degrees(np.arctan2(position[2], np.sqrt(position[0]**2 + position[1]**2)))
|
|
53
|
+
getLon = lambda position: np.degrees(np.arctan2(position[1], position[0]))
|
|
54
|
+
getAlt = lambda position: ((np.sqrt(position[0]**2 + position[1]**2 + position[2]**2) - 6371) * 1000)
|
|
55
|
+
|
|
56
|
+
for dSeconds in range(0, time_limit + stepSeconds , stepSeconds):
|
|
57
|
+
delta_days = dSeconds / 86400.0
|
|
58
|
+
jd_total = jd_epochStr + fr_epochStr + delta_days
|
|
59
|
+
jd_int = int(jd_total)
|
|
60
|
+
fr = jd_total - jd_int
|
|
61
|
+
|
|
62
|
+
position = satrec.sgp4(jd_int, fr)[1]
|
|
63
|
+
|
|
64
|
+
positions.append(position)
|
|
65
|
+
lat = getLat(position)
|
|
66
|
+
lon = getLon(position)
|
|
67
|
+
alt = getAlt(position)
|
|
68
|
+
coord_list.extend([dSeconds, lon, lat, alt])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
if only_one_period:
|
|
72
|
+
lat0 = getLat(positions[0])
|
|
73
|
+
lon0 = getLon(positions[0])
|
|
74
|
+
alt0 = getAlt(positions[0])
|
|
75
|
+
coord_list.extend([periodInSeconds, lon0, lat0, alt0])
|
|
76
|
+
|
|
77
|
+
# if only_one_period:
|
|
78
|
+
# coord_list.extend([periodInSeconds, getLon(positions[0]), getLat(positions[0]), getAlt(positions[0])])
|
|
79
|
+
|
|
80
|
+
return positions, coord_list
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
import colorsys
|
|
84
|
+
|
|
85
|
+
def get_cluster_colors(labels):
|
|
86
|
+
"""
|
|
87
|
+
Generate a distinct, bright RGBA color for each unique cluster label.
|
|
88
|
+
Uses HSV color space to space hues evenly, keeping high saturation and value.
|
|
89
|
+
Noise label (-1) gets grey.
|
|
90
|
+
"""
|
|
91
|
+
unique_labels = sorted(set(str(l).replace('.0', '') for l in labels))
|
|
92
|
+
# Remove noise
|
|
93
|
+
non_noise = [l for l in unique_labels if l != '-1']
|
|
94
|
+
|
|
95
|
+
color_map = {}
|
|
96
|
+
n = len(non_noise)
|
|
97
|
+
|
|
98
|
+
for idx, label in enumerate(non_noise):
|
|
99
|
+
# Space hues evenly across the spectrum, skip dark blues/near-black region
|
|
100
|
+
hue = (idx / max(n, 1)) % 1.0
|
|
101
|
+
# High saturation and value to avoid dark/muted colors
|
|
102
|
+
r, g, b = colorsys.hsv_to_rgb(hue, 0.85, 0.95)
|
|
103
|
+
color_map[label] = [int(r * 255), int(g * 255), int(b * 255), 255]
|
|
104
|
+
|
|
105
|
+
# Noise gets grey
|
|
106
|
+
color_map['-1'] = [128, 128, 128, 255]
|
|
107
|
+
|
|
108
|
+
return color_map
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def build_czml(df):
|
|
112
|
+
|
|
113
|
+
epochTime = calculate_average_epoch(df)
|
|
114
|
+
endTime = epochTime + dt.timedelta(days=65)
|
|
115
|
+
epochStr, endTimeStr = map(lambda x: x.strftime('%Y-%m-%dT%H:%M:%S.%fZ'), [epochTime, endTime])
|
|
116
|
+
|
|
117
|
+
# Pre-compute color map across all cluster labels in the dataframe
|
|
118
|
+
color_map = get_cluster_colors(df['label'].tolist())
|
|
119
|
+
|
|
120
|
+
czml = [{'id': 'document', 'version': '1.0'}]
|
|
121
|
+
|
|
122
|
+
for i, row in df.iterrows():
|
|
123
|
+
|
|
124
|
+
_, coordinates = get_posvcs(row['line1'], row['line2'], epochStr)
|
|
125
|
+
coords = [int(coord) if idx % 4 == 0 else float(coord)
|
|
126
|
+
for idx, coord in enumerate(coordinates)]
|
|
127
|
+
|
|
128
|
+
label_str = str(row['label']).replace('.0', '')
|
|
129
|
+
|
|
130
|
+
# Synthetic orbits override color to distinguish from real satellites
|
|
131
|
+
if row['satNo'] == '99999':
|
|
132
|
+
if row['name'] in ('MaximallySeparated', 'Maximally Separated'):
|
|
133
|
+
color = [255, 255, 255, 255] # white — maximally separated
|
|
134
|
+
else:
|
|
135
|
+
color = [255, 165, 0, 255] # orange — Fréchet mean
|
|
136
|
+
else:
|
|
137
|
+
color = color_map.get(label_str, [0, 255, 0, 255])
|
|
138
|
+
|
|
139
|
+
name = row['name']
|
|
140
|
+
if name == 'MaximallySeparated':
|
|
141
|
+
name = 'Maximally Separated'
|
|
142
|
+
if name == 'Optimized':
|
|
143
|
+
name = 'Fréchet mean'
|
|
144
|
+
|
|
145
|
+
czml.append({
|
|
146
|
+
'id': f"{row['satNo']}_{i}",
|
|
147
|
+
'name': str(name),
|
|
148
|
+
'availability': f"{epochStr}/{endTimeStr}",
|
|
149
|
+
'position': {
|
|
150
|
+
'epoch': epochStr,
|
|
151
|
+
'cartographicDegrees': coords,
|
|
152
|
+
'interpolationDegree': 5,
|
|
153
|
+
'interpolationAlgorithm': 'LAGRANGE'
|
|
154
|
+
},
|
|
155
|
+
'properties': {
|
|
156
|
+
'satNo': row['satNo'],
|
|
157
|
+
'apogee': row['apogee'],
|
|
158
|
+
'inclination': row['inclination'],
|
|
159
|
+
'prop_correlated': row['correlated'],
|
|
160
|
+
'label': label_str,
|
|
161
|
+
'prop_orbitColor': {"rgba": color}
|
|
162
|
+
},
|
|
163
|
+
'point': {
|
|
164
|
+
'color': {
|
|
165
|
+
'rgba': color
|
|
166
|
+
},
|
|
167
|
+
'pixelSize': 2
|
|
168
|
+
}
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
with open('orbX/output.czml', 'w') as file:
|
|
172
|
+
json.dump(czml, file, indent=2, separators=(',', ': '))
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
if __name__ == '__main__':
|
|
176
|
+
build_czml()
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import boto3
|
|
2
|
+
import requests
|
|
3
|
+
import time
|
|
4
|
+
|
|
5
|
+
def ionop_czml():
|
|
6
|
+
|
|
7
|
+
ACCESSTOKEN = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJqdGkiOiI5OTMwYjJlMS0yYjBhLTQwMmMtYjJkZi1mZWZiY2RiYTNmN2UiLCJpZCI6MjQwODIwLCJpYXQiOjE3MzgzMDM2ODl9.h1pXOgujWRPoS6ZFc5wL-l5_XJnSyUsPZym3ssZj7TQ'
|
|
8
|
+
|
|
9
|
+
BASEURL = 'https://api.cesium.com/v1/assets'
|
|
10
|
+
FILEPATH = 'orbX/output.czml'
|
|
11
|
+
|
|
12
|
+
headers = {'Authorization': f'Bearer {ACCESSTOKEN}'}
|
|
13
|
+
|
|
14
|
+
try:
|
|
15
|
+
postBody = {
|
|
16
|
+
'name': 'output',
|
|
17
|
+
'description': "l-dit demo czml file",
|
|
18
|
+
'type': 'CZML',
|
|
19
|
+
'options': {
|
|
20
|
+
'sourceType': 'CZML'
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
response = requests.post(BASEURL, headers = headers, json = postBody)
|
|
25
|
+
response.raise_for_status()
|
|
26
|
+
|
|
27
|
+
assetId = response.json()['assetMetadata']['id']
|
|
28
|
+
uploadLocation = response.json().get('uploadLocation', {})
|
|
29
|
+
|
|
30
|
+
s3Client = boto3.client(
|
|
31
|
+
's3',
|
|
32
|
+
aws_access_key_id = uploadLocation['accessKey'],
|
|
33
|
+
aws_secret_access_key = uploadLocation['secretAccessKey'],
|
|
34
|
+
aws_session_token = uploadLocation['sessionToken'],
|
|
35
|
+
endpoint_url = uploadLocation['endpoint']
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
with open(FILEPATH, 'rb') as f:
|
|
39
|
+
s3Client.upload_fileobj(f, uploadLocation['bucket'], f"{uploadLocation['prefix']}output.czml")
|
|
40
|
+
|
|
41
|
+
print('PASSED')
|
|
42
|
+
|
|
43
|
+
onComplete = response.json().get('onComplete', {})
|
|
44
|
+
onCompleteURL = onComplete.get('url')
|
|
45
|
+
onCompleteMethod = onComplete.get('method')
|
|
46
|
+
onCompleteFields = onComplete.get('fields')
|
|
47
|
+
|
|
48
|
+
response = requests.request(
|
|
49
|
+
method = onCompleteMethod,
|
|
50
|
+
url = onCompleteURL,
|
|
51
|
+
headers = headers,
|
|
52
|
+
json = onCompleteFields
|
|
53
|
+
)
|
|
54
|
+
response.raise_for_status()
|
|
55
|
+
|
|
56
|
+
print('PASSED')
|
|
57
|
+
|
|
58
|
+
while True:
|
|
59
|
+
statusResponse = requests.get(BASEURL + '/' + str(assetId), headers = headers)
|
|
60
|
+
statusResponse.raise_for_status()
|
|
61
|
+
status = statusResponse.json()['status']
|
|
62
|
+
if status == 'COMPLETE':
|
|
63
|
+
print('PASSED')
|
|
64
|
+
break
|
|
65
|
+
elif status == 'DATA_ERROR':
|
|
66
|
+
print('FAILED')
|
|
67
|
+
break
|
|
68
|
+
else:
|
|
69
|
+
print(f"Tiling status: {statusResponse.json()['percentComplete']}")
|
|
70
|
+
time.sleep(20)
|
|
71
|
+
|
|
72
|
+
params = {
|
|
73
|
+
'sortBy': 'DATE_ADDED',
|
|
74
|
+
'sortOrder': 'DESC'
|
|
75
|
+
}
|
|
76
|
+
response = requests.get(BASEURL, headers = headers, params = params)
|
|
77
|
+
response.raise_for_status()
|
|
78
|
+
|
|
79
|
+
assets = [item['id'] for item in response.json()['items'] if item['status'] == 'COMPLETE']
|
|
80
|
+
if len(assets) >= 2:
|
|
81
|
+
assetIdToDelete = assets[1]
|
|
82
|
+
deleteURL = f'{BASEURL}/{assetIdToDelete}'
|
|
83
|
+
response = requests.delete(deleteURL, headers = headers)
|
|
84
|
+
response.raise_for_status()
|
|
85
|
+
print('PASSED')
|
|
86
|
+
else:
|
|
87
|
+
print('FAILED')
|
|
88
|
+
|
|
89
|
+
except requests.exceptions.RequestException as e:
|
|
90
|
+
print('ERROR:', e)
|
|
91
|
+
except Exception as e:
|
|
92
|
+
print('ERROR:', e)
|
|
93
|
+
|
|
94
|
+
if __name__ == '__main__':
|
|
95
|
+
ionop_czml()
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
from sgp4.api import Satrec
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
class VectorizedKeplerianOrbit:
|
|
5
|
+
def __init__(self, line1_array, line2_array=None):
|
|
6
|
+
if line2_array is not None:
|
|
7
|
+
satellites = np.vectorize(Satrec.twoline2rv)(line1_array, line2_array)
|
|
8
|
+
radius_earth_km = satellites[0].radiusearthkm
|
|
9
|
+
|
|
10
|
+
self.a = np.array([sat.a * radius_earth_km for sat in satellites])
|
|
11
|
+
self.e = np.array([sat.ecco for sat in satellites])
|
|
12
|
+
self.i = np.array([sat.inclo for sat in satellites])
|
|
13
|
+
self.omega = np.array([sat.argpo for sat in satellites])
|
|
14
|
+
self.raan = np.array([sat.nodeo for sat in satellites])
|
|
15
|
+
self.q = np.array([sat.altp * radius_earth_km for sat in satellites])
|
|
16
|
+
self.p = self.a * (1 - self.e ** 2)
|
|
17
|
+
else:
|
|
18
|
+
# Here we assume line1_array is a complete orbit [a, e, i, omega, raan, q, p]
|
|
19
|
+
arr = np.array(line1_array)
|
|
20
|
+
self.a = np.array([arr[0]])
|
|
21
|
+
self.e = np.array([arr[1]])
|
|
22
|
+
self.i = np.array([arr[2]])
|
|
23
|
+
self.omega = np.array([arr[3]])
|
|
24
|
+
self.raan = np.array([arr[4]])
|
|
25
|
+
self.q = np.array([arr[5]])
|
|
26
|
+
self.p = np.array([arr[6]])
|
|
27
|
+
|
|
28
|
+
def __getitem__(self, key):
|
|
29
|
+
return VectorizedKeplerianOrbit(
|
|
30
|
+
np.array([self.a[key], self.e[key], self.i[key], self.omega[key], self.raan[key], self.q[key], self.p[key]])
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
@staticmethod
|
|
34
|
+
def DistanceMetric(orbit1, orbit2):
|
|
35
|
+
w1 = orbit1.omega[:, np.newaxis]
|
|
36
|
+
w2 = orbit2.omega[np.newaxis, :]
|
|
37
|
+
|
|
38
|
+
c1 = np.cos(orbit1.i)[:, np.newaxis]
|
|
39
|
+
c2 = np.cos(orbit2.i)[np.newaxis, :]
|
|
40
|
+
|
|
41
|
+
s1 = np.sin(orbit1.i)[:, np.newaxis]
|
|
42
|
+
s2 = np.sin(orbit2.i)[np.newaxis, :]
|
|
43
|
+
|
|
44
|
+
delta = orbit1.raan[:, np.newaxis] - orbit2.raan[np.newaxis, :]
|
|
45
|
+
cosI = c1 * c2 + s1 * s2 * np.cos(delta)
|
|
46
|
+
|
|
47
|
+
cosP = s1 * s2 * np.sin(w1) * np.sin(w2) + \
|
|
48
|
+
(np.cos(w1) * np.cos(w2) + c1 * c2 * np.sin(w1) * np.sin(w2)) * np.cos(delta) + \
|
|
49
|
+
(c2 * np.cos(w1) * np.sin(w2) - c1 * np.sin(w1) * np.cos(w2)) * np.sin(delta)
|
|
50
|
+
|
|
51
|
+
q = (1 + orbit1.e[:, np.newaxis]**2) * orbit1.p[:, np.newaxis] + \
|
|
52
|
+
(1 + orbit2.e[np.newaxis, :]**2) * orbit2.p[np.newaxis, :] - \
|
|
53
|
+
2 * np.sqrt(orbit1.p[:, np.newaxis] * orbit2.p[np.newaxis, :]) * \
|
|
54
|
+
(cosI + orbit1.e[:, np.newaxis] * orbit2.e[np.newaxis, :] * cosP)
|
|
55
|
+
|
|
56
|
+
return q
|
|
57
|
+
|