Skip to content

Repository files navigation

Constellation Planner

Walker constellation generation with ground grid coverage and revisit time analysis.

CI Python License

Coverage against latitude band for four Walker patterns flying 66 satellites each: the 53 degree delta falls to zero above 75 degrees of latitude while the 70 degree delta drops to 68.9 percent at the equator, so the two designs fail at opposite ends of the globe

Which constellation to fly

You have sixty six satellites. The question is what to do with them, and the honest answer is that it depends on the requirement, in a way the usual headline number will not tell you.

Four designs, all at sixty six satellites so the comparison is geometry and not budget, swept over twenty four hours against an equal-area partition of the WGS-84 ellipsoid with a ten degree elevation mask:

Design Notation Pattern Altitude (km) Coverage Two-fold Satellites in view Mean revisit Max revisit
star-86 86.4:66/6/1 star 780 99.02% 45.04% 1.76 0.5 min 2.5 min
delta-53 53:66/6/2 delta 780 89.63% 66.50% 1.76 2.1 min 2.64 h
delta-70 70:66/6/2 delta 780 89.43% 61.62% 1.76 4.0 min 83.5 min
delta-53-1200 53:66/6/2 delta 1200 96.47% 92.49% 2.88 0.3 min 51.0 min

If the requirement is a bound on the worst gap anywhere on Earth, fly the star pattern. Its largest gap at any cell over the whole window is 2.5 minutes, against 83.5 minutes for the 70 degree delta and 2.64 hours for the 53 degree one, and it is the only one of the four that meets a tight bound.

If the requirement is two satellites in view over the latitudes where people live, the ranking reverses. The star pattern is last on two-fold coverage at 45.04 percent, because its planes converge at the poles and the surplus is taken from everywhere else, and the 53 degree delta at 1200 km is the only design holding 100 percent two-fold coverage from 15 to 60 degrees.

Now the part that is easy to get wrong. The two 780 km delta patterns report 89.63 and 89.43 percent coverage, a difference of 0.2 points, which is noise. On that number they are the same design. They are not the same design. The 53 degree pattern cannot reach past 71.7 degrees of latitude, its inclination plus the 18.658 degree Earth central angle of its footprint, so 78 of the 2000 grid cells are never covered at all and the band below them has a 2.64 hour gap. The 70 degree pattern covers the poles and pays for it at the equator, where its coverage is 68.9 percent and its mean gap is 12.5 minutes, fifteen times the 0.8 minute mean gap of the star pattern in the same band. The figure above is those two numbers, 89.63 and 89.43, drawn out against latitude, and it is the reason this tool reports per band and not only globally.

Results

Produced by uv run python examples/compare_designs.py, on Python 3.12.10 with numpy 2.5.1, scipy 1.18.0, and matplotlib 3.11.1, on one core of an AMD64 desktop under Windows 11. All four designs are swept on the same 2000 cell equal-area grid, each cell 255033 km2, over 24 hours at 30 second sampling with a 10 degree elevation mask. The whole run takes about 21 seconds; one design on its own is 380 million elevation evaluations and takes 5.4 seconds.

Coverage and two-fold coverage by latitude band, banded in the magnitude of the geodetic latitude:

Latitude band (deg) star-86 coverage / two-fold delta-53 coverage / two-fold delta-70 coverage / two-fold delta-53-1200 coverage / two-fold
0 to 15 97.2% / 16.4% 88.7% / 50.6% 68.9% / 46.4% 99.9% / 92.9%
15 to 30 99.1% / 23.5% 98.9% / 62.7% 90.7% / 37.9% 100.0% / 100.0%
30 to 45 99.9% / 45.2% 100.0% / 96.6% 99.6% / 60.6% 100.0% / 100.0%
45 to 60 100.0% / 83.6% 100.0% / 96.2% 100.0% / 98.8% 100.0% / 100.0%
60 to 75 100.0% / 99.1% 64.8% / 24.5% 100.0% / 100.0% 97.8% / 81.1%
75 to 90 100.0% / 100.0% 0.0% / 0.0% 98.7% / 84.8% 15.6% / 0.0%

Mean and maximum revisit time over the same bands:

