orbit-gen 0.1.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.
orbit_gen/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ from orbit_gen.orbit import Orbit
2
+ from orbit_gen.family import Family
3
+ from orbit_gen.setup.config import RunConfig
orbit_gen/family.py ADDED
@@ -0,0 +1,296 @@
1
+ import numpy as np
2
+ import spiceypy as spice
3
+ from matplotlib import pyplot as plt
4
+ from scipy.optimize import fsolve
5
+ import astropy.units as u
6
+
7
+ from orbit_gen.utils import eom, units
8
+ from orbit_gen.orbit import Orbit
9
+ from orbit_gen.setup.config import RunConfig
10
+ from orbit_gen.setup.system_config import CanonicalUnits
11
+
12
+
13
+ class Family:
14
+ """Family of CR3BP periodic orbits
15
+
16
+ Generates a family of periodic orbits in the circular restricted
17
+ three-body problem (CR3BP).
18
+
19
+ Args:
20
+ cfg (RunConfig):
21
+ Full run configuration.
22
+
23
+ Attributes:
24
+ mu_star (float):
25
+ CR3BP mass ratio.
26
+ m1 (float):
27
+ Normalized primary mass (1 - mu_star).
28
+ m2 (float):
29
+ Normalized secondary mass (mu_star).
30
+ r_secondary (float):
31
+ Radius of the secondary body in canonical distance units (DU).
32
+ orbits (list[Orbit]):
33
+ Collection of valid periodic orbit solutions obtained.
34
+ """
35
+
36
+ def __init__(self, cfg: RunConfig):
37
+ self.cfg = cfg
38
+ self.orbits: list[Orbit] = []
39
+
40
+ self._setup_constants()
41
+ self._run_continuation()
42
+
43
+ def _setup_constants(self):
44
+ """Load SPICE kernel and compute CR3BP constants from system config."""
45
+ for path in self.cfg.kernel_paths.values():
46
+ spice.furnsh(str(path))
47
+
48
+ sys = self.cfg.system
49
+ gm_primary = spice.bodvrd(sys.primary, "GM", 1)[1][0]
50
+ gm_secondary = spice.bodvrd(sys.secondary, "GM", 1)[1][0]
51
+
52
+ self.mu_star = gm_secondary / (gm_primary + gm_secondary)
53
+ self.m1 = 1 - self.mu_star
54
+ self.m2 = self.mu_star
55
+
56
+ dist_km = sys.canonical_units.dist_km
57
+ time_seconds = np.sqrt(dist_km**3 / (gm_primary + gm_secondary))
58
+ sys.canonical_units = CanonicalUnits(
59
+ dist_km=dist_km, time_days=time_seconds / 86400
60
+ )
61
+
62
+ radii = spice.bodvrd(sys.secondary, "RADII", 3)[1][0]
63
+ self.r_secondary = units.pos_to_canonical(radii * u.km, sys.canonical_units)
64
+
65
+ def _run_continuation(self):
66
+ """Outer pseudo-arclength loop. Populates self.orbits."""
67
+ cont = self.cfg.continuation
68
+ Tp_lim = units.time_to_canonical(
69
+ cont.max_period_days * u.d, self.cfg.system.canonical_units
70
+ )
71
+ stm_identity = np.reshape(np.eye(6), (1, 36))[0]
72
+
73
+ # Free variables: [x0, z0, vy0, T/2]
74
+ X = self.cfg.ic.as_list
75
+ z = np.array([0.0, 0.0, 0.0, -1.0])
76
+
77
+ while X[-1] * 2 < Tp_lim and len(self.orbits) < cont.max_solutions:
78
+ X, error = self._correct_orbit(X, stm_identity)
79
+
80
+ if X[-1] < 0:
81
+ print("Negative period.")
82
+ break
83
+ if error > cont.eps:
84
+ print("Corrector did not converge.")
85
+ break
86
+
87
+ period_days = units.time_to_dimensional(
88
+ 2 * X[-1], self.cfg.system.canonical_units
89
+ ).to_value(u.day)
90
+ print(f"Period: {period_days:.4f} days")
91
+
92
+ states, times, valid = self._propagate_and_validate(X)
93
+
94
+ if valid:
95
+ orbit = Orbit(
96
+ x0=X[0],
97
+ z0=X[1],
98
+ vy0=X[2],
99
+ period=times[-1],
100
+ mu_star=self.mu_star,
101
+ canonical_units=self.cfg.system.canonical_units,
102
+ )
103
+ # Store already-computed trajectory so propagate() isn't needed again
104
+ orbit.states = states
105
+ orbit.times = times
106
+ self.orbits.append(orbit)
107
+
108
+ X, z, success = self._continuation_step(X, z)
109
+ if not success:
110
+ break
111
+
112
+ print(f"Orbits found: {len(self.orbits)}\n")
113
+
114
+ if not self.orbits:
115
+ raise RuntimeError("Continuation failed")
116
+
117
+ def _correct_orbit(self, X: list, stm_identity: np.ndarray) -> tuple[list, float]:
118
+ """Single-shooting differential corrector
119
+
120
+ Newton-style differential correction using a single-shooting formulation
121
+ for periodic orbit boundary condition enforcement.
122
+
123
+ Args:
124
+ X (array-like):
125
+ Free-variable vector of the form [x0, z0, vy0, T/2].
126
+ stm_identity (ndarray):
127
+ Flattened 6x6 identity matrix used to initialize/propagate the
128
+ state transition matrix.
129
+
130
+ Returns:
131
+ X (ndarray):
132
+ Corrected free-variable vector after convergence.
133
+ error (float):
134
+ Final residual norm ||F(X)|| at termination.
135
+ """
136
+
137
+ cont = self.cfg.continuation
138
+ error = np.inf
139
+
140
+ for i in range(cont.max_iter):
141
+ X_full = np.append(X, stm_identity).tolist()
142
+
143
+ Fx, state_half_T, Phi = eom.constraint(X_full, self.mu_star)
144
+ error = np.linalg.norm(Fx)
145
+
146
+ if error < cont.eps:
147
+ break
148
+
149
+ dFx = eom.jacobian(X, self.mu_star, state_half_T, Phi)
150
+ X = X - 0.3 * dFx.T @ (np.linalg.inv(dFx @ dFx.T) @ Fx)
151
+
152
+ print(
153
+ f" iter {i+1:2d} | error = {error:.3e} | cond = {np.linalg.cond(dFx @ dFx.T):.2e}"
154
+ )
155
+
156
+ return X, error
157
+
158
+ def _propagate_and_validate(self, X: list) -> tuple[np.ndarray, np.ndarray, bool]:
159
+ """Propagate one full orbital period and check secondary-body intersection
160
+
161
+ Numerically integrates the trajectory over a full orbital period from the
162
+ given initial conditions, then evaluates whether the trajectory intersects
163
+ or enters the secondary body's exclusion region.
164
+
165
+ Args:
166
+ X (array-like):
167
+ Free-variable vector [x0, z0, vy0, T/2] defining the initial state
168
+ and half-period used for symmetric propagation.
169
+
170
+ Returns:
171
+ states (ndarray):
172
+ Propagated state history with shape (N, 6), where each row is
173
+ [x, y, z, vx, vy, vz].
174
+ times (ndarray):
175
+ Time samples corresponding to each state, shape (N,).
176
+ valid (bool):
177
+ False if the trajectory intersects the secondary body or violates
178
+ the exclusion radius; True otherwise.
179
+ """
180
+
181
+ X_full = X.copy()
182
+ X_full[-1] = 2 * X_full[-1]
183
+
184
+ states, times = eom.propagate(X_full, self.mu_star)
185
+ positions = states[:, 0:3]
186
+ r_mag = np.linalg.norm(positions, axis=1)
187
+
188
+ periapsis_km = units.pos_to_dimensional(
189
+ min(r_mag), self.cfg.system.canonical_units
190
+ ).to_value(u.km)
191
+ print(f" Periapsis: {periapsis_km:.2f} km")
192
+
193
+ valid = not np.any(r_mag < self.r_secondary)
194
+ if not valid:
195
+ print(" Intersects secondary — discarding.")
196
+
197
+ return states, times, valid
198
+
199
+ def _continuation_step(
200
+ self, X: list, z: np.ndarray
201
+ ) -> tuple[list, np.ndarray, bool]:
202
+ """Advance one pseudo-arclength continuation step along an orbit family
203
+
204
+ Performs a predictor-corrector step along the solution manifold using a
205
+ pseudo-arclength continuation method. Advances the current solution estimate
206
+ and updates the tangent direction along the family.
207
+
208
+ Args:
209
+ X (ndarray):
210
+ Current free-variable vector at the current solution point.
211
+ z (ndarray):
212
+ Current tangent direction vector along the continuation manifold.
213
+
214
+ Returns:
215
+ X_new (ndarray):
216
+ Predicted/corrected free-variable vector for the next orbit.
217
+ z_new (ndarray):
218
+ Updated tangent direction at the new solution point.
219
+ success (bool):
220
+ False if the Jacobian is singular or the correction step fails;
221
+ True otherwise.
222
+ """
223
+
224
+ sol_predictor = X + z * self.cfg.continuation.step
225
+
226
+ fss = fsolve(
227
+ eom.continuation_constraint,
228
+ X,
229
+ args=(z, sol_predictor, self.mu_star),
230
+ full_output=True,
231
+ xtol=1e-6,
232
+ )
233
+ X_new = fss[0]
234
+
235
+ # Reconstruct Jacobian
236
+ Q = fss[1]["fjac"]
237
+ Rs = fss[1]["r"]
238
+ R = np.zeros((4, 4))
239
+ idx, col = np.triu_indices(4, k=0)
240
+ R[idx, col] = Rs
241
+ J = Q.T @ R
242
+
243
+ try:
244
+ z_new = np.linalg.inv(J) @ z
245
+ z_new = z_new / np.linalg.norm(z_new)
246
+ return X_new, z_new, True
247
+ except np.linalg.LinAlgError:
248
+ print("Singular Jacobian — stopping continuation.")
249
+ return X_new, z, False
250
+
251
+ def plot(self):
252
+ """Plot all orbits in the family on a single 3D figure
253
+
254
+ Generates a 3D visualization of all stored periodic orbits in the family,
255
+ typically in canonical CR3BP coordinates. Each orbit is plotted in the same
256
+ figure for qualitative comparison of geometry across the continuation family.
257
+
258
+ Args:
259
+ show (bool):
260
+ If True, displays the figure via matplotlib.pyplot.show().
261
+ If False, the figure is created but not rendered interactively.
262
+ """
263
+
264
+ fig = plt.figure()
265
+ ax = fig.add_subplot(projection="3d")
266
+
267
+ for orbit in self.orbits:
268
+ orbit.plot(ax=ax)
269
+
270
+ ax.set_title(f"{self.cfg.ic.name} family — {len(self.orbits)} orbits")
271
+
272
+ plt.show()
273
+
274
+ return ax
275
+
276
+ def save(self):
277
+ """Save all orbit ICs, periods, and mu_star to cfg.output_path."""
278
+ self.cfg.output_dir.mkdir(parents=True, exist_ok=True)
279
+
280
+ states = np.array([o.states[0, :] for o in self.orbits])
281
+ periods = np.array([o.period for o in self.orbits])
282
+
283
+ np.savez(
284
+ self.cfg.output_path,
285
+ states=states,
286
+ periods=periods,
287
+ mu_star=self.mu_star,
288
+ )
289
+ print(f"Saved {len(self.orbits)} orbits to {self.cfg.output_path}")
290
+
291
+ def __repr__(self):
292
+ return (
293
+ f"Family(system={self.cfg.system.name}, "
294
+ f"ic={self.cfg.ic.name}, "
295
+ f"orbits={len(self.orbits)})"
296
+ )
orbit_gen/orbit.py ADDED
@@ -0,0 +1,135 @@
1
+ import numpy as np
2
+ from matplotlib import pyplot as plt
3
+
4
+ from orbit_gen.utils import eom, units
5
+ from orbit_gen.setup.system_config import CanonicalUnits
6
+ import astropy.units as u
7
+
8
+
9
+ class Orbit:
10
+ """Single corrected periodic orbit in the CR3BP
11
+
12
+ Represents a converged periodic orbit obtained from a differential correction
13
+ procedure in the circular restricted three-body problem (CR3BP). The orbit
14
+ is defined at a symmetry-plane crossing and can be numerically propagated to
15
+ recover the full trajectory.
16
+
17
+ Args:
18
+ x0 (float):
19
+ x-position at symmetry-plane crossing in canonical distance units (DU).
20
+ z0 (float):
21
+ z-position at symmetry-plane crossing in DU.
22
+ vy0 (float):
23
+ y-velocity at symmetry-plane crossing in canonical units (DU/TU).
24
+ period (float):
25
+ Full orbital period T in canonical time units (TU).
26
+ mu_star (float):
27
+ CR3BP mass ratio (normalized secondary mass parameter).
28
+
29
+ Attributes:
30
+ states (ndarray or None):
31
+ Trajectory array of shape (N, 6) containing
32
+ [x, y, z, vx, vy, vz] along the orbit. None until `propagate()` is called.
33
+ times (ndarray or None):
34
+ Time array of shape (N,) corresponding to `states`. None until
35
+ `propagate()` is called.
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ x0: float,
41
+ z0: float,
42
+ vy0: float,
43
+ period: float,
44
+ mu_star: float,
45
+ canonical_units: CanonicalUnits,
46
+ ):
47
+ self.x0 = x0
48
+ self.z0 = z0
49
+ self.vy0 = vy0
50
+ self.period = period
51
+ self.mu_star = mu_star
52
+ self.canonical_units = canonical_units
53
+
54
+ self.states = None
55
+ self.times = None
56
+
57
+ @property
58
+ def period_days(self) -> float:
59
+ """Full orbital period in days."""
60
+ return units.time_to_dimensional(self.period, self.canonical_units).to_value(
61
+ u.day
62
+ )
63
+
64
+ @property
65
+ def _free_var(self) -> list:
66
+ """Full-period free variable vector [x0, z0, vy0, T]."""
67
+ return [self.x0, self.z0, self.vy0, self.period]
68
+
69
+ def propagate(self):
70
+ """Integrate the trajectory for one full period.
71
+
72
+ Stores results in self.states and self.times.
73
+ """
74
+ self.states, self.times = eom.propagate(self._free_var, self.mu_star)
75
+
76
+ def plot(self, ax=None, **kwargs):
77
+ """Plot the orbit in 3D
78
+
79
+ Plots the trajectory of the orbit in a 3D matplotlib figure using the
80
+ stored state history.
81
+
82
+ Args:
83
+ ax (matplotlib.axes._subplots.Axes3DSubplot, optional):
84
+ Existing 3D axes object. If None, a new figure and 3D axes are
85
+ created internally.
86
+ **kwargs:
87
+ Additional keyword arguments forwarded directly to `ax.plot()`,
88
+ controlling styling (e.g., color, linewidth, alpha).
89
+
90
+ Returns:
91
+ ax (matplotlib.axes._subplots.Axes3DSubplot):
92
+ The matplotlib 3D axes containing the plotted orbit.
93
+ """
94
+
95
+ if self.states is None:
96
+ self.propagate()
97
+
98
+ if ax is None:
99
+ fig = plt.figure()
100
+ ax = fig.add_subplot(projection="3d")
101
+
102
+ ax.plot(self.states[:, 0], self.states[:, 1], self.states[:, 2], **kwargs)
103
+ ax.set_xlabel("X [DU]")
104
+ ax.set_ylabel("Y [DU]")
105
+ ax.set_zlabel("Z [DU]")
106
+
107
+ return ax
108
+
109
+ def save(self, path):
110
+ """Save orbit initial conditions and period to an NPZ file
111
+
112
+ Serializes the orbit’s defining parameters (initial state and period)
113
+ to a NumPy `.npz` archive for later reuse or reconstruction.
114
+
115
+ Args:
116
+ path (str or pathlib.Path):
117
+ Destination file path for the `.npz` archive. If the extension is
118
+ omitted, `.npz` is assumed.
119
+ """
120
+
121
+ if self.states is None:
122
+ self.propagate()
123
+
124
+ np.savez(
125
+ path,
126
+ ic=self.states[0, :],
127
+ period=self.period,
128
+ mu_star=self.mu_star,
129
+ )
130
+
131
+ def __repr__(self):
132
+ return (
133
+ f"Orbit(x0={self.x0:.6f}, z0={self.z0:.6f}, "
134
+ f"vy0={self.vy0:.6f}, T={self.period:.6f})"
135
+ )
File without changes
@@ -0,0 +1,114 @@
1
+ from dataclasses import dataclass, field
2
+ from .orbit_ic import OrbitIC
3
+ from .system_config import SystemConfig, EM_SYSTEM, SE_SYSTEM
4
+ from .presets import PRESETS
5
+ from .kernels import ensure_kernels
6
+ from pathlib import Path
7
+ import yaml
8
+
9
+ PROJECT_ROOT = Path(__file__).resolve().parent.parent
10
+
11
+ _SYSTEMS = {
12
+ "EM": EM_SYSTEM,
13
+ "SE": SE_SYSTEM,
14
+ }
15
+
16
+
17
+ @dataclass
18
+ class ContinuationConfig:
19
+ """Parameters controlling differential correction and family continuation.
20
+
21
+ Attributes:
22
+ eps (float):
23
+ Convergence tolerance on the constraint residual norm
24
+ max_iter (int):
25
+ Maximum number of Newton iterations for a single
26
+ differential correction attempt
27
+ max_period_days (float):
28
+ Maximum allowable full orbit period, in days
29
+ max_solutions (int):
30
+ Maximum number of converged periodic orbits to generate before
31
+ terminating continuation
32
+ step (float):
33
+ Pseudo-arclength continuation step size in canonical units
34
+
35
+ """
36
+
37
+ max_iter: int = 1000
38
+ eps: float = 1e-6
39
+ step: float = 0.01
40
+ max_solutions: int = 10
41
+ max_period_days: float = 1000.0
42
+
43
+
44
+ @dataclass
45
+ class RunConfig:
46
+ """Top-level configuration for a continuation run.
47
+
48
+ Attributes:
49
+ continuation (ContinuationConfig):
50
+ Differential correction and continuation parameters
51
+ ic (OrbitIC):
52
+ Periodic orbit used to initialize continuation
53
+ output_dir (Path):
54
+ Directory where output files will be written
55
+ output_filename (str):
56
+ Name of the output .npz file containing continuation results
57
+ show_plots (bool):
58
+ If True, display plots interactively during execution
59
+ spice_kernel (Path):
60
+ Path to the SPICE meta-kernel furnished before propagation and
61
+ analysis
62
+ system (SystemConfig):
63
+ CR3BP system used
64
+
65
+ """
66
+
67
+ system: SystemConfig
68
+ ic: OrbitIC
69
+ continuation: ContinuationConfig = field(default_factory=ContinuationConfig)
70
+ output_dir: Path = PROJECT_ROOT.parent.parent / "results"
71
+ show_plots: bool = True
72
+ kernel_paths: dict[str, Path] = field(
73
+ init=False, default_factory=ensure_kernels, repr=False
74
+ )
75
+
76
+ @property
77
+ def output_filename(self) -> str:
78
+ return f"{self.ic.name}.npz"
79
+
80
+ @property
81
+ def output_path(self) -> Path:
82
+ return self.output_dir / self.output_filename
83
+
84
+ @classmethod
85
+ def from_yaml(cls, yaml_path: Path) -> "RunConfig":
86
+ with open(yaml_path) as yp:
87
+ data = yaml.safe_load(yp)
88
+
89
+ system = _SYSTEMS[data["system"]]
90
+ ic = PRESETS[data["preset"]]
91
+
92
+ if system.name == "EM":
93
+ period_max_default = 50.0
94
+ elif system.name == "SE":
95
+ period_max_default = 500.0
96
+ else:
97
+ raise RuntimeError("Unknown system name")
98
+
99
+ continuation = ContinuationConfig(
100
+ max_iter=data.get("max_iter", 100),
101
+ eps=data.get("eps", 1e-6),
102
+ step=data.get("step", 0.01),
103
+ max_solutions=data.get("max_solutions", 20),
104
+ max_period_days=data.get("max_period_days", period_max_default),
105
+ )
106
+
107
+ return cls(
108
+ system=system,
109
+ ic=ic,
110
+ continuation=continuation,
111
+ output_dir=PROJECT_ROOT.parent.parent
112
+ / Path(data.get("output_dir", "results")),
113
+ show_plots=data.get("show_plots", True),
114
+ )
@@ -0,0 +1,56 @@
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+ from urllib.request import urlopen
4
+ from urllib.error import URLError
5
+ from platformdirs import user_cache_dir
6
+
7
+
8
+ @dataclass(frozen=True)
9
+ class KernelSpec:
10
+ filename: str
11
+ url: str
12
+
13
+
14
+ REQUIRED_KERNELS: tuple[KernelSpec, ...] = (
15
+ KernelSpec(
16
+ filename="naif0012.tls",
17
+ url="https://naif.jpl.nasa.gov/pub/naif/generic_kernels/lsk/naif0012.tls",
18
+ ),
19
+ KernelSpec(
20
+ filename="gm_de440.tpc",
21
+ url="https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/gm_de440.tpc",
22
+ ),
23
+ KernelSpec(
24
+ filename="pck00011.tpc",
25
+ url="https://naif.jpl.nasa.gov/pub/naif/generic_kernels/pck/pck00011.tpc",
26
+ ),
27
+ )
28
+
29
+
30
+ def cache_dir() -> Path:
31
+ direc = Path(user_cache_dir("orbit-gen")) / "kernels"
32
+ direc.mkdir(parents=True, exist_ok=True)
33
+ return direc
34
+
35
+
36
+ def ensure_kernel(spec: KernelSpec) -> Path:
37
+ cache = cache_dir()
38
+ dest = cache / spec.filename
39
+
40
+ if dest.exists() and dest.stat().st_size > 0:
41
+ return dest
42
+
43
+ tmp = dest.with_suffix(dest.suffix + ".part")
44
+ try:
45
+ with urlopen(spec.url, timeout=30) as resp, open(tmp, "wb") as f:
46
+ f.write(resp.read())
47
+ except URLError as e:
48
+ raise RuntimeError(
49
+ f"Failed to download {spec.filename} from {spec.url}: {e}"
50
+ ) from e
51
+ tmp.rename(dest)
52
+ return dest
53
+
54
+
55
+ def ensure_kernels() -> dict[str, Path]:
56
+ return {spec.filename: ensure_kernel(spec) for spec in REQUIRED_KERNELS}
@@ -0,0 +1,41 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass
5
+ class OrbitIC:
6
+ """A single periodic orbit initial condition.
7
+
8
+ Attributes:
9
+ half_period (float):
10
+ Half-period T/2 in canonical time units (TU)
11
+ name (str):
12
+ Label for the orbit family, including the system and orbit type
13
+ vy0 (float):
14
+ y-velocity in canonical velocity units (DU/TU)
15
+ x0 (float):
16
+ x-position in canonical distance units (DU)
17
+ z0 (float):
18
+ z-position in canonical distance units (DU)
19
+
20
+ .. note::
21
+
22
+ y0, vx0, and vz0 are always zero by the CR3BP symmetry condition. System
23
+ is denoted by "EM" for Earth/Moon, and "SE" for Sun/Earth.
24
+
25
+ """
26
+
27
+ name: str
28
+ x0: float
29
+ z0: float
30
+ vy0: float
31
+ half_period: float
32
+
33
+ @property
34
+ def as_list(self) -> list:
35
+ """Return free variables as [x0, z0, vy0, T/2]."""
36
+ return [self.x0, self.z0, self.vy0, self.half_period]
37
+
38
+ @property
39
+ def full_ic(self) -> list:
40
+ """Return the full 7-element IC [x0, 0, z0, 0, vy0, 0, T/2]."""
41
+ return [self.x0, 0.0, self.z0, 0.0, self.vy0, 0.0, self.half_period]
@@ -0,0 +1,102 @@
1
+ from .orbit_ic import OrbitIC
2
+
3
+ PRESETS: dict[str, OrbitIC] = {
4
+ "EM_L2_Northern_Halo": OrbitIC(
5
+ name="EM_L2_Northern",
6
+ x0=1.0110350588,
7
+ z0=-0.1731500000,
8
+ vy0=-0.0780141199,
9
+ half_period=1.3632096570 / 2,
10
+ ),
11
+ "EM_DRO_1": OrbitIC(
12
+ name="EM_DRO_1",
13
+ x0=0.583856747,
14
+ z0=0.0,
15
+ vy0=0.96455414,
16
+ half_period=5.70245716 / 2,
17
+ ),
18
+ "EM_DRO_2": OrbitIC(
19
+ name="EM_DRO_2",
20
+ x0=0.429519110229904,
21
+ z0=0.0,
22
+ vy0=1.440796689672539,
23
+ half_period=3.051133070334277,
24
+ ),
25
+ "EM_L1": OrbitIC(
26
+ name="EM_L1",
27
+ x0=0.856382122325864,
28
+ z0=0.181519309916197,
29
+ vy0=0.257898218422393,
30
+ half_period=1.22727308466325,
31
+ ),
32
+ "EM_Butterfly": OrbitIC(
33
+ name="EM_Butterfly",
34
+ x0=1.06896234204296,
35
+ z0=0.159599443574046,
36
+ vy0=-0.00769167653854165,
37
+ half_period=1.66142030228280,
38
+ ),
39
+ "EM_Unnamed_1": OrbitIC(
40
+ name="EM_Unnamed_1",
41
+ x0=0.95571113,
42
+ z0=0.16892834,
43
+ vy0=0.29101955,
44
+ half_period=6.8828406 / 2,
45
+ ),
46
+ "SE_L2_Northern_Halo": OrbitIC(
47
+ name="SE_L2_Northern_Halo",
48
+ x0=1.0082893998277027,
49
+ z0=0.011434182696839030,
50
+ vy0=-0.015179690420484332,
51
+ half_period=2.7047531095113841 / 2,
52
+ ),
53
+ "SE_L1_Vertical": OrbitIC(
54
+ name="SE_L1_Vertical",
55
+ x0=1.0022515716338869,
56
+ z0=-1.9346947758385478e-13,
57
+ vy0=-1.3010231214778553e-1,
58
+ half_period=6.2826376084648130 / 2,
59
+ ),
60
+ "SE_L2_Lyapunov": OrbitIC(
61
+ name="SE_L2_Lyapunov",
62
+ x0=1.0030516289641727,
63
+ z0=1.7613075082610678e-17,
64
+ vy0=4.2071729820709863e-2,
65
+ half_period=4.1379689311747248 / 2,
66
+ ),
67
+ "SE_L2_Small_Lyapunov": OrbitIC(
68
+ name="SE_L2_Small_Lyapunov",
69
+ x0=1.0173058431158761,
70
+ z0=-8.9129284257995943e-27,
71
+ vy0=-3.5071329991448227e-2,
72
+ half_period=5.5314456810353905 / 2,
73
+ ),
74
+ "SE_L2_Large_Lyapunov": OrbitIC(
75
+ name="SE_L2_Large_Lyapunov",
76
+ x0=1.0437961011210057,
77
+ z0=0,
78
+ vy0=-8.2206717351832326e-2,
79
+ half_period=8.3852287199494260 / 2,
80
+ ),
81
+ "SE_L2_Southern_Halo": OrbitIC(
82
+ name="SE_L2_Southern_Halo",
83
+ x0=1.0057032659079346,
84
+ z0=-1.2409260038177099e-2,
85
+ vy0=-1.2623398608372105e-2,
86
+ half_period=2.1172370586539060 / 2,
87
+ ),
88
+ "SE_Butterfly": OrbitIC(
89
+ name="SE_Butterfly",
90
+ x0=9.9478400099057562e-1,
91
+ z0=9.3307317319370944e-3,
92
+ vy0=-2.7018843432500038e-3,
93
+ half_period=3.6918732624381563 / 2,
94
+ ),
95
+ "SE_Dragonfly": OrbitIC(
96
+ name="SE_Dragonfly",
97
+ x0=1.0087123143716323,
98
+ z0=1.0173103235716078e-2,
99
+ vy0=-1.6680589951751042e-2,
100
+ half_period=5.6228423897645730 / 2,
101
+ ),
102
+ }
@@ -0,0 +1,54 @@
1
+ from dataclasses import dataclass
2
+ import astropy.units as u
3
+
4
+
5
+ @dataclass(frozen=True)
6
+ class CanonicalUnits:
7
+ """Characteristic length/time scales for a specific CR3BP system."""
8
+
9
+ dist_km: float # canonical distance [km]
10
+ time_days: float | None = None # canonical time / (2*pi) [days]
11
+
12
+ @property
13
+ def du_m(self) -> float:
14
+ return (self.dist_km * u.km).to("m").value
15
+
16
+
17
+ @dataclass
18
+ class SystemConfig:
19
+ """Determines parameters based on the CR3BP system
20
+
21
+ Attributes:
22
+ name (str):
23
+ Label for the system
24
+ primary (str):
25
+ Primary body in the CR3BP
26
+ secondary (str):
27
+ Secondary body in the CR3BP
28
+
29
+ .. note::
30
+
31
+ "SE" for Sun/Earth "EM" for Earth/Moon, "Sun" for sun, "Earth" for Earth
32
+ "Moon" for moon
33
+
34
+ """
35
+
36
+ name: str
37
+ primary: str
38
+ secondary: str
39
+ canonical_units: CanonicalUnits
40
+
41
+
42
+ EM_SYSTEM = SystemConfig(
43
+ name="EM",
44
+ primary="Earth",
45
+ secondary="Moon",
46
+ canonical_units=CanonicalUnits(dist_km=3.844e5),
47
+ )
48
+
49
+ SE_SYSTEM = SystemConfig(
50
+ name="SE",
51
+ primary="Sun",
52
+ secondary="Earth",
53
+ canonical_units=CanonicalUnits(dist_km=u.au.to("km")),
54
+ )
orbit_gen/utils/eom.py ADDED
@@ -0,0 +1,265 @@
1
+ import numpy as np
2
+ from scipy.integrate import solve_ivp
3
+ from scipy.optimize import fsolve
4
+
5
+
6
+ def crtbp_eom(t, w, mu_star):
7
+ """Equations of motion for the CR3BP in the rotating frame with optional STM propagation
8
+
9
+ Computes the time derivative of the state in the circular restricted three-body problem
10
+ (CR3BP). Supports both pure state propagation and augmented state propagation including
11
+ a flattened 6×6 state transition matrix (STM).
12
+
13
+ Args:
14
+ t (float):
15
+ Time variable (required by ODE integrators such as `scipy.integrate.solve_ivp`
16
+ but not explicitly used in the CR3BP formulation).
17
+ w (array_like):
18
+ State vector. Two supported formats:
19
+ - len == 6: [x, y, z, vx, vy, vz]
20
+ - len == 42: augmented state including flattened 6×6 STM.
21
+ mu_star (float):
22
+ CR3BP mass parameter (normalized secondary mass).
23
+
24
+ Returns:
25
+ dw (ndarray):
26
+ Time derivative of the input state vector (and STM if present),
27
+ matching the shape of `w`.
28
+ """
29
+
30
+ [x, y, z, vx, vy, vz] = w[:6]
31
+
32
+ m1 = 1 - mu_star
33
+ m2 = mu_star
34
+
35
+ r1 = mu_star
36
+ r2 = 1 - mu_star
37
+ r_1O = r1 * np.array([-1, 0, 0])
38
+ r_2O = r2 * np.array([1, 0, 0])
39
+
40
+ r_PO = np.array([x, y, z])
41
+ v_PO = np.array([vx, vy, vz])
42
+
43
+ r_P1 = r_PO - r_1O
44
+ r_P2 = r_PO - r_2O
45
+ r1_mag = np.linalg.norm(r_P1)
46
+ r2_mag = np.linalg.norm(r_P2)
47
+
48
+ F_g = -m1 / r1_mag**3 * r_P1 - m2 / r2_mag**3 * r_P2
49
+
50
+ e3 = np.array([0, 0, 1])
51
+ a_PO = F_g - 2 * np.cross(e3, v_PO) - np.cross(e3, np.cross(e3, r_PO))
52
+ ax, ay, az = a_PO
53
+
54
+ if len(w) > 7:
55
+ dxdx = (
56
+ 1
57
+ - m1 / r1_mag**3
58
+ - m2 / r2_mag**3
59
+ + 3 * m1 * (x + m2) ** 2 / r1_mag**5
60
+ + 3 * m2 * (x - 1 + m2) ** 2 / r2_mag**5
61
+ )
62
+ dxdy = 3 * m1 * (x + m2) * y / r1_mag**5 + 3 * m2 * (x - 1 + m2) * y / r2_mag**5
63
+ dxdz = 3 * m1 * (x + m2) * z / r1_mag**5 + 3 * m2 * (x - 1 + m2) * z / r2_mag**5
64
+ dydy = (
65
+ 1
66
+ - m1 / r1_mag**3
67
+ - m2 / r2_mag**3
68
+ + 3 * m1 * y**2 / r1_mag**5
69
+ + 3 * m2 * y**2 / r2_mag**5
70
+ )
71
+ dydz = 3 * m1 * y * z / r1_mag**5 + 3 * m2 * y * z / r2_mag**5
72
+ dzdz = (
73
+ -m1 / r1_mag**3
74
+ - m2 / r2_mag**3
75
+ + 3 * m1 * z**2 / r1_mag**5
76
+ + 3 * m2 * z**2 / r2_mag**5
77
+ )
78
+
79
+ Phi = np.reshape(w[6:], (6, 6))
80
+ Z = np.zeros([3, 3])
81
+ I = np.identity(3)
82
+ A = np.array([[dxdx, dxdy, dxdz], [dxdy, dydy, dydz], [dxdz, dydz, dzdz]])
83
+ Omega = np.array([[0, 2, 0], [-2, 0, 0], [0, 0, 0]])
84
+ J = np.block([[Z, I], [A, Omega]])
85
+ dPhi = np.reshape(J @ Phi, (1, 36))[0]
86
+ dw = np.append([vx, vy, vz, ax, ay, az], dPhi)
87
+ else:
88
+ dw = [vx, vy, vz, ax, ay, az]
89
+
90
+ return dw
91
+
92
+
93
+ def propagate(free_var, mu_star):
94
+ """Integrate the CR3BP equations of motion over one full orbital period
95
+
96
+ Performs numerical propagation of the CR3BP dynamics using the provided
97
+ initial free variables. Optionally propagates the state transition matrix
98
+ (STM) if included in the input.
99
+
100
+ Args:
101
+ free_var (array_like):
102
+ Initial condition vector. Supported formats:
103
+ - [x0, z0, vy0, T]
104
+ - [x0, z0, vy0, T, STM_flat] where STM_flat has length 36
105
+ (flattened 6×6 STM, total state dimension = 42).
106
+ mu_star (float):
107
+ CR3BP mass parameter.
108
+
109
+ Returns:
110
+ states (ndarray):
111
+ Trajectory array. Shape is either (N, 6) for state-only propagation
112
+ or (N, 42) when STM propagation is included.
113
+ times (ndarray):
114
+ Time vector of shape (N,) corresponding to integration output.
115
+ """
116
+
117
+ if len(free_var) > 4:
118
+ x0 = np.append(
119
+ [free_var[0], 0, free_var[1], 0, free_var[2], 0], free_var[4:]
120
+ ).tolist()
121
+ else:
122
+ x0 = [free_var[0], 0, free_var[1], 0, free_var[2], 0]
123
+
124
+ T = free_var[3]
125
+
126
+ sol = solve_ivp(crtbp_eom, [0, T], x0, args=(mu_star,), rtol=1e-12, atol=1e-12)
127
+ return sol.y.T, sol.t
128
+
129
+
130
+ def constraint(free_var, mu_star):
131
+ """Evaluate periodicity constraints for CR3BP symmetric periodic orbit
132
+
133
+ Computes the constraint vector enforcing periodicity at the half-period
134
+ crossing and returns the associated state transition matrix (STM) evaluated
135
+ at T/2.
136
+
137
+ Args:
138
+ free_var (array_like):
139
+ Free-variable vector of the form [x0, z0, vy0, T/2] optionally
140
+ followed by a flattened 6×6 state transition matrix (STM).
141
+ mu_star (float):
142
+ CR3BP mass parameter (normalized secondary mass).
143
+
144
+ Returns:
145
+ Fx (ndarray, shape (3,)):
146
+ Periodicity constraint residuals evaluated at T/2:
147
+ typically enforcing symmetry conditions such as [y, vx, vz].
148
+ state_half_T (ndarray, shape (6,)):
149
+ State evaluated at T/2.
150
+ Phi (ndarray, shape (6, 6)):
151
+ State transition matrix evaluated at T/2.
152
+ """
153
+
154
+ states, _ = propagate(free_var, mu_star)
155
+ state = states[-1]
156
+
157
+ Fx = np.array([state[1], state[3], state[5]])
158
+
159
+ if len(state) > 7:
160
+ Phi = np.reshape(state[6:], (6, 6))
161
+ else:
162
+ Phi = None
163
+
164
+ state_half_T = state[0:6]
165
+
166
+ return Fx, state_half_T, Phi
167
+
168
+
169
+ def jacobian(free_var, mu_star, state_half_T, Phi):
170
+ """Compute Jacobian of periodicity constraints for differential correction
171
+
172
+ Evaluates the sensitivity matrix dFx/dX for the CR3BP periodicity constraints
173
+ used in the single-shooting differential corrector.
174
+
175
+ Args:
176
+ free_var (array_like):
177
+ Free-variable vector [x0, z0, vy0, T/2].
178
+ mu_star (float):
179
+ CR3BP mass parameter (normalized secondary mass).
180
+ state_half_T (ndarray, shape (6,)):
181
+ State evaluated at T/2.
182
+ Phi (ndarray, shape (6, 6)):
183
+ State transition matrix evaluated at T/2.
184
+
185
+ Returns:
186
+ dFx (ndarray, shape (3, 4)):
187
+ Jacobian matrix of constraint residuals with respect to free variables.
188
+ """
189
+
190
+ phis = np.array(
191
+ [
192
+ [Phi[1, 0], Phi[1, 2], Phi[1, 4]],
193
+ [Phi[3, 0], Phi[3, 2], Phi[3, 4]],
194
+ [Phi[5, 0], Phi[5, 2], Phi[5, 4]],
195
+ ]
196
+ )
197
+
198
+ dw = crtbp_eom(free_var[-1], state_half_T, mu_star)
199
+ ddT = np.array([dw[1], dw[3], dw[5]])
200
+
201
+ dFx = np.zeros([3, 4])
202
+ dFx[:, 0:3] = phis
203
+ dFx[:, 3] = ddT
204
+
205
+ return dFx
206
+
207
+
208
+ def continuation_constraint(w, z, sol_predictor, mu_star):
209
+ """System of equations for pseudo-arclength continuation (for root finding)
210
+
211
+ Defines the augmented nonlinear system used in pseudo-arclength continuation,
212
+ typically solved using `fsolve`. Combines the CR3BP periodicity constraints
213
+ with the arclength constraint relative to a predictor step.
214
+
215
+ Args:
216
+ w (array_like):
217
+ Current free-variable vector [x0, z0, vy0, T/2].
218
+ z (ndarray):
219
+ Tangent direction vector at the current solution point.
220
+ sol_predictor (ndarray):
221
+ Predicted solution from the continuation predictor step.
222
+ mu_star (float):
223
+ CR3BP mass parameter (normalized secondary mass).
224
+
225
+ Returns:
226
+ ndarray, shape (4,):
227
+ Residual vector combining periodicity constraints and arclength
228
+ condition, suitable for nonlinear root solving.
229
+ """
230
+
231
+ Fx, _, _ = constraint(w, mu_star)
232
+ zeq = z.T @ (w - sol_predictor)
233
+ return np.append(Fx, zeq)
234
+
235
+
236
+ def jacobi_constant(pos, vel, mu_star):
237
+ """Compute the Jacobi constant in the circular restricted three-body problem
238
+
239
+ Evaluates the Jacobi integral in the rotating frame for a given state. The
240
+ Jacobi constant is conserved along CR3BP trajectories and is commonly used
241
+ as an invariant for validation and classification of orbits.
242
+
243
+ Args:
244
+ pos (array_like, shape (3,)):
245
+ Position vector [x, y, z] in canonical rotating-frame coordinates.
246
+ vel (array_like, shape (3,)):
247
+ Velocity vector [vx, vy, vz] in canonical units.
248
+ mu_star (float):
249
+ CR3BP mass parameter (normalized secondary mass).
250
+
251
+ Returns:
252
+ float:
253
+ Value of the Jacobi constant for the given state.
254
+ """
255
+
256
+ r_Mbary = np.array([1 - mu_star, 0, 0])
257
+ r_Ebary = np.array([-mu_star, 0, 0])
258
+
259
+ KE = np.dot(vel, vel) / 2
260
+ U1 = -(pos[0] ** 2 + pos[1] ** 2) / 2
261
+ U2 = -(
262
+ (1 - mu_star) / np.linalg.norm(pos - r_Ebary)
263
+ + mu_star / np.linalg.norm(pos - r_Mbary)
264
+ )
265
+ return KE + U1 + U2
@@ -0,0 +1,27 @@
1
+ from ..setup.system_config import CanonicalUnits
2
+ import astropy.units as u
3
+
4
+
5
+ def time_to_canonical(dim_time, cu: CanonicalUnits):
6
+ return dim_time.to("day").value / cu.time_days
7
+
8
+
9
+ def time_to_dimensional(canonical_time, cu: CanonicalUnits):
10
+ return canonical_time * cu.time_days * u.day
11
+
12
+
13
+ def pos_to_canonical(dim_pos, cu: CanonicalUnits):
14
+ return dim_pos.to("m").value / cu.du_m
15
+
16
+
17
+ def pos_to_dimensional(canonical_pos, cu: CanonicalUnits):
18
+ return canonical_pos * cu.du_m * u.m
19
+
20
+
21
+ def vel_to_canonical(dim_vel, cu: CanonicalUnits):
22
+ dim_vel_mps = dim_vel.to("m/d").value
23
+ return (dim_vel_mps / cu.du_m) * cu.time_days
24
+
25
+
26
+ def vel_to_dimensional(canonical_vel, cu: CanonicalUnits):
27
+ return canonical_vel * cu.du_m / cu.time_days * (u.m / u.day)
@@ -0,0 +1,134 @@
1
+ Metadata-Version: 2.4
2
+ Name: orbit-gen
3
+ Version: 0.1.0
4
+ Summary: Tools for generating families and orbits in the CR3BP
5
+ Author: Jayden Ripic
6
+ Author-email: Jayden Ripic <jmr595@cornell.edu>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: astropy>=7.2.0
10
+ Requires-Dist: matplotlib>=3.10.9
11
+ Requires-Dist: numpy>=2.4.6
12
+ Requires-Dist: platformdirs>=4.10.0
13
+ Requires-Dist: pyyaml>=6.0.3
14
+ Requires-Dist: scipy>=1.17.1
15
+ Requires-Dist: spiceypy>=8.1.0
16
+ Requires-Python: >=3.12
17
+ Description-Content-Type: text/markdown
18
+
19
+ # orbit-gen
20
+
21
+ Periodic orbit family generation and continuation in the Circular Restricted
22
+ Three-Body Problem (CR3BP).
23
+
24
+ ## Overview
25
+
26
+ `orbit-gen` generates families of periodic orbits in the CR3BP using
27
+ single-shooting differential correction and pseudo-arclength continuation.
28
+ It currently supports various families in the Earth-Moon and Sun-Earth systems.
29
+
30
+ ## Installation
31
+
32
+ ```bash
33
+ pip install orbit-gen
34
+ ```
35
+ Requires Python 3.12+.
36
+
37
+ ## Usage
38
+
39
+ ### From a config file
40
+
41
+ ```yaml
42
+ # orbit_config.yaml
43
+ system: SE # "EM" (Earth-Moon) or "SE" (Sun-Earth)
44
+ preset: SE_L2_Northern_Halo
45
+
46
+ max_iter: 1000 # Maximum Newton iterations for a single differential correct attempt
47
+ eps: 1.0e-6 # Differential-correction convergence tolerance on residual norm
48
+ step: 0.01 # Pseudo-arclength continuation step size in canonical units
49
+ max_solutions: 20 # Maximum number of orbits to generate
50
+ max_period_days: 1000.0 # Maximum full orbit period in days
51
+
52
+ output_dir: results
53
+ show_plots: true # Optional; for user use only
54
+ ```
55
+
56
+ ```python
57
+ from orbit_gen import Family, RunConfig
58
+
59
+ cfg = RunConfig.from_yaml("orbit_config.yaml")
60
+ fam = Family(cfg)
61
+
62
+ fam.save()
63
+ if cfg.show_plots:
64
+ fam.plot()
65
+ ```
66
+
67
+ > **Note:** constructing `Family(cfg)` runs the continuation, but does not save or plot results on its own — call `.save()` / `.plot()` explicitly, as shown above.
68
+
69
+ ### Programmatically
70
+
71
+ ```python
72
+ from orbit_gen import Family, RunConfig
73
+ from orbit_gen.setup.system_config import SE_SYSTEM
74
+ from orbit_gen.setup.presets import PRESETS
75
+
76
+ cfg = RunConfig(system=SE_SYSTEM, ic=PRESETS["SE_L2_Northern_Halo"])
77
+ fam = Family(cfg)
78
+ fam.save()
79
+ ```
80
+
81
+ Available preset keys (see `orbit_gen.setup.presets.PRESETS`):
82
+ `EM_L2_Northern_Halo`, `EM_DRO_1`, `EM_DRO_2`, `EM_L1`, `EM_Butterfly`,
83
+ `EM_Unnamed_1`, `SE_L2_Northern_Halo`, `SE_L1_Vertical`, `SE_L2_Lyapunov`,
84
+ `SE_L2_Small_Lyapunov`, `SE_L2_Large_Lyapunov`, `SE_L2_Southern_Halo`,
85
+ `SE_Butterfly`, `SE_Dragonfly`.
86
+
87
+ ## Supported Systems
88
+
89
+ | Key | System |
90
+ |-----|--------|
91
+ | `EM` | Earth-Moon |
92
+ | `SE` | Sun-Earth |
93
+
94
+ ## Output
95
+
96
+ `fam.save()` writes a `.npz` file (named after the preset, in `cfg.output_dir`)
97
+ containing initial states, periods, and `mu_star` for all converged orbits:
98
+
99
+ ```python
100
+ import numpy as np
101
+ data = np.load("results/SE_L2_Northern_Halo.npz")
102
+ data["states"] # (N, 6) containing initial [x, y, z, vx, vy, vz] in canonical units for each orbit
103
+ data["periods"] # (N,) containing full period in canonical units for each orbit
104
+ data["mu_star"] # System mass ratio
105
+ ```
106
+
107
+ ## Contributing
108
+
109
+ Clone or download the repository and run
110
+
111
+ ```bash
112
+ uv sync
113
+ ```
114
+
115
+ to install the package in editable mode plus dev dependencies. Run the test suite with:
116
+
117
+ ```bash
118
+ uv run pytest
119
+ ```
120
+
121
+ The repo includes a driver script, `scripts/crtbpModel.py`,
122
+ which reads `orbit_config.yaml` from the project root, runs continuation,
123
+ saves results, and shows plots.
124
+
125
+ An example configuration file (`orbit_config.example.yaml`), is included in the repository, and `orbit_config.yaml` can be created and used by running:
126
+
127
+ ```bash
128
+ cp orbit_config.example.yaml orbit_config.yaml # edit as needed
129
+ uv run python scripts/crtbpModel.py
130
+ ```
131
+
132
+ ## License
133
+
134
+ MIT — see [LICENSE](LICENSE).
@@ -0,0 +1,15 @@
1
+ orbit_gen/__init__.py,sha256=WwscDhNcpEt4Du2Km-jRjQHIdx2NlXqhE7wGaz_-ZKs,115
2
+ orbit_gen/family.py,sha256=_FCp6Mk9tR2VZ8jf4mVBEf2gru6ylWmNlX0iwmkAwmo,10052
3
+ orbit_gen/orbit.py,sha256=eaOgC3oMFKWTpKynpxQeOPETj1bsiSqhVI51y0WP2mg,4244
4
+ orbit_gen/setup/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ orbit_gen/setup/config.py,sha256=VWNFEZqbzcouLLTMQaidEC35KDlPFTOPI-4GGT1849Y,3482
6
+ orbit_gen/setup/kernels.py,sha256=vv7oOnD-C50SvdUDoQDR9vSCMcUnmy8t1romdhBXWU8,1497
7
+ orbit_gen/setup/orbit_ic.py,sha256=c1Vq4DuJESXYSiP-oQf-GS4PQk4zsom8SxxuO6sxXFY,1159
8
+ orbit_gen/setup/presets.py,sha256=TjHxsmqIMekbkNqTocWAAfU3bbi_scsCD2GCkgInIdc,2865
9
+ orbit_gen/setup/system_config.py,sha256=5-0DdHuO6AfqsTE3K8vIIO1MEGYFzIzhiiUar-APxiw,1179
10
+ orbit_gen/utils/eom.py,sha256=LNUCAojl1HbnRGAP3QqXtym5zV76rUUBymNIfUiNatk,8609
11
+ orbit_gen/utils/units.py,sha256=UC84KnM5ixz_-Cnz-FKUBab6CYdCVPRHBnO-XinbOek,763
12
+ orbit_gen-0.1.0.dist-info/licenses/LICENSE,sha256=gL4A20LPzIj-2WSYJhRqu2qJMDJbcWYveyQzUtswiK0,1069
13
+ orbit_gen-0.1.0.dist-info/WHEEL,sha256=4OL6Foqnnp3xRY5wMkjgc25_i5YJC6dKsC6LPcjqEoU,80
14
+ orbit_gen-0.1.0.dist-info/METADATA,sha256=L_sn__BgYYPr8tx8sXUwLBwgwYyPLcvle4m9FDyi_dA,3826
15
+ orbit_gen-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.12.5
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jayden Ripic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.