kelvinzero 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.
- kelvinzero-0.1.0/LICENSE +21 -0
- kelvinzero-0.1.0/MANIFEST.in +1 -0
- kelvinzero-0.1.0/PKG-INFO +131 -0
- kelvinzero-0.1.0/README.md +97 -0
- kelvinzero-0.1.0/pyproject.toml +48 -0
- kelvinzero-0.1.0/setup.cfg +4 -0
- kelvinzero-0.1.0/src/kelvinzero/__init__.py +64 -0
- kelvinzero-0.1.0/src/kelvinzero/__main__.py +102 -0
- kelvinzero-0.1.0/src/kelvinzero/constants.py +25 -0
- kelvinzero-0.1.0/src/kelvinzero/figures.py +393 -0
- kelvinzero-0.1.0/src/kelvinzero/frames.py +62 -0
- kelvinzero-0.1.0/src/kelvinzero/orbit.py +247 -0
- kelvinzero-0.1.0/src/kelvinzero/reference.py +69 -0
- kelvinzero-0.1.0/src/kelvinzero/regions.py +89 -0
- kelvinzero-0.1.0/src/kelvinzero/sun.py +136 -0
- kelvinzero-0.1.0/src/kelvinzero/viewer_dist/assets/index-Co4P4QfB.js +4443 -0
- kelvinzero-0.1.0/src/kelvinzero/viewer_dist/assets/index-DXHzM_N1.css +1 -0
- kelvinzero-0.1.0/src/kelvinzero/viewer_dist/index.html +14 -0
- kelvinzero-0.1.0/src/kelvinzero/viewer_dist/textures/earth-blue-marble.webp +0 -0
- kelvinzero-0.1.0/src/kelvinzero/viewer_dist/textures/earth-topology.webp +0 -0
- kelvinzero-0.1.0/src/kelvinzero/viewer_dist/vite.svg +1 -0
- kelvinzero-0.1.0/src/kelvinzero/viz.py +62 -0
- kelvinzero-0.1.0/src/kelvinzero.egg-info/PKG-INFO +131 -0
- kelvinzero-0.1.0/src/kelvinzero.egg-info/SOURCES.txt +27 -0
- kelvinzero-0.1.0/src/kelvinzero.egg-info/dependency_links.txt +1 -0
- kelvinzero-0.1.0/src/kelvinzero.egg-info/entry_points.txt +2 -0
- kelvinzero-0.1.0/src/kelvinzero.egg-info/requires.txt +10 -0
- kelvinzero-0.1.0/src/kelvinzero.egg-info/top_level.txt +1 -0
- kelvinzero-0.1.0/tests/test_model.py +187 -0
kelvinzero-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kyle Cooper and Yanni Nikas
|
|
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.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include src/kelvinzero/viewer_dist *
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: kelvinzero
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Eclipse-minimising sun-synchronous orbits in a mean-Sun rotating frame: the model and the visualizer behind the Kelvin Zero paper.
|
|
5
|
+
Author: Kyle Cooper, Yanni Nikas
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://k0webapp.web.app/
|
|
8
|
+
Project-URL: Paper, https://k0webapp.web.app/Kelvin-Zero-Whitepaper.pdf
|
|
9
|
+
Project-URL: Source, https://github.com/kylecooper7/kelvin-zero
|
|
10
|
+
Project-URL: Issues, https://github.com/kylecooper7/kelvin-zero/issues
|
|
11
|
+
Keywords: sun-synchronous orbit,eclipse,analemma,astrodynamics,J2,beta angle
|
|
12
|
+
Classifier: Development Status :: 4 - Beta
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
20
|
+
Classifier: Topic :: Scientific/Engineering :: Astronomy
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
License-File: LICENSE
|
|
25
|
+
Requires-Dist: numpy>=1.24
|
|
26
|
+
Provides-Extra: figures
|
|
27
|
+
Requires-Dist: matplotlib>=3.7; extra == "figures"
|
|
28
|
+
Requires-Dist: contourpy>=1.1; extra == "figures"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest>=7; extra == "dev"
|
|
31
|
+
Requires-Dist: matplotlib>=3.7; extra == "dev"
|
|
32
|
+
Requires-Dist: contourpy>=1.1; extra == "dev"
|
|
33
|
+
Dynamic: license-file
|
|
34
|
+
|
|
35
|
+
# Kelvin Zero
|
|
36
|
+
|
|
37
|
+
**Eclipse-minimising sun-synchronous orbits in a mean-Sun rotating frame — the open model.**
|
|
38
|
+
|
|
39
|
+
This package is the complete, inspectable source of every number, figure and 3-D view in the
|
|
40
|
+
Kelvin Zero whitepaper and the interactive visualizer at <https://k0webapp.web.app>. It is pure
|
|
41
|
+
NumPy, a few hundred lines, and organised the way the paper is:
|
|
42
|
+
|
|
43
|
+
| module | what it holds |
|
|
44
|
+
|---|---|
|
|
45
|
+
| `kelvinzero.sun` | the apparent Sun (USNO low-precision ephemeris), the mean Sun, the **analemma**, Earth's heliocentric position |
|
|
46
|
+
| `kelvinzero.frames` | the **EMI** (inertial) and **MSSI** (mean-Sun synchronous) frames, sidereal time |
|
|
47
|
+
| `kelvinzero.orbit` | the sun-synchronous condition, J2 nodal precession, 3-D positions, the **β angle**, the cylindrical-shadow **eclipse fraction**, yearly statistics |
|
|
48
|
+
| `kelvinzero.regions` | the always / sometimes / never-eclipsed **region maps** and their ∂U / ∂I contours |
|
|
49
|
+
| `kelvinzero.figures` | the paper's figures (matplotlib) |
|
|
50
|
+
| `kelvinzero.viz` | runs the **visualizer** locally — the same one as the website |
|
|
51
|
+
| `kelvinzero.reference` | reference values for cross-checking other implementations |
|
|
52
|
+
|
|
53
|
+
## Install
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
pip install kelvinzero # model + the built visualizer (NumPy only)
|
|
57
|
+
pip install "kelvinzero[figures]" # + matplotlib, for the figures
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
or from a checkout: `pip install -e ".[dev]"`.
|
|
61
|
+
|
|
62
|
+
## Thirty seconds
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from kelvinzero import Orbit, yearly_eclipse_stats, no_eclipse_beta_deg
|
|
66
|
+
|
|
67
|
+
o = Orbit(altitude_km=800, raan_offset_deg=270) # dawn–dusk, LTAN 06:00; inclination is set by the SSO condition
|
|
68
|
+
print(o.inclination_deg) # 98.603
|
|
69
|
+
print(no_eclipse_beta_deg(800)) # 62.69 — eclipse-free once |β| exceeds this
|
|
70
|
+
print(yearly_eclipse_stats(o)) # eclipse_fraction≈0.0267, eclipse_days≈74, β from −89.9° to −58.0°
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
kelvinzero stats --alt 1500 --raan 270 # a fixed plane that is never eclipsed
|
|
75
|
+
kelvinzero sweep --alt 800 # yearly eclipse versus node offset
|
|
76
|
+
kelvinzero figures out/ # regenerate the paper's figures
|
|
77
|
+
kelvinzero viz --alt 800 --raan 270 --view eclipse # the visualizer, in your browser, served from your machine
|
|
78
|
+
kelvinzero reference reference.json # values to test another implementation against
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## The visualizer
|
|
82
|
+
|
|
83
|
+
`kelvinzero viz` serves a built copy of the visualizer from <https://k0webapp.web.app> on a local
|
|
84
|
+
port and opens it — the identical React + Three.js application, minus the website chrome. Its
|
|
85
|
+
source is in [`viewer/`](viewer): the JavaScript model in `viewer/src/model` mirrors this package
|
|
86
|
+
function for function (`sun.js`, `frames.js`, `orbit.js`, `regions.js`), and the 3-D scene is
|
|
87
|
+
`viewer/src/scene/OrbitScene.js`. To work on it:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
cd viewer && npm install
|
|
91
|
+
npm run dev # live-reloading dev server
|
|
92
|
+
npm test # the JS model against this package's reference values (reference.json)
|
|
93
|
+
npm run build # refreshes the copy shipped in src/kelvinzero/viewer_dist
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## The model in one paragraph
|
|
97
|
+
|
|
98
|
+
A circular orbit of altitude *h* is sun-synchronous when its J2 nodal precession equals the mean
|
|
99
|
+
Sun's rate, which fixes the inclination (`sso_inclination_deg`). In the **MSSI** frame — EMI
|
|
100
|
+
rotated about the spin axis by the mean solar longitude *L(d)* — such a plane is frozen and the
|
|
101
|
+
real Sun traces the **analemma** (`sun_direction_mssi`, `analemma_mssi`). The Sun's elevation
|
|
102
|
+
above the plane is the **β angle** (`beta_angle`); the fraction of each revolution spent in
|
|
103
|
+
Earth's cylindrical shadow is the closed form `eclipse_fraction_per_orbit`, zero once
|
|
104
|
+
|β| ≥ asin(R/(R+h)). Averaging over the year gives `yearly_eclipse_stats`. Mapping
|
|
105
|
+
`max_t p·s` and `min_t p·s` over the sphere (`compute_sun_dot_extrema`) classifies every direction
|
|
106
|
+
as always / sometimes / never eclipsed for a given altitude (`classify_regions`,
|
|
107
|
+
`region_contours`).
|
|
108
|
+
|
|
109
|
+
Conventions: `d` is days since J2000.0 (2000-01-01 12:00); the node offset Ω is measured from the
|
|
110
|
+
mean Sun (Ω = 0 → LTAN 12:00, 90 → 18:00, 270 → 06:00); EMI has x toward the vernal equinox and z
|
|
111
|
+
along the spin axis.
|
|
112
|
+
|
|
113
|
+
## Verification
|
|
114
|
+
|
|
115
|
+
`pytest` checks the closed-form eclipse fraction against brute-force cylindrical-shadow sweeps,
|
|
116
|
+
the USNO ephemeris against the NOAA/Spencer series, frame conventions (an exact SSO is frozen in
|
|
117
|
+
MSSI, the Sun is west of the mean Sun in November, Ω = 90° ⇒ LTAN 18:00), the region maps against
|
|
118
|
+
the β-angle picture, and the headline numbers of the paper. `kelvinzero reference` writes the
|
|
119
|
+
values that the visualizer's JavaScript model is tested against.
|
|
120
|
+
|
|
121
|
+
## Citing
|
|
122
|
+
|
|
123
|
+
If you use the model or the paper, please cite the release (a `CITATION.cff` is included, so
|
|
124
|
+
GitHub's "Cite this repository" button gives you BibTeX/APA):
|
|
125
|
+
|
|
126
|
+
> K. Cooper and Y. Nikas, *Kelvin Zero: eclipse-minimising sun-synchronous orbits in a mean-Sun
|
|
127
|
+
> rotating frame*, v0.1.0 (2026). https://github.com/kylecooper7/kelvin-zero
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT.
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Kelvin Zero
|
|
2
|
+
|
|
3
|
+
**Eclipse-minimising sun-synchronous orbits in a mean-Sun rotating frame — the open model.**
|
|
4
|
+
|
|
5
|
+
This package is the complete, inspectable source of every number, figure and 3-D view in the
|
|
6
|
+
Kelvin Zero whitepaper and the interactive visualizer at <https://k0webapp.web.app>. It is pure
|
|
7
|
+
NumPy, a few hundred lines, and organised the way the paper is:
|
|
8
|
+
|
|
9
|
+
| module | what it holds |
|
|
10
|
+
|---|---|
|
|
11
|
+
| `kelvinzero.sun` | the apparent Sun (USNO low-precision ephemeris), the mean Sun, the **analemma**, Earth's heliocentric position |
|
|
12
|
+
| `kelvinzero.frames` | the **EMI** (inertial) and **MSSI** (mean-Sun synchronous) frames, sidereal time |
|
|
13
|
+
| `kelvinzero.orbit` | the sun-synchronous condition, J2 nodal precession, 3-D positions, the **β angle**, the cylindrical-shadow **eclipse fraction**, yearly statistics |
|
|
14
|
+
| `kelvinzero.regions` | the always / sometimes / never-eclipsed **region maps** and their ∂U / ∂I contours |
|
|
15
|
+
| `kelvinzero.figures` | the paper's figures (matplotlib) |
|
|
16
|
+
| `kelvinzero.viz` | runs the **visualizer** locally — the same one as the website |
|
|
17
|
+
| `kelvinzero.reference` | reference values for cross-checking other implementations |
|
|
18
|
+
|
|
19
|
+
## Install
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
pip install kelvinzero # model + the built visualizer (NumPy only)
|
|
23
|
+
pip install "kelvinzero[figures]" # + matplotlib, for the figures
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
or from a checkout: `pip install -e ".[dev]"`.
|
|
27
|
+
|
|
28
|
+
## Thirty seconds
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from kelvinzero import Orbit, yearly_eclipse_stats, no_eclipse_beta_deg
|
|
32
|
+
|
|
33
|
+
o = Orbit(altitude_km=800, raan_offset_deg=270) # dawn–dusk, LTAN 06:00; inclination is set by the SSO condition
|
|
34
|
+
print(o.inclination_deg) # 98.603
|
|
35
|
+
print(no_eclipse_beta_deg(800)) # 62.69 — eclipse-free once |β| exceeds this
|
|
36
|
+
print(yearly_eclipse_stats(o)) # eclipse_fraction≈0.0267, eclipse_days≈74, β from −89.9° to −58.0°
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
kelvinzero stats --alt 1500 --raan 270 # a fixed plane that is never eclipsed
|
|
41
|
+
kelvinzero sweep --alt 800 # yearly eclipse versus node offset
|
|
42
|
+
kelvinzero figures out/ # regenerate the paper's figures
|
|
43
|
+
kelvinzero viz --alt 800 --raan 270 --view eclipse # the visualizer, in your browser, served from your machine
|
|
44
|
+
kelvinzero reference reference.json # values to test another implementation against
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## The visualizer
|
|
48
|
+
|
|
49
|
+
`kelvinzero viz` serves a built copy of the visualizer from <https://k0webapp.web.app> on a local
|
|
50
|
+
port and opens it — the identical React + Three.js application, minus the website chrome. Its
|
|
51
|
+
source is in [`viewer/`](viewer): the JavaScript model in `viewer/src/model` mirrors this package
|
|
52
|
+
function for function (`sun.js`, `frames.js`, `orbit.js`, `regions.js`), and the 3-D scene is
|
|
53
|
+
`viewer/src/scene/OrbitScene.js`. To work on it:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
cd viewer && npm install
|
|
57
|
+
npm run dev # live-reloading dev server
|
|
58
|
+
npm test # the JS model against this package's reference values (reference.json)
|
|
59
|
+
npm run build # refreshes the copy shipped in src/kelvinzero/viewer_dist
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## The model in one paragraph
|
|
63
|
+
|
|
64
|
+
A circular orbit of altitude *h* is sun-synchronous when its J2 nodal precession equals the mean
|
|
65
|
+
Sun's rate, which fixes the inclination (`sso_inclination_deg`). In the **MSSI** frame — EMI
|
|
66
|
+
rotated about the spin axis by the mean solar longitude *L(d)* — such a plane is frozen and the
|
|
67
|
+
real Sun traces the **analemma** (`sun_direction_mssi`, `analemma_mssi`). The Sun's elevation
|
|
68
|
+
above the plane is the **β angle** (`beta_angle`); the fraction of each revolution spent in
|
|
69
|
+
Earth's cylindrical shadow is the closed form `eclipse_fraction_per_orbit`, zero once
|
|
70
|
+
|β| ≥ asin(R/(R+h)). Averaging over the year gives `yearly_eclipse_stats`. Mapping
|
|
71
|
+
`max_t p·s` and `min_t p·s` over the sphere (`compute_sun_dot_extrema`) classifies every direction
|
|
72
|
+
as always / sometimes / never eclipsed for a given altitude (`classify_regions`,
|
|
73
|
+
`region_contours`).
|
|
74
|
+
|
|
75
|
+
Conventions: `d` is days since J2000.0 (2000-01-01 12:00); the node offset Ω is measured from the
|
|
76
|
+
mean Sun (Ω = 0 → LTAN 12:00, 90 → 18:00, 270 → 06:00); EMI has x toward the vernal equinox and z
|
|
77
|
+
along the spin axis.
|
|
78
|
+
|
|
79
|
+
## Verification
|
|
80
|
+
|
|
81
|
+
`pytest` checks the closed-form eclipse fraction against brute-force cylindrical-shadow sweeps,
|
|
82
|
+
the USNO ephemeris against the NOAA/Spencer series, frame conventions (an exact SSO is frozen in
|
|
83
|
+
MSSI, the Sun is west of the mean Sun in November, Ω = 90° ⇒ LTAN 18:00), the region maps against
|
|
84
|
+
the β-angle picture, and the headline numbers of the paper. `kelvinzero reference` writes the
|
|
85
|
+
values that the visualizer's JavaScript model is tested against.
|
|
86
|
+
|
|
87
|
+
## Citing
|
|
88
|
+
|
|
89
|
+
If you use the model or the paper, please cite the release (a `CITATION.cff` is included, so
|
|
90
|
+
GitHub's "Cite this repository" button gives you BibTeX/APA):
|
|
91
|
+
|
|
92
|
+
> K. Cooper and Y. Nikas, *Kelvin Zero: eclipse-minimising sun-synchronous orbits in a mean-Sun
|
|
93
|
+
> rotating frame*, v0.1.0 (2026). https://github.com/kylecooper7/kelvin-zero
|
|
94
|
+
|
|
95
|
+
## License
|
|
96
|
+
|
|
97
|
+
MIT.
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "kelvinzero"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Eclipse-minimising sun-synchronous orbits in a mean-Sun rotating frame: the model and the visualizer behind the Kelvin Zero paper."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Kyle Cooper" }, { name = "Yanni Nikas" }]
|
|
13
|
+
keywords = ["sun-synchronous orbit", "eclipse", "analemma", "astrodynamics", "J2", "beta angle"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Science/Research",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
"License :: OSI Approved :: MIT License",
|
|
23
|
+
"Topic :: Scientific/Engineering :: Astronomy",
|
|
24
|
+
"Topic :: Scientific/Engineering :: Physics",
|
|
25
|
+
]
|
|
26
|
+
dependencies = ["numpy>=1.24"]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
figures = ["matplotlib>=3.7", "contourpy>=1.1"]
|
|
30
|
+
dev = ["pytest>=7", "matplotlib>=3.7", "contourpy>=1.1"]
|
|
31
|
+
|
|
32
|
+
[project.urls]
|
|
33
|
+
Homepage = "https://k0webapp.web.app/"
|
|
34
|
+
Paper = "https://k0webapp.web.app/Kelvin-Zero-Whitepaper.pdf"
|
|
35
|
+
Source = "https://github.com/kylecooper7/kelvin-zero"
|
|
36
|
+
Issues = "https://github.com/kylecooper7/kelvin-zero/issues"
|
|
37
|
+
|
|
38
|
+
[project.scripts]
|
|
39
|
+
kelvinzero = "kelvinzero.__main__:main"
|
|
40
|
+
|
|
41
|
+
[tool.setuptools.packages.find]
|
|
42
|
+
where = ["src"]
|
|
43
|
+
|
|
44
|
+
[tool.setuptools.package-data]
|
|
45
|
+
kelvinzero = ["viewer_dist/**/*"]
|
|
46
|
+
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
"""Kelvin Zero — eclipse-minimising sun-synchronous orbits in a mean-Sun rotating frame.
|
|
2
|
+
|
|
3
|
+
The model behind the paper and the visualizer at https://k0webapp.web.app. Everything is pure
|
|
4
|
+
NumPy; figures need matplotlib, the local 3-D viewer needs plotly (``pip install kelvinzero[viz]``).
|
|
5
|
+
|
|
6
|
+
Quick start::
|
|
7
|
+
|
|
8
|
+
from kelvinzero import Orbit, yearly_eclipse_stats
|
|
9
|
+
print(yearly_eclipse_stats(Orbit(altitude_km=800, raan_offset_deg=270)))
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from .constants import DEG, J2, MU, R_EARTH, TROPICAL_YEAR_DAYS
|
|
13
|
+
from .frames import eci_to_display, frame_angle, gmst, lat_lng_to_vec, rotate_z, vec_to_lat_lng
|
|
14
|
+
from .orbit import (
|
|
15
|
+
Orbit,
|
|
16
|
+
YearlyStats,
|
|
17
|
+
alpha_deg_for_altitude,
|
|
18
|
+
altitude_for_sso_inclination_deg,
|
|
19
|
+
argument_of_latitude,
|
|
20
|
+
best_node_offset,
|
|
21
|
+
beta_angle,
|
|
22
|
+
eclipse_fraction_per_orbit,
|
|
23
|
+
is_eclipsed,
|
|
24
|
+
ltan_hours,
|
|
25
|
+
no_eclipse_beta_deg,
|
|
26
|
+
orbit_normal_eci,
|
|
27
|
+
orbit_ring_eci,
|
|
28
|
+
orbital_period,
|
|
29
|
+
plane_normal,
|
|
30
|
+
plane_point,
|
|
31
|
+
raan_eci,
|
|
32
|
+
raan_mssi,
|
|
33
|
+
raan_precession_rate,
|
|
34
|
+
satellite_position_eci,
|
|
35
|
+
sso_inclination_deg,
|
|
36
|
+
yearly_eclipse_fraction_sampled,
|
|
37
|
+
yearly_eclipse_stats,
|
|
38
|
+
)
|
|
39
|
+
from .regions import (
|
|
40
|
+
REGION_ALWAYS_ECLIPSE,
|
|
41
|
+
REGION_NO_ECLIPSE,
|
|
42
|
+
REGION_SOME_ECLIPSE,
|
|
43
|
+
SunDotExtrema,
|
|
44
|
+
classify_regions,
|
|
45
|
+
compute_sun_dot_extrema,
|
|
46
|
+
region_area_fractions,
|
|
47
|
+
region_contours,
|
|
48
|
+
sun_dot_extrema_at,
|
|
49
|
+
)
|
|
50
|
+
from .sun import (
|
|
51
|
+
SunState,
|
|
52
|
+
analemma_mssi,
|
|
53
|
+
earth_heliocentric_ecliptic,
|
|
54
|
+
ecliptic_eci,
|
|
55
|
+
mean_sun_longitude,
|
|
56
|
+
sun_direction_eci,
|
|
57
|
+
sun_direction_mssi,
|
|
58
|
+
sun_state,
|
|
59
|
+
year_markers,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
__version__ = "0.1.0"
|
|
63
|
+
|
|
64
|
+
__all__ = [name for name in dir() if not name.startswith("_")]
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
"""Command-line interface.
|
|
2
|
+
|
|
3
|
+
python -m kelvinzero stats --alt 800 --raan 270
|
|
4
|
+
python -m kelvinzero sweep --alt 800
|
|
5
|
+
python -m kelvinzero figures out/
|
|
6
|
+
python -m kelvinzero viz --alt 800 --raan 270 --view eclipse
|
|
7
|
+
python -m kelvinzero reference reference.json
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
import argparse
|
|
13
|
+
import sys
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
|
|
17
|
+
from .orbit import Orbit, alpha_deg_for_altitude, best_node_offset, ltan_hours, no_eclipse_beta_deg, orbital_period, yearly_eclipse_stats
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def cmd_stats(a):
|
|
21
|
+
o = Orbit(a.alt, a.raan)
|
|
22
|
+
st = yearly_eclipse_stats(o, 366)
|
|
23
|
+
print(f"altitude {o.altitude_km:8.1f} km")
|
|
24
|
+
print(f"SSO inclination {o.inclination_deg:8.3f} deg")
|
|
25
|
+
print(f"period {orbital_period(o.altitude_km) / 60:8.2f} min")
|
|
26
|
+
print(f"node offset Ω {o.raan_offset_deg:8.2f} deg (LTAN {ltan_hours(o.raan_offset_deg):05.2f} h)")
|
|
27
|
+
print(f"eclipse-free |β| ≥ {float(no_eclipse_beta_deg(o.altitude_km)):8.2f} deg")
|
|
28
|
+
print(f"β over the year {st.beta_min_deg:8.2f} … {st.beta_max_deg:.2f} deg")
|
|
29
|
+
print(f"yearly eclipse {100 * st.eclipse_fraction:8.3f} % of time (sun exposure {100 * st.sun_exposure:.3f} %)")
|
|
30
|
+
print(f"days with eclipse {st.eclipse_days:8.1f} / 365")
|
|
31
|
+
print(f"region cone α {float(alpha_deg_for_altitude(o.altitude_km)):8.2f} deg")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def cmd_sweep(a):
|
|
35
|
+
print(f"altitude {a.alt:g} km: yearly eclipse vs node offset")
|
|
36
|
+
for O in np.arange(0, 360, a.step):
|
|
37
|
+
st = yearly_eclipse_stats(Orbit(a.alt, float(O)), 183)
|
|
38
|
+
bar = "#" * int(round(st.eclipse_fraction * 100))
|
|
39
|
+
print(f" Ω = {O:5.1f}° {100 * st.eclipse_fraction:6.2f} % {bar}")
|
|
40
|
+
O, st = best_node_offset(a.alt, a.step, 183)
|
|
41
|
+
print(f"best: Ω = {O:g}° ({100 * st.eclipse_fraction:.3f} % of the year, {st.eclipse_days:.0f} days with eclipse)")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def cmd_figures(a):
|
|
45
|
+
from .figures import make_all
|
|
46
|
+
|
|
47
|
+
make_all(a.out, a.only)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cmd_viz(a):
|
|
51
|
+
from .viz import serve
|
|
52
|
+
|
|
53
|
+
serve(port=a.port, open_browser=not a.no_browser, alt=a.alt, raan=a.raan, view=a.view, day=a.day, cam=a.cam, frame=a.frame)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cmd_reference(a):
|
|
57
|
+
from .reference import write_reference
|
|
58
|
+
|
|
59
|
+
print("wrote", write_reference(a.out))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def main(argv=None):
|
|
63
|
+
# Windows consoles often default to a legacy code page; make Greek letters and ≥ printable everywhere.
|
|
64
|
+
for stream in (sys.stdout, sys.stderr):
|
|
65
|
+
if hasattr(stream, "reconfigure"):
|
|
66
|
+
try:
|
|
67
|
+
stream.reconfigure(encoding="utf-8", errors="replace")
|
|
68
|
+
except (ValueError, OSError):
|
|
69
|
+
pass
|
|
70
|
+
p = argparse.ArgumentParser(prog="kelvinzero", description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
|
71
|
+
sub = p.add_subparsers(dest="cmd", required=True)
|
|
72
|
+
s = sub.add_parser("stats", help="yearly eclipse statistics for one orbit")
|
|
73
|
+
s.add_argument("--alt", type=float, default=800.0, help="altitude (km)")
|
|
74
|
+
s.add_argument("--raan", type=float, default=270.0, help="node offset from the mean Sun (deg)")
|
|
75
|
+
s.set_defaults(fn=cmd_stats)
|
|
76
|
+
s = sub.add_parser("sweep", help="yearly eclipse versus node offset at one altitude")
|
|
77
|
+
s.add_argument("--alt", type=float, default=800.0)
|
|
78
|
+
s.add_argument("--step", type=float, default=10.0)
|
|
79
|
+
s.set_defaults(fn=cmd_sweep)
|
|
80
|
+
s = sub.add_parser("figures", help="regenerate the paper's figures (matplotlib)")
|
|
81
|
+
s.add_argument("out", nargs="?", default="figures")
|
|
82
|
+
s.add_argument("--only", nargs="*")
|
|
83
|
+
s.set_defaults(fn=cmd_figures)
|
|
84
|
+
s = sub.add_parser("viz", help="run the Kelvin Zero visualizer locally (the same one as k0webapp.web.app)")
|
|
85
|
+
s.add_argument("--alt", type=float, help="altitude (km)")
|
|
86
|
+
s.add_argument("--raan", type=float, help="node offset from the mean Sun (deg)")
|
|
87
|
+
s.add_argument("--view", choices=["overview", "eclipse", "frames", "helio", "sun"], help="preset view to open")
|
|
88
|
+
s.add_argument("--day", type=float, help="timeline day (0-365.24)")
|
|
89
|
+
s.add_argument("--cam", help="camera preset: sun, antisun, pole, side, follow, above")
|
|
90
|
+
s.add_argument("--frame", choices=["EMI", "MSSI"])
|
|
91
|
+
s.add_argument("--port", type=int, default=0, help="local port (default: a free one)")
|
|
92
|
+
s.add_argument("--no-browser", action="store_true", help="just serve; do not open a browser")
|
|
93
|
+
s.set_defaults(fn=cmd_viz)
|
|
94
|
+
s = sub.add_parser("reference", help="write reference values (JSON) for cross-checking other implementations")
|
|
95
|
+
s.add_argument("out", nargs="?", default="reference.json")
|
|
96
|
+
s.set_defaults(fn=cmd_reference)
|
|
97
|
+
a = p.parse_args(argv)
|
|
98
|
+
a.fn(a)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
if __name__ == "__main__":
|
|
102
|
+
sys.exit(main())
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Physical constants. These match the values used by the Kelvin Zero visualizer so the two
|
|
2
|
+
implementations can be compared number for number."""
|
|
3
|
+
|
|
4
|
+
import math
|
|
5
|
+
|
|
6
|
+
DEG = math.pi / 180.0
|
|
7
|
+
TWO_PI = 2.0 * math.pi
|
|
8
|
+
|
|
9
|
+
MU = 398600.4418 # km^3/s^2, Earth gravitational parameter
|
|
10
|
+
R_EARTH = 6378.137 # km, WGS84 equatorial radius
|
|
11
|
+
J2 = 1.08262668e-3
|
|
12
|
+
SECONDS_PER_DAY = 86400.0
|
|
13
|
+
TROPICAL_YEAR_DAYS = 365.2422
|
|
14
|
+
|
|
15
|
+
#: Mean-sun rate: the uniform eastward motion of the fictitious mean Sun along the equator
|
|
16
|
+
#: (USNO low-precision ephemeris), degrees per day. An exactly sun-synchronous orbit's node
|
|
17
|
+
#: precesses at this rate.
|
|
18
|
+
MEAN_SUN_RATE_DEG_DAY = 0.9856474
|
|
19
|
+
MEAN_SUN_RATE_RAD_S = MEAN_SUN_RATE_DEG_DAY * DEG / SECONDS_PER_DAY
|
|
20
|
+
|
|
21
|
+
#: Rate used when solving the sun-synchronous inclination (rad/s).
|
|
22
|
+
OMEGA_SSO_REQ = TWO_PI / (TROPICAL_YEAR_DAYS * SECONDS_PER_DAY)
|
|
23
|
+
|
|
24
|
+
ALT_MIN_KM = 100.0
|
|
25
|
+
ALT_MAX_KM = 2000.0
|