Latitude band (deg) Area share star-86 mean / max delta-53 mean / max delta-70 mean / max delta-53-1200 mean / max
0 to 15 27.0% 0.8 min / 2.5 min 4.6 min / 55.5 min 12.5 min / 83.5 min 0.3 min / 2.0 min
15 to 30 21.7% 0.7 min / 2.5 min 0.5 min / 2.5 min 2.7 min / 20.0 min 0.0 min / 0.0 min
30 to 45 24.5% 0.4 min / 1.0 min 0.1 min / 0.5 min 0.3 min / 1.5 min 0.0 min / 0.0 min
45 to 60 13.9% 0.0 min / 0.0 min 0.0 min / 0.0 min 0.0 min / 0.0 min 0.0 min / 0.0 min
60 to 75 9.0% 0.0 min / 0.0 min 6.9 min / 2.64 h 0.0 min / 0.0 min 0.2 min / 1.5 min
75 to 90 3.9% 0.0 min / 0.0 min n/a / n/a 0.1 min / 6.5 min 8.0 min / 51.0 min

A zero entry means the band was covered at every sample, so no gap was observed. An n/a entry means no cell in the band was ever covered, so no revisit time exists; reporting it as a large number would be an invention and reporting it as zero would be wrong in the opposite direction.

Two equal-area world maps of the worst coverage gap at every grid cell, one per design: the 53 degree delta leaves both polar caps never covered and a ring of long gaps below them, while the 70 degree delta covers the poles and leaves a band of long gaps along the equator

Three further readings of the tables:

  • The three designs at 780 km see the same mean number of satellites, 1.76, because that quantity depends only on the satellite count and the footprint size, not on where the footprints are put. The closed form agrees: at 780 km with a 10 degree mask the Earth central angle is 18.658 degrees, so one footprint is 2.628 percent of the sphere and sixty six of them sum to 1.734 footprints per point, the shortfall against 1.76 being the difference between the spherical closed form and the oblate model. Coverage is therefore entirely a question of where a design puts that fixed area, which is what separates the rows.
  • The star pattern wins the worst case and loses on redundancy. Its planes converge at the poles, so the polar bands are covered continuously and by four or more satellites at once, and the surplus is taken from the equator, where its coverage is 97.2 percent, the lowest of the three at that altitude.
  • Raising the altitude buys more than rearranging the planes. Moving the 53 degree delta from 780 km to 1200 km raises the Earth central angle from 18.658 to 24.018 degrees and the footprint from 2.628 to 4.329 percent of the sphere. Coverage rises from 89.63 to 96.47 percent, two-fold coverage from 66.50 to 92.49 percent, and the worst gap falls from 2.64 hours to 51 minutes. What no altitude fixes is the polar hole: the design now reaches 77.0 degrees rather than 71.7, and the band above that is still empty.

The browser animation in viz/ shows the same designs frame by frame over a rotating globe or an equal-area map, reading JSON written by examples/export_viz_trace.py. It is described in viz/README.md.

About the figures

The three figures in docs/figures are snapshots of one run of

uv run python examples/make_figures.py

which regenerates all three and prints their total size against the 250 kB budget they are kept inside. CI does not compare them byte for byte, because matplotlib output is not byte reproducible across platforms or versions: the same command on a Linux runner draws the same picture and writes different bytes. What CI does check is that the command runs and writes what it claims to, which is what tests/test_examples.py does under a reduced grid.

The equal area grid

Every number above is a plain unweighted mean over grid cells. That is only an area weighted mean over the Earth because the cells have equal area, and getting that wrong would have biased exactly the quantity under study.

A grid of equal steps in latitude and longitude has cells whose area falls as the cosine of the latitude. At one degree resolution a polar cell is about one fifty seventh the area of an equatorial one, so averaging a coverage indicator over such cells weights the poles roughly fifty seven times too heavily. The designs compared here differ mainly in how they trade polar coverage against equatorial coverage, so that bias falls precisely on the answer: a latitude and longitude grid would grade the 53 degree delta, which covers nothing above 71.7 degrees, against a version of the Earth in which the region it misses is many times larger than it is.

Cells therefore come from the recursive zonal equal-area partition of Leopardi (2006), specialised to the two-sphere. Two polar caps of the target area are taken off, the rest is divided into collars, each collar is given an integer number of cells by rounding with a carried error, and the collar boundaries are then recomputed from the cumulative cell count so that every cell encloses exactly the target area. The construction is exact rather than approximate.

