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.
Files changed (29) hide show
  1. kelvinzero-0.1.0/LICENSE +21 -0
  2. kelvinzero-0.1.0/MANIFEST.in +1 -0
  3. kelvinzero-0.1.0/PKG-INFO +131 -0
  4. kelvinzero-0.1.0/README.md +97 -0
  5. kelvinzero-0.1.0/pyproject.toml +48 -0
  6. kelvinzero-0.1.0/setup.cfg +4 -0
  7. kelvinzero-0.1.0/src/kelvinzero/__init__.py +64 -0
  8. kelvinzero-0.1.0/src/kelvinzero/__main__.py +102 -0
  9. kelvinzero-0.1.0/src/kelvinzero/constants.py +25 -0
  10. kelvinzero-0.1.0/src/kelvinzero/figures.py +393 -0
  11. kelvinzero-0.1.0/src/kelvinzero/frames.py +62 -0
  12. kelvinzero-0.1.0/src/kelvinzero/orbit.py +247 -0
  13. kelvinzero-0.1.0/src/kelvinzero/reference.py +69 -0
  14. kelvinzero-0.1.0/src/kelvinzero/regions.py +89 -0
  15. kelvinzero-0.1.0/src/kelvinzero/sun.py +136 -0
  16. kelvinzero-0.1.0/src/kelvinzero/viewer_dist/assets/index-Co4P4QfB.js +4443 -0
  17. kelvinzero-0.1.0/src/kelvinzero/viewer_dist/assets/index-DXHzM_N1.css +1 -0
  18. kelvinzero-0.1.0/src/kelvinzero/viewer_dist/index.html +14 -0
  19. kelvinzero-0.1.0/src/kelvinzero/viewer_dist/textures/earth-blue-marble.webp +0 -0
  20. kelvinzero-0.1.0/src/kelvinzero/viewer_dist/textures/earth-topology.webp +0 -0
  21. kelvinzero-0.1.0/src/kelvinzero/viewer_dist/vite.svg +1 -0
  22. kelvinzero-0.1.0/src/kelvinzero/viz.py +62 -0
  23. kelvinzero-0.1.0/src/kelvinzero.egg-info/PKG-INFO +131 -0
  24. kelvinzero-0.1.0/src/kelvinzero.egg-info/SOURCES.txt +27 -0
  25. kelvinzero-0.1.0/src/kelvinzero.egg-info/dependency_links.txt +1 -0
  26. kelvinzero-0.1.0/src/kelvinzero.egg-info/entry_points.txt +2 -0
  27. kelvinzero-0.1.0/src/kelvinzero.egg-info/requires.txt +10 -0
  28. kelvinzero-0.1.0/src/kelvinzero.egg-info/top_level.txt +1 -0
  29. kelvinzero-0.1.0/tests/test_model.py +187 -0
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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