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.
Files changed (36) hide show
  1. orbx/Configs.py +7 -0
  2. orbx/Models.py +27 -0
  3. orbx/__init__.py +5 -0
  4. orbx/clustering/Cluster.py +48 -0
  5. orbx/clustering/Core.py +44 -0
  6. orbx/clustering/Schema.py +41 -0
  7. orbx/clustering/__init__.py +3 -0
  8. orbx/clustering/algorithm_wrappers/DBSCANWrapper.py +216 -0
  9. orbx/clustering/algorithm_wrappers/HDBSCANWrapper.py +37 -0
  10. orbx/clustering/algorithm_wrappers/OPTICSWrapper.py +96 -0
  11. orbx/clustering/algorithm_wrappers/cluster_wrapper.py +12 -0
  12. orbx/clustering/data_handling/DataHandler.py +144 -0
  13. orbx/density/Density.py +74 -0
  14. orbx/density/__init__.py +3 -0
  15. orbx/density/tools/DMT.py +49 -0
  16. orbx/density/tools/distance_matrix.py +147 -0
  17. orbx/synthetic_orbits/DMT.py +49 -0
  18. orbx/synthetic_orbits/__init__.py +3 -0
  19. orbx/synthetic_orbits/data_handling/build_czml.py +176 -0
  20. orbx/synthetic_orbits/data_handling/ionop_czml.py +95 -0
  21. orbx/synthetic_orbits/orbit_finder/DMT.py +57 -0
  22. orbx/synthetic_orbits/orbit_finder/frechet_orbit_finder.py +215 -0
  23. orbx/synthetic_orbits/orbit_finder/get_optimum_orbit.py +524 -0
  24. orbx/synthetic_orbits/orbit_finder/max_separation_orbit_finder.py +173 -0
  25. orbx/synthetic_orbits/orbit_finder/optimum_orbit_tle.py +161 -0
  26. orbx/synthetic_orbits/orbit_finder/process_input.py +89 -0
  27. orbx/synthetic_orbits/orbit_finder/query_spacetrack.py +45 -0
  28. orbx/synthetic_orbits/process_input.py +53 -0
  29. orbx/synthetic_orbits/synthetic_orbit.py +108 -0
  30. orbx/tools/DMT.py +49 -0
  31. orbx/tools/distance_matrix.py +147 -0
  32. orbx-1.0.0.dist-info/METADATA +233 -0
  33. orbx-1.0.0.dist-info/RECORD +36 -0
  34. orbx-1.0.0.dist-info/WHEEL +5 -0
  35. orbx-1.0.0.dist-info/licenses/LICENSE +201 -0
  36. orbx-1.0.0.dist-info/top_level.txt +1 -0
@@ -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)
@@ -0,0 +1,3 @@
1
+ from .Density import density
2
+
3
+ __all__ = ["density"]
@@ -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,3 @@
1
+ from orbx.synthetic_orbits.synthetic_orbit import synthetic_orbit
2
+
3
+ __all__ = ["synthetic_orbit"]
@@ -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
+