PyCG-DTN
Build DTN contact graphs for deep-space networks from real ephemerides.
Give it a set of bodies and a number of days. It works out which SPICE kernels it needs, downloads them from NASA/NAIF, computes when every pair of nodes can actually see each other, and writes a contact plan.
from pycg_dtn import ContactGraph
cg = ContactGraph()
cg.AddCelestial("Earth")
cg.AddCelestial("Mars")
cg.AddSatellite("MRO-LIKE", "Mars", altitude_km=400, inclination_deg=93.0)
plan = cg.GenerateContactGraph(days=780)
plan.Write("out/")
out/contactGraph.csv ION contact plan
out/contactGraph.json full plan with metadata
out/summary.json per-link statistics
Install
pip install pycg-dtn
Requires Python 3.10+.
Full documentation: pycg-dtn.readthedocs.io
What it computes
A contact is an interval during which two nodes can exchange data. Two things take a deep-space link away even when both endpoints are nominally in view:
Occultation — a third body sits in the line of sight. Mars hides Phobos; Jupiter hides Io.
Solar conjunction — the signal path passes close to the Sun, whose corona scatters and delays the signal badly enough that operators stand the link down. The threshold is the Sun–Earth–probe angle; below about 3° the link is treated as unusable, following DSN practice (DSN handbook 810-005).
Contact time is the analysed span minus the union of everything that blocks it. Each surviving interval is then cut into sub-contacts wherever the achievable data rate has drifted more than 10%, so no single contact misrepresents how much data fits through it.
Rates come from a Friis free-space path loss → SNR → Shannon capacity chain, with defaults modelling an X-band spacecraft high-gain dish talking to a DSN 70 m antenna (74.18 dBi, Rodemich 1989).
Bodies
AddCelestial accepts anything NAIF names, case-insensitively — or a NAIF integer
ID code.
cg.AddCelestial("Titan")
cg.AddCelestial("Europa", eid="ipn:5.2") # custom ION endpoint identifier
cg.AddCelestial("401") # Phobos, by ID
An unrecognised name raises UnknownCelestialBodyError.
Satellites
Satellites here are defined by you, as a classical Keplerian orbit about a central body:
cg.AddSatellite("MRO-LIKE", "Mars", altitude_km=400,
eccentricity=0.001, inclination_deg=93.0)
Size the orbit with either altitude_km (above the central body's equatorial
radius) or semi_major_axis_km, and give exactly one of them. The rest —
eccentricity, inclination_deg, raan_deg, arg_periapsis_deg,
mean_anomaly_deg, epoch_utc — default to a circular orbit at the
start epoch.
A satellite is a node like any other. It is treated as a point target, so it never occults anything, but things certainly occult it, a low orbiter is hidden by its own planet once per revolution, which is what dominates its contact plan:
MERCURY MARS inter in contact 95.8% longest outage 4.27 d
MERCURY MRO-LIKE inter in contact 71.5% longest outage 4.27 d
MARS MRO-LIKE intra in contact 100.0% longest outage 0.00 d
Kernels
A kernel is NAIF's term for a SPICE data file. Only the ones your bodies need are downloaded, which matters — the Jovian satellite ephemeris alone is over a gigabyte, and an Earth–Mars scenario should never pay for it.
Check the cost before committing:
pycg kernels --bodies Earth Mars Phobos
kernel directory: /home/you/kernels
4 kernels, about 96 MB total
[need] naif0012.tls 0.0 MB leap seconds, for UTC <-> ET conversion
[need] pck00011.tpc 0.1 MB body radii and IAU body-fixed orientation
[need] de440s.bsp 31.2 MB planetary ephemeris DE440 (short), 1849-2150
[need] mar099s.bsp 64.5 MB Phobos and Deimos
All files come from NAIF's public generic-kernel archive.
Configuring the link budget
Every radio parameter has a getter and a setter. Setters chain and validate.
lb = cg.GetLinkBudget()
lb.SetFrequency(32.0e9) # Ka-band instead of X-band
lb.SetRxGain(79.0) # a larger ground antenna
lb.SetBandwidth(5.0e6)
lb.SetMinRate(1000.0) # drop contacts below 1 kbps
print(lb.GetWavelength(), lb.GetNoisePower())
| Parameter | Getter / Setter | Default |
|---|---|---|
| Transmit power, W | GetTxPower / SetTxPower |
100 |
| Carrier, Hz | GetFrequency / SetFrequency |
8.42e9 (X-band) |
| Bandwidth, Hz | GetBandwidth / SetBandwidth |
1.0e6 |
| Transmit gain, dBi | GetTxGain / SetTxGain |
48 |
| Receive gain, dBi | GetRxGain / SetRxGain |
74.18 |
| Noise PSD, dBm/Hz | GetNoisePsd / SetNoisePsd |
−174 |
| Rate floor, bits/s | GetMinRate / SetMinRate |
1 |
Configuring the geometry search
Same pattern on cg.GetGeometry():
geo = cg.GetGeometry()
geo.SetSepExclusion(2.0) # tighter solar exclusion, degrees
geo.SetOccultStep(300.0) # finer occultation search, seconds
geo.SetRateTolerance(0.05) # split contacts on 5% rate drift
Choosing a span
Contact plans repeat on the synodic period — the time for two bodies to
return to the same relative arrangement, which for planets with orbital periods
Pa and Pb is 1 / |1/Pa − 1/Pb|.
Reading the results
plan = cg.GenerateContactGraph(days=780)
len(plan) # number of contacts
plan.ForLink("EARTH", "MARS") # contacts on one link, time-ordered
for s in plan.LongestOutages(5):
print(s.a, s.b, s.t_maxgap_days)
The CSV is the contact plan format ingested by ION,
NASA JPL's reference DTN implementation. Each contact becomes four rows — a
contact and a range line in each direction.
Command line
pycg kernels --bodies Earth Mars Phobos
pycg fetch --bodies Earth Mars Phobos
pycg build --bodies Earth Mars --days 780 --out out/
pycg build --bodies Earth Mars --days 780 --frequency 32e9 --rx-gain 79
Satellites take a NAME,CENTRAL,key=value,... spec and the flag repeats:
pycg build --bodies Earth Mars --days 780 \
--satellite RELAY-1,Mars,alt=400,inc=93 \
--satellite RELAY-2,Mars,sma=20000,ecc=0.3
Keys are alt or sma (give one), ecc, inc, raan, argp, ma, eid.
The central body does not have to be a node — it still occults its own orbiter.
Citing
If you use PyCG-DTN in published work, please cite it.
@software{satalight_pycg-dtn,
author = {Lagwankar, Ishaan and Klevering, Griffin},
title = {{PyCG-DTN}: {DTN} contact graphs for deep-space networks
from {SPICE} ephemerides},
version = {1.2.0},
year = {2026},
license = {GPL-3.0-or-later},
url = {https://github.com/shaanzie/pycg-dtn}
}
License
GNU General Public License v3.0 or later. See LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pycg_dtn-1.2.0.tar.gz.
File metadata
- Download URL: pycg_dtn-1.2.0.tar.gz
- Upload date:
- Size: 65.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ad98fa31602e15e7690b5588253837d207ea0bb8264d9ff44c56a7db8723daae
|
|
| MD5 |
8e50923893f659da40bef9aac454f2bc
|
|
| BLAKE2b-256 |
2d00c0ff03261ff60a507efe69e0d2da5bbf86ff1d06e24e26dc3c277a452c41
|
File details
Details for the file pycg_dtn-1.2.0-py3-none-any.whl.
File metadata
- Download URL: pycg_dtn-1.2.0-py3-none-any.whl
- Upload date:
- Size: 50.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/7.0.0 CPython/3.11.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
1aaafa49f108b72a47f6d2726ab8f4e2a63e38c569edd30d97c622243f9b93aa
|
|
| MD5 |
6400d89bbfe8a76082f4153d5754aa38
|
|
| BLAKE2b-256 |
a98974ae8246c778aa305aabb7b70af2da0094432d2f1ebde375ef7ffcaf7aa6
|