The partition is built on the sphere of authalic latitude, where equal area on the sphere is equal area on the ellipsoid by the defining property of that latitude (Snyder 1987), and the cell centres are mapped back to geodetic latitude. Cells therefore have equal area on the WGS-84 ellipsoid itself and not merely equal solid angle. tests/test_grid.py checks this against an independent quadrature of the ellipsoidal area element, which shares no code with the partition, and requires agreement to 1e-11 relative.

docs/design-notes.md records why a latitude and longitude grid, HEALPix, and a Fibonacci lattice were each rejected.

From elevation angle to revisit time

What counts as visible. A cell is visible from a satellite when the satellite stands at or above the required elevation measured against the geodetic vertical at the cell centre, which on an oblate Earth departs from the geocentric radial direction by up to 0.1924 degrees. The requirement is normally one angle applied everywhere, which is how a coverage requirement is written and which absorbs terrain, refraction margin, and the antenna pattern into a single number. It can also be a HorizonMask, which carries an angle for every cell and every azimuth sector, so a surveyed horizon profile or a bounded scan cone can be expressed directly. A mask that does not vary with azimuth costs what the single angle costs; one that does costs about four times as much, since the azimuth of every satellite from every cell has to be resolved at every time sample.

The kernel forms the vertical component and the squared range as inner products rather than materialising the satellites by cells by three difference array,

numerator = r_sat . up_g  -  r_g . up_g,
range^2   = |r_sat|^2 - 2 r_sat . r_g + |r_g|^2,

which turns it into two matrix products with an inner dimension of three. The comparison is made on the sine of the elevation, which is monotone over the range of interest, so no arcsine is evaluated over the large array.

What counts as a gap. A gap at a cell is a maximal run of consecutive samples with no satellite in view, and its duration is the number of uncovered samples times the time step, which is within one time step of the truth. Runs that touch either end of the window are censored and dropped, because their true length is not observable and keeping them would bias the maximum downwards, which is the direction that matters. Cells that are never covered are counted separately rather than scored as zero.

Both the mean and the maximum are reported, because for the designs that are interesting they are different questions. The mean gap of the 53 degree delta is 2.1 minutes and its largest is 2.64 hours, a factor of seventy five, and its 95th percentile gap is 8.0 minutes. A design chosen on the mean would be chosen on a number no user experiences.

Distribution of coverage gap durations for the 53 degree delta by latitude band on a logarithmic axis: gaps below 45 degrees of latitude all finish inside three minutes while the 60 to 75 degree band runs out past 150 minutes

Why the propagation has to be more than two-body. Two-body motion gives a ground track that closes on itself, so a coverage gap found on the first day would be the gap for ever. Right ascension of the ascending node, argument of perigee, and mean anomaly are therefore advanced at the first-order J2 secular rates of Brouwer (1959) and Kozai (1959); the semi-major axis, eccentricity, and inclination have no secular rate to first order and are held fixed. The nodal regression is 4.00 degrees a day at 780 km and 53 degrees inclination, which moves the coverage pattern well inside the span of a normal analysis. The same rates define the two orbit families a coverage argument needs, and examples/repeat_ground_track.py tabulates both: the model gives the sun-synchronous inclination at 800 km as 98.6031 degrees against the tabulated 98.60, and it solves for a semi-major axis whose ground track repeats after a given number of revolutions in a given number of nodal days, which the tests then confirm by propagating and comparing the Earth fixed positions.

Installation

Requires Python 3.12 or later. Continuous integration runs the whole suite on 3.12 and 3.13, on Linux and on Windows, so the version floor in pyproject.toml is a tested claim rather than a declared one.

git clone https://github.com/Eelis03/constellation-planner.git
cd constellation-planner
uv sync

Using pip instead of uv:

python -m venv .venv
.venv/bin/activate      # Windows: .venv\Scripts\activate
pip install -e ".[dev]"

The package ships a py.typed marker, so an installing project gets the annotations rather than Any at every boundary.

Running it

import math

from constellation_planner import (
    ConstellationDesign,
    EarthModel,
    EqualAreaGrid,
    SweepConfig,
    WalkerPattern,
    run_design_sweep,
    summarise_coverage,
)

