orbit-gen 0.1.0__tar.gz
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-0.1.0/LICENSE +21 -0
- orbit_gen-0.1.0/PKG-INFO +134 -0
- orbit_gen-0.1.0/README.md +116 -0
- orbit_gen-0.1.0/pyproject.toml +36 -0
- orbit_gen-0.1.0/pyproject.toml.orig +35 -0
- orbit_gen-0.1.0/src/orbit_gen/__init__.py +3 -0
- orbit_gen-0.1.0/src/orbit_gen/family.py +296 -0
- orbit_gen-0.1.0/src/orbit_gen/orbit.py +135 -0
- orbit_gen-0.1.0/src/orbit_gen/setup/__init__.py +0 -0
- orbit_gen-0.1.0/src/orbit_gen/setup/config.py +114 -0
- orbit_gen-0.1.0/src/orbit_gen/setup/kernels.py +56 -0
- orbit_gen-0.1.0/src/orbit_gen/setup/orbit_ic.py +41 -0
- orbit_gen-0.1.0/src/orbit_gen/setup/presets.py +102 -0
- orbit_gen-0.1.0/src/orbit_gen/setup/system_config.py +54 -0
- orbit_gen-0.1.0/src/orbit_gen/utils/eom.py +265 -0
- orbit_gen-0.1.0/src/orbit_gen/utils/units.py +27 -0
orbit_gen-0.1.0/LICENSE
ADDED
|
@@ -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.
|
orbit_gen-0.1.0/PKG-INFO
ADDED
|
@@ -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,116 @@
|
|
|
1
|
+
# orbit-gen
|
|
2
|
+
|
|
3
|
+
Periodic orbit family generation and continuation in the Circular Restricted
|
|
4
|
+
Three-Body Problem (CR3BP).
|
|
5
|
+
|
|
6
|
+
## Overview
|
|
7
|
+
|
|
8
|
+
`orbit-gen` generates families of periodic orbits in the CR3BP using
|
|
9
|
+
single-shooting differential correction and pseudo-arclength continuation.
|
|
10
|
+
It currently supports various families in the Earth-Moon and Sun-Earth systems.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install orbit-gen
|
|
16
|
+
```
|
|
17
|
+
Requires Python 3.12+.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
### From a config file
|
|
22
|
+
|
|
23
|
+
```yaml
|
|
24
|
+
# orbit_config.yaml
|
|
25
|
+
system: SE # "EM" (Earth-Moon) or "SE" (Sun-Earth)
|
|
26
|
+
preset: SE_L2_Northern_Halo
|
|
27
|
+
|
|
28
|
+
max_iter: 1000 # Maximum Newton iterations for a single differential correct attempt
|
|
29
|
+
eps: 1.0e-6 # Differential-correction convergence tolerance on residual norm
|
|
30
|
+
step: 0.01 # Pseudo-arclength continuation step size in canonical units
|
|
31
|
+
max_solutions: 20 # Maximum number of orbits to generate
|
|
32
|
+
max_period_days: 1000.0 # Maximum full orbit period in days
|
|
33
|
+
|
|
34
|
+
output_dir: results
|
|
35
|
+
show_plots: true # Optional; for user use only
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
from orbit_gen import Family, RunConfig
|
|
40
|
+
|
|
41
|
+
cfg = RunConfig.from_yaml("orbit_config.yaml")
|
|
42
|
+
fam = Family(cfg)
|
|
43
|
+
|
|
44
|
+
fam.save()
|
|
45
|
+
if cfg.show_plots:
|
|
46
|
+
fam.plot()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
> **Note:** constructing `Family(cfg)` runs the continuation, but does not save or plot results on its own — call `.save()` / `.plot()` explicitly, as shown above.
|
|
50
|
+
|
|
51
|
+
### Programmatically
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from orbit_gen import Family, RunConfig
|
|
55
|
+
from orbit_gen.setup.system_config import SE_SYSTEM
|
|
56
|
+
from orbit_gen.setup.presets import PRESETS
|
|
57
|
+
|
|
58
|
+
cfg = RunConfig(system=SE_SYSTEM, ic=PRESETS["SE_L2_Northern_Halo"])
|
|
59
|
+
fam = Family(cfg)
|
|
60
|
+
fam.save()
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Available preset keys (see `orbit_gen.setup.presets.PRESETS`):
|
|
64
|
+
`EM_L2_Northern_Halo`, `EM_DRO_1`, `EM_DRO_2`, `EM_L1`, `EM_Butterfly`,
|
|
65
|
+
`EM_Unnamed_1`, `SE_L2_Northern_Halo`, `SE_L1_Vertical`, `SE_L2_Lyapunov`,
|
|
66
|
+
`SE_L2_Small_Lyapunov`, `SE_L2_Large_Lyapunov`, `SE_L2_Southern_Halo`,
|
|
67
|
+
`SE_Butterfly`, `SE_Dragonfly`.
|
|
68
|
+
|
|
69
|
+
## Supported Systems
|
|
70
|
+
|
|
71
|
+
| Key | System |
|
|
72
|
+
|-----|--------|
|
|
73
|
+
| `EM` | Earth-Moon |
|
|
74
|
+
| `SE` | Sun-Earth |
|
|
75
|
+
|
|
76
|
+
## Output
|
|
77
|
+
|
|
78
|
+
`fam.save()` writes a `.npz` file (named after the preset, in `cfg.output_dir`)
|
|
79
|
+
containing initial states, periods, and `mu_star` for all converged orbits:
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
import numpy as np
|
|
83
|
+
data = np.load("results/SE_L2_Northern_Halo.npz")
|
|
84
|
+
data["states"] # (N, 6) containing initial [x, y, z, vx, vy, vz] in canonical units for each orbit
|
|
85
|
+
data["periods"] # (N,) containing full period in canonical units for each orbit
|
|
86
|
+
data["mu_star"] # System mass ratio
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## Contributing
|
|
90
|
+
|
|
91
|
+
Clone or download the repository and run
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
uv sync
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
to install the package in editable mode plus dev dependencies. Run the test suite with:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
uv run pytest
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
The repo includes a driver script, `scripts/crtbpModel.py`,
|
|
104
|
+
which reads `orbit_config.yaml` from the project root, runs continuation,
|
|
105
|
+
saves results, and shows plots.
|
|
106
|
+
|
|
107
|
+
An example configuration file (`orbit_config.example.yaml`), is included in the repository, and `orbit_config.yaml` can be created and used by running:
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
cp orbit_config.example.yaml orbit_config.yaml # edit as needed
|
|
111
|
+
uv run python scripts/crtbpModel.py
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
MIT — see [LICENSE](LICENSE).
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.11.7,<0.12.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "orbit-gen"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Tools for generating families and orbits in the CR3BP"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.12"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"astropy>=7.2.0",
|
|
15
|
+
"matplotlib>=3.10.9",
|
|
16
|
+
"numpy>=2.4.6",
|
|
17
|
+
"platformdirs>=4.10.0",
|
|
18
|
+
"pyyaml>=6.0.3",
|
|
19
|
+
"scipy>=1.17.1",
|
|
20
|
+
"spiceypy>=8.1.0",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[[project.authors]]
|
|
24
|
+
name = "Jayden Ripic"
|
|
25
|
+
email = "jmr595@cornell.edu"
|
|
26
|
+
|
|
27
|
+
[dependency-groups]
|
|
28
|
+
dev = [
|
|
29
|
+
"deptry>=0.25.1",
|
|
30
|
+
"pytest>=9.1.1",
|
|
31
|
+
"pytest-cov>=7.1.0",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[tool.pytest.ini_options]
|
|
35
|
+
log_cli = true
|
|
36
|
+
log_cli_level = "WARNING"
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["uv_build>=0.11.7,<0.12.0"]
|
|
3
|
+
build-backend = "uv_build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "orbit-gen"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Tools for generating families and orbits in the CR3BP"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Jayden Ripic", email = "jmr595@cornell.edu" },
|
|
14
|
+
]
|
|
15
|
+
requires-python = ">=3.12"
|
|
16
|
+
dependencies = [
|
|
17
|
+
"astropy>=7.2.0",
|
|
18
|
+
"matplotlib>=3.10.9",
|
|
19
|
+
"numpy>=2.4.6",
|
|
20
|
+
"platformdirs>=4.10.0",
|
|
21
|
+
"pyyaml>=6.0.3",
|
|
22
|
+
"scipy>=1.17.1",
|
|
23
|
+
"spiceypy>=8.1.0",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"deptry>=0.25.1",
|
|
29
|
+
"pytest>=9.1.1",
|
|
30
|
+
"pytest-cov>=7.1.0",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
log_cli = true
|
|
35
|
+
log_cli_level = "WARNING"
|
|
@@ -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
|
+
)
|