earth = EarthModel.wgs84()
grid = EqualAreaGrid.equal_area(2000, earth)

design = ConstellationDesign(
    name="delta-53",
    pattern=WalkerPattern.from_notation("53:66/6/2", kind="delta"),
    altitude=780_000.0,
)
config = SweepConfig(duration=24 * 3600.0, time_step=30.0, elevation_mask=math.radians(10.0))

trace = run_design_sweep(design, grid, config, earth)
summary = summarise_coverage(trace.fold, config.time_step)
print(f"{summary.single_fold_fraction:.4f} {summary.max_revisit / 60.0:.1f} min")
# 0.8963 158.5 min

Passing kind="star" to the pattern, or a different altitude to the design, requires no other change. To vary the visibility criterion over the Earth, pass a HorizonMask instead of an angle:

import numpy as np

from constellation_planner import HorizonMask

# Every cell needs 10 degrees, except one that has a ridge filling the northern
# quadrant of its sky up to 40 degrees.
profiles = np.full((grid.cell_count, 4), math.radians(10.0))
profiles[17, 0] = math.radians(40.0)
config = SweepConfig(
    duration=24 * 3600.0,
    time_step=30.0,
    elevation_mask=HorizonMask.from_profiles(profiles),
)

Five runnable scripts live in examples/. They carry no logic; everything they call is in the package.

uv run python examples/compare_designs.py       # the tables above
uv run python examples/analyse_design.py --design star-86
uv run python examples/make_figures.py          # the figures in docs/figures
uv run python examples/repeat_ground_track.py   # sun-synchronous and repeating orbits
uv run python examples/export_viz_trace.py      # JSON for the browser animation

Checks:

uv run pytest --cov=src/constellation_planner --cov-report=term-missing
uv run ruff check .
uv run ruff format --check .
uv run mypy

270 tests run in about 15 seconds and cover 99 percent of the package. CI runs the same command with --cov-fail-under=96, a floor set below the measured figure so that moving statements around does not fail a build while a module added without tests does.

The suite has three tiers. Property and invariant tests cover the mathematics: a Walker pattern has t satellites in p planes with the published right ascension and phasing spacing; every grid cell has the nominal area on the ellipsoid; visibility from the ground and an unobstructed line of sight agree, and the line of sight predicate does not depend on argument order; a satellite on the local vertical has an elevation of 90 degrees and one at the closed-form horizon has an elevation of zero and a range of sqrt(r^2 - Re^2); a geostationary satellite covers a set of cells that does not change over a day, whose boundary matches arccos((Re / r) cos(eps)) - eps to within one cell radius; raising the elevation mask never adds a visible pair, and neither does raising any entry of a horizon profile; and a single satellite in a repeating ground track produces a coverage history that is exactly periodic with the repeat period.

Regression tests compare a fresh 12 hour, 500 cell, 24 satellite run against tests/data/reference_coverage.json. Only reproducible quantities are pinned, and every tolerance is derived from the resolution of its measurement rather than from an observed difference: coverage fractions are means over cells and are compared to within two cells, 2 / cell_count; revisit times are counted in samples and are compared to within two time steps, one for each end of a gap. The reasoning is written into the docstring of each test.

Integration tests run every script in examples/ as a subprocess under a reduced grid and window, writing into a temporary directory, and check that the declared outputs appear. One of them asserts that no script can be added to examples/ without an entry in that table.

What is not modelled

Visibility here is geometric. Received power, antenna gain, rain fade, interference, Doppler, and the number of users a satellite can serve at once are all out of scope, and a design reported as covering 99 percent of the Earth may deliver no usable service at the low elevations that make up much of that figure. The propagator carries only the first-order secular J2 terms, so it is adequate over days and not over months: drag, higher harmonics, third bodies, and station keeping are absent. Coverage fractions are quantised by the cell count and revisit times by the time step. Gaps longer than the window cannot be measured, so a reported maximum is a lower bound whenever a design has gaps of the order of the window. The frame model is a rotation about the polar axis at a constant rate, with no precession, nutation, polar motion, or calendar, so no result can be tied to a date and nothing can be said about lighting or local solar time.

docs/design-notes.md states each of these in full, together with what it would cost to remove, and records the one that has been removed.

References

Algorithms:

  • J. G. Walker, "Satellite constellations", Journal of the British Interplanetary Society 37, 1984, pages 559 to 572. The i: t/p/f notation and the phasing rule.
  • J. G. Walker, "Circular orbit patterns providing continuous whole earth coverage", Royal Aircraft Establishment Technical Report 70211, 1970. https://apps.dtic.mil/sti/citations/AD0722776
  • A. H. Ballard, "Rosette constellations of earth satellites", IEEE Transactions on Aerospace and Electronic Systems AES-16(5), 1980, pages 656 to 673. DOI 10.1109/TAES.1980.308932
  • W. S. Adams and L. Rider, "Circular polar constellations providing continuous single or multiple coverage above a specified latitude", The Journal of the Astronautical Sciences 35(2), 1987, pages 155 to 192. The star pattern.
  • L. Rider, "Analytic design of satellite constellations for zonal earth coverage using inclined circular orbits", The Journal of the Astronautical Sciences 34(1), 1986, pages 31 to 64.
  • D. Brouwer, "Solution of the problem of artificial satellite theory without drag", The Astronomical Journal 64, 1959, pages 378 to 397. DOI 10.1086/107958
  • Y. Kozai, "The motion of a close earth satellite", The Astronomical Journal 64, 1959, pages 367 to 377. DOI 10.1086/107957
  • D. A. Vallado, "Fundamentals of Astrodynamics and Applications", 4th edition, Microcosm Press, 2013, section 9.6. The secular J2 rates in the form used here. ISBN 978-1881883180.
  • J. R. Wertz, "Mission Geometry: Orbit and Constellation Design and Management", Microcosm Press and Springer, 2001, chapter 5. The nadir angle, Earth central angle, and slant range closed forms. ISBN 978-1881883074.
  • P. Leopardi, "A partition of the unit sphere into regions of equal area and small diameter", Electronic Transactions on Numerical Analysis 25, 2006, pages 309 to 327. https://etna.math.kent.edu/vol.25.2006/pp309-327.dir/pp309-327.pdf
  • J. P. Snyder, "Map Projections: A Working Manual", United States Geological Survey Professional Paper 1395, 1987, pages 16 and 249. Authalic latitude, and the Mollweide projection used by the figures and the visualisation. DOI 10.3133/pp1395
  • B. R. Bowring, "Transformation from spatial to geographical coordinates", Survey Review 23(181), 1976, pages 323 to 327. DOI 10.1179/sre.1976.23.181.323
  • National Imagery and Mapping Agency, "Department of Defense World Geodetic System 1984", NIMA Technical Report 8350.2, third edition, amendment 1, 2000. The ellipsoid, gravitational parameter, J2, rotation rate, surface area, and authalic radius. https://nsgreg.nga.mil/doc/view?i=4085
  • K. M. Gorski, E. Hivon, A. J. Banday, B. D. Wandelt, F. K. Hansen, M. Reinecke and M. Bartelmann, "HEALPix: a framework for high-resolution discretization and fast analysis of data distributed on the sphere", The Astrophysical Journal 622(2), 2005, pages 759 to 771. Considered for the grid and rejected; see the design notes. DOI 10.1086/427976
  • A. Gonzalez, "Measurement of areas on a sphere using Fibonacci and latitude-longitude lattices", Mathematical Geosciences 42, 2010, pages 49 to 64. Also considered and rejected. DOI 10.1007/s11004-009-9257-x

Dependencies:

  • numpy (BSD 3-Clause). Array arithmetic throughout: the propagation, the visibility kernel, and the gap extraction.
  • scipy (BSD 3-Clause). scipy.optimize.brentq for the repeating ground track root solve, and scipy.integrate.quad in the tests for the independent check of the cell areas.
  • matplotlib (matplotlib license, a BSD-style permissive license). The figures in the analysis layer, built through the object-oriented interface so that no interactive backend is selected.
  • pytest (MIT), pytest-cov (MIT), ruff (MIT), and mypy (MIT). Development only: test running, coverage measurement, linting, and type checking.
  • TypeScript (Apache-2.0). Build time only, and only for viz/. The published page loads no library at runtime.

License

Released under the MIT license. See LICENSE.

About

Walker constellation generation with ground grid coverage and revisit time analysis.

Topics

Resources

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages