diff --git a/documentation/earth-satellites.rst b/documentation/earth-satellites.rst
index f6fd98e95..8e5f1df1c 100644
--- a/documentation/earth-satellites.rst
+++ b/documentation/earth-satellites.rst
@@ -8,7 +8,7 @@ by downloading the satellite’s standard SGP4 orbital elements.
Orbital elements are published by organizations like `CelesTrak`_.
Beware of these limitations:
-.. _Celestrak: https://celestrak.org/
+.. _CelesTrak: https://celestrak.org/
1. Don’t expect perfect agreement between
any two pieces of software that are trying to predict satellite positions.
@@ -56,28 +56,26 @@ Beware of these limitations:
The TLE format and its rivals
=============================
-Where most folks download satellite element sets at Celestrak:
+Most folks download satellite element sets from CelesTrak:
https://celestrak.org/NORAD/elements/index.php
-Celestrak supports several data formats.
-The original Two-Line Element ‘TLE’ format —
-the URL will specify ``FORMAT=tle`` —
+CelesTrak supports several data formats.
+The original Two-Line Element ‘TLE’ format
describes a satellite orbit using two lines of dense ASCII text.
Whitespace is significant
because every character needs to be aligned in exactly the right column.
-Here, for example, are elements for the ISS::
+Here, for example, are elements for the International Space Station (ISS)::
ISS (ZARYA)
1 25544U 98067A 24127.82853009 .00015698 00000+0 27310-3 0 9995
2 25544 51.6393 160.4574 0003580 140.6673 205.7250 15.50957674452123
-Here are the same elements as JSON —
-in this case,
-with ``FORMAT=json-pretty`` specified in the Celestrak URL
-because the ‘pretty’ newlines and indentation make it easy to read;
-use ``FORMAT=json`` if you don’t need the indentation
-and want data that’s more compact::
+But CelesTrak also supports modern formats.
+Here is the same element set in JSON format —
+specifically, CelesTrak’s ``FORMAT=json-pretty`` format,
+which adds indentation to make it easy to read;
+use ``FORMAT=json`` if you don’t need the extra spaces and newlines::
[{
"OBJECT_NAME": "ISS (ZARYA)",
@@ -99,7 +97,10 @@ and want data that’s more compact::
"MEAN_MOTION_DDOT": 0
}]
-And here are the same elements with ``FORMAT=CSV``::
+And here are the same elements with ``FORMAT=CSV``
+(this is only two lines of text,
+but your browser will probably wrap them
+to fit your screen)::
OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
ISS (ZARYA),1998-067A,2024-05-06T19:53:04.999776,15.50957674,.000358,51.6393,160.4574,140.6673,205.7250,0,U,25544,999,45212,.2731E-3,.15698E-3,0
@@ -113,184 +114,202 @@ Note these differences:
with four digits past the decimal point —
both JSON and CSV will be able to carry greater precision in the future.
The old TLE format, by contrast,
- can’t add more decimal places
+ will never be able to add more decimal places
because each element has a fixed-width number of characters.
* The TLE format has run out of simple integer catalog numbers,
- because the catalog number
+ because it limits the catalog number
(in the above example, ``25544``)
- is limited to 5 digits.
+ to 5 digits.
The JSON and CSV formats, by contrast,
don’t place a limit on the size of the catalog number.
* The JSON and CSV formats are self-documenting.
You can tell, even as a first-time reader,
which element is the inclination.
- But the TLE lines provide no hint about which element is which.
+ The old TLE lines provide no hint about which element is which.
* The TLE and CSV formats are both very efficient,
and describe an orbit using about 150 characters.
- Yes, the CSV file has that big 225-character header at the top,
- but it only appears once,
+ Yes, the CSV file has that big 225-character header at the top;
+ but the header line only appears once,
no matter how many satellites are listed after it.
By contrast,
the bulky JSON format requires more than 400 characters per satellite
- because the element names need to be repeated over again
- for every satellite.
+ because the element names need to be repeated again every time.
-There are more obscure formats in use at Celestrak,
-including ‘key-value notation (KVN)’ and an unfortunate XML format,
+There are more obscure formats in use at CelesTrak,
+including a ‘key-value notation (KVN)’ and an unfortunate XML format,
but here we will focus on the mainstream formats listed above.
-Downloading a TLE file
-----------------------
+Downloading satellite elements
+------------------------------
+
+Whether you choose one of CelesTrak’s
+`pre-packaged satellite lists
+`_
+like ‘Space Stations’ or ‘CubeSats’,
+or perform a
+`query for a particular satellite
+`_,
+there are three issues to beware of:
+
+* Be sure to save the data to a file the first time your script runs,
+ so that subsequent runs won’t need to download the same data again.
+ This is crucial to reducing the load on the CelesTrak servers
+ and helping them continue to provide CelesTrak as a free service.
+
+* Satellite elements gradually go out of date.
+ Once a file is a few days old,
+ you will probably want to download the file again.
+
+* The CelesTrak data URLs all use the exact same filename.
+ So if you download the ‘Space Stations’
+ whose URL looks like ``gp.php?GROUP=stations``,
+ and then the ‘CubeSats’ with ``gp.php?GROUP=cubesat``,
+ then Skyfield will save them to the same filename ``gp.php``
+ and the CubeSats will wind up overwriting the Space Stations.
+ To avoid this, use the ``filename=`` optional argument
+ to create a separate local file for each remote URL.
+
+Here’s a useful pattern for downloading element sets with Skyfield:
+
+.. include:: ../examples/satellite_download.py
+ :literal:
+
+The next section will illustrate how to load satellites
+once you have downloaded the file.
+
+If your project is serious enough
+that you will need to be able to double-check and replicate old results later,
+then don’t follow this example —
+every time the file gets too old,
+this code will overwrite the file with new data.
+Instead, you will probably want to put the date in the filename,
+and archive each file along with your project’s code.
+
+Loading satellite elements
+--------------------------
+
+Once you have downloaded a file of elements,
+use one of these patterns to load them into Skyfield.
+For the traditional TLE format:
-You can find satellite element sets at the
-`NORAD Two-Line Element Sets `_
-page of the Celestrak web site.
-
-Beware that the two-line element (TLE) format is very rigid.
-The meaning of each character
-is based on its exact offset from the beginning of the line.
-You must download and use the element set’s text
-without making any change to its whitespace.
-
-Skyfield loader objects offer a :meth:`~skyfield.iokit.Loader.tle_file()`
-method that can download and cache a file full of satellite elements
-from a site like Celestrak.
-A popular observing target for satellite observers
-is the International Space Station,
-which is listed in Celestrak’s ``stations.txt`` file:
-
-.. testsetup::
+.. testcode::
- stations_txt_bytes = b"""\
- ISS (ZARYA) \n\
- 1 25544U 98067A 14020.93268519 .00009878 00000-0 18200-3 0 5082
- 2 25544 51.6498 109.4756 0003572 55.9686 274.8005 15.49815350868473
- """ * 60
- open('stations.txt', 'wb').write(stations_txt_bytes)
+ from skyfield.api import load
+ from skyfield.iokit import parse_tle_file
-.. testcode::
+ ts = load.timescale()
- from skyfield.api import load, wgs84
+ with load.open('stations.tle') as f:
+ satellites = list(parse_tle_file(f, ts))
- stations_url = 'http://celestrak.org/NORAD/elements/stations.txt'
- satellites = load.tle_file(stations_url)
print('Loaded', len(satellites), 'satellites')
.. testoutput::
- Loaded 60 satellites
+ Loaded 27 satellites
-Indexing satellites by name or number
--------------------------------------
-
-If you want to operate on every satellite
-in the list that you have loaded from a file,
-you can use Python’s ``for`` loop.
-But if you instead want to select individual satellites by name or number,
-try building a lookup dictionary
-using Python’s dictionary comprehension syntax:
+For the verbose but easy-to-read JSON format:
.. testcode::
- by_name = {sat.name: sat for sat in satellites}
- satellite = by_name['ISS (ZARYA)']
- print(satellite)
+ import json
+ from skyfield.api import EarthSatellite, load
+
+ with load.open('stations.json') as f:
+ data = json.load(f)
+
+ ts = load.timescale()
+ sats = [EarthSatellite.from_omm(ts, fields) for fields in data]
+ print('Loaded', len(sats), 'satellites')
.. testoutput::
- ISS (ZARYA) catalog #25544 epoch 2014-01-20 22:23:04 UTC
+ Loaded 27 satellites
+
+For the more compact CSV format:
.. testcode::
- by_number = {sat.model.satnum: sat for sat in satellites}
- satellite = by_number[25544]
- print(satellite)
+ import csv
+ from skyfield.api import EarthSatellite, load
-.. testoutput::
+ with load.open('stations.csv', mode='r') as f:
+ data = list(csv.DictReader(f))
- ISS (ZARYA) catalog #25544 epoch 2014-01-20 22:23:04 UTC
+ ts = load.timescale()
+ sats = [EarthSatellite.from_omm(ts, fields) for fields in data]
+ print('Loaded', len(sats), 'satellites')
-Performing a TLE query
-----------------------
+.. testoutput::
-In addition to offering traditional text files
-like ``stations.txt`` and ``active.txt``,
-Celestrak supports queries that return TLE elements.
+ Loaded 27 satellites
-But be careful!
+In each case,
+you are asked to provide a ``ts`` timescale.
+Why?
+Because Skyfield needs the timescale’s knowledge of leap seconds
+to turn each satellite’s epoch date
+into an ``.epoch`` :class:`~skyfield.timelib.Time` object.
-Because every query to Celestrak requests the same filename ``tle.php``
-Skyfield will by default only download the first result.
-Your second, third, and all subsequent attempts to query Celestrak
-will simply return the contents
-of the ``tle.php`` file that’s already on disk —
-giving you the results of your first query over and over again.
+Loading satellite data from a string
+------------------------------------
-Here are two easy remedies:
+If your program has already loaded TLE, JSON, or CSV data into memory
+and doesn’t need to read it over again from a file,
+you can make the string behave like a file
+by wrapping it in a Python I/O object::
-1. Specify the argument ``reload=True``,
- which asks Skyfield to always download new results
- even if there is already a file on disk.
- Every query will overwrite the file with new data.
+ # For TLE and JSON:
-2. Or, specify a ``filename=`` argument
- so that each query’s result
- is saved to a file specific to that query.
- Each query result will be saved to disk with its own filename.
+ from io import BytesIO
+ f = BytesIO(byte_string)
-Here’s an example of the second approach —
-code that requests one specific satellite,
-saving the result to a file specific to the query:
+ # For CSV:
-.. testcode::
+ from io import StringIO
+ f = StringIO(text_string)
- n = 25544
- url = 'https://celestrak.org/satcat/tle.php?CATNR={}'.format(n)
- filename = 'tle-CATNR-{}.txt'.format(n)
- satellites = load.tle_file(url, filename=filename)
- print(satellites)
+You can then use the resulting file object ``f``
+with the example code in the previous section.
-.. testoutput::
+Indexing satellites by name or number
+-------------------------------------
+
+If you want to operate on every satellite
+in the list that you have loaded from a file,
+you can use Python’s ``for`` loop.
+But if you instead want to select individual satellites by name or number,
+try building a lookup dictionary
+using Python’s dictionary comprehension syntax:
- []
+.. testcode::
-The above code will download a new result
-each time it’s asked for a satellite that it hasn’t yet fetched.
-But note that when asked again for the same satellite,
-it will simply reload the existing file from disk
-unless ``reload=True`` is specified.
+ by_name = {sat.name: sat for sat in satellites}
+ satellite = by_name['ISS (ZARYA)']
+ print(satellite)
-Loading a TLE file from a string
---------------------------------
+.. testoutput::
-If your program has already loaded the text of a TLE file,
-and you don’t need Skyfield to handle the download for you,
-then you can turn it into a list of Skyfield satellites
-with the :func:`~skyfield.iokit.parse_tle_file()` function.
-The function is a Python generator,
-so we use Python’s ``list()`` constructor
-to load all the satellites:
+ ISS (ZARYA) catalog #25544 epoch 2024-05-09 08:48:20 UTC
.. testcode::
- from io import BytesIO
- from skyfield.iokit import parse_tle_file
+ by_number = {sat.model.satnum: sat for sat in satellites}
+ satellite = by_number[25544]
+ print(satellite)
- f = BytesIO(stations_txt_bytes)
- satellites = list(parse_tle_file(f))
+.. testoutput::
-If it’s simpler in your case,
-you can instead pass :func:`~skyfield.iokit.parse_tle_file()`
-a simple list of lines.
+ ISS (ZARYA) catalog #25544 epoch 2024-05-09 08:48:20 UTC
Loading a single TLE set from strings
-------------------------------------
If your program already has the two lines of TLE data for a satellite
-and doesn’t need Skyfield to download and parse a Celestrak file,
+and doesn’t need Skyfield to download and parse a CelesTrak file,
you can instantiate an :class:`~skyfield.sgp4lib.EarthSatellite` directly.
.. testcode::
@@ -401,6 +420,8 @@ over the span of a single day:
.. testcode::
+ from skyfield.api import wgs84
+
bluffton = wgs84.latlon(+40.8939, -83.8917)
t0 = ts.utc(2014, 1, 23)
t1 = ts.utc(2014, 1, 24)
@@ -702,7 +723,6 @@ and ``False`` otherwise.
.. testcode::
eph = load('de421.bsp')
- satellite = by_name['ISS (ZARYA)']
two_hours = ts.utc(2014, 1, 20, 0, range(0, 120, 20))
sunlit = satellite.at(two_hours).is_sunlit(eph)
@@ -749,7 +769,6 @@ through the :meth:`~skyfield.positionlib.ICRF.is_behind_earth()` method.
eph = load('de421.bsp')
earth, venus = eph['earth'], eph['venus']
- satellite = by_name['ISS (ZARYA)']
two_hours = ts.utc(2014, 1, 20, 0, range(0, 120, 20))
p = (earth + satellite).at(two_hours).observe(venus).apparent()
diff --git a/documentation/stations.csv b/documentation/stations.csv
new file mode 100644
index 000000000..3e8d0c608
--- /dev/null
+++ b/documentation/stations.csv
@@ -0,0 +1,28 @@
+OBJECT_NAME,OBJECT_ID,EPOCH,MEAN_MOTION,ECCENTRICITY,INCLINATION,RA_OF_ASC_NODE,ARG_OF_PERICENTER,MEAN_ANOMALY,EPHEMERIS_TYPE,CLASSIFICATION_TYPE,NORAD_CAT_ID,ELEMENT_SET_NO,REV_AT_EPOCH,BSTAR,MEAN_MOTION_DOT,MEAN_MOTION_DDOT
+ISS (ZARYA),1998-067A,2024-05-09T08:48:19.938816,15.51043222,.0003466,51.6381,147.8590,150.4670,338.5502,0,U,25544,999,45252,.27777E-3,.16024E-3,0
+CSS (TIANHE),2021-035A,2024-05-09T00:08:21.259968,15.63450567,.0003879,41.4681,322.4705,310.0852,49.9647,0,U,48274,999,17296,.47178E-3,.44782E-3,0
+ISS (NAUKA),2021-066A,2024-05-08T19:52:52.426848,15.51025615,.0003349,51.6355,150.5366,149.2285,210.8902,0,U,49044,999,15817,.28217E-3,.16275E-3,0
+FREGAT DEB,2011-037PF,2024-05-09T01:52:27.087456,12.14183725,.0970755,51.6682,100.0873,38.9529,327.7334,0,U,49271,999,13044,.53806E-1,.22918E-3,0
+CSS (WENTIAN),2022-085A,2024-05-08T19:32:24.132480,15.63430646,.0003878,41.4681,323.6416,309.1053,50.9442,0,U,53239,999,10185,.44581E-3,.4225E-3,0
+CSS (MENGTIAN),2022-143A,2024-05-08T19:32:24.132480,15.63430646,.0003878,41.4681,323.6416,309.1053,50.9442,0,U,54216,999,8633,.44581E-3,.4225E-3,0
+ISS DEB [SPX-28 IPA FSE],1998-067VP,2024-05-08T15:57:19.131264,16.01941673,.0004565,51.6185,113.9060,57.1524,302.9926,0,U,57212,999,4985,.725E-3,.424541E-2,.90482E-4
+ISS DEB,1998-067WC,2024-05-08T12:13:05.124576,15.79259615,.0006542,51.6266,136.3716,42.3437,317.8068,0,U,58229,999,2938,.87658E-3,.163586E-2,0
+PROGRESS-MS 25,2023-184A,2024-05-08T19:52:52.426848,15.51025615,.0003349,51.6355,150.5366,149.2285,210.8902,0,U,58460,999,2447,.28217E-3,.16275E-3,0
+BEAK,1998-067WD,2024-05-09T03:41:34.279008,15.76580994,.0005919,51.6309,136.9549,51.3320,308.8208,0,U,58612,999,2238,.95703E-3,.158289E-2,0
+TIANZHOU-7,2024-013A,2024-05-08T19:32:24.132480,15.63430646,.0003878,41.4681,323.6416,309.1053,50.9442,0,U,58811,999,17242,.44581E-3,.4225E-3,0
+CYGNUS NG-20,2024-021A,2024-05-08T19:52:52.426848,15.51025615,.0003349,51.6355,150.5366,149.2285,210.8902,0,U,58898,999,1456,.28217E-3,.16275E-3,0
+PROGRESS-MS 26,2024-029A,2024-05-08T19:52:52.426848,15.51025615,.0003349,51.6355,150.5366,149.2285,210.8902,0,U,58961,999,1262,.28217E-3,.16275E-3,0
+CREW DRAGON 8,2024-042A,2024-05-08T19:52:52.426848,15.51025615,.0003349,51.6355,150.5366,149.2285,210.8902,0,U,59097,999,983,.28217E-3,.16275E-3,0
+SOYUZ-MS 25,2024-055A,2024-05-08T19:52:52.426848,15.51025615,.0003349,51.6355,150.5366,149.2285,210.8902,0,U,59294,999,637,.28217E-3,.16275E-3,0
+MICROORBITER-1,1998-067WF,2024-05-09T05:26:00.617856,15.56086321,.000293,51.6338,147.9454,101.0748,259.0573,0,U,59483,999,439,.11224E-2,.80169E-3,0
+CURTIS,1998-067WG,2024-05-08T23:26:48.164640,15.55130715,.000163,51.6338,149.2796,101.4702,258.6472,0,U,59507,999,436,.96605E-3,.66412E-3,0
+KASHIWA,1998-067WH,2024-05-08T10:52:35.969376,15.56340642,.0002884,51.6337,151.7802,96.6079,263.5241,0,U,59508,999,428,.12306E-2,.88878E-3,0
+1998-067WJ,1998-067WJ,2024-05-08T23:50:31.532064,15.54210810,.0004934,51.6352,149.3927,75.1638,284.9899,0,U,59559,999,323,.95997E-3,.63795E-3,0
+1998-067WK,1998-067WK,2024-05-08T23:33:34.532352,15.55918508,.0007105,51.6349,149.3135,85.4197,274.7606,0,U,59560,999,315,.14945E-2,.106686E-2,0
+1998-067WL,1998-067WL,2024-05-08T08:42:20.237472,15.52592728,.000278,51.6357,152.6792,98.5651,261.5654,0,U,59561,999,310,.51054E-3,.31623E-3,0
+BURSTCUBE,1998-067WM,2024-05-08T23:52:55.364736,15.54147491,.0002632,51.6351,149.4024,117.5195,242.6063,0,U,59562,999,321,.95233E-3,.63112E-3,0
+1998-067WN,1998-067WN,2024-05-08T23:58:11.728704,15.53686017,.0003114,51.6355,149.4273,129.0540,231.0728,0,U,59563,999,320,.76265E-3,.49516E-3,0
+SHENZHOU-18 (SZ-18),2024-078A,2024-05-08T13:24:27.582912,15.63411877,.0003902,41.4681,325.2027,308.5199,51.5291,0,U,59591,999,17289,.4696E-3,.44503E-3,0
+1998-067WP,1998-067WP,2024-05-08T12:56:24.246528,15.54734172,.0005166,51.6350,151.6080,73.0758,287.0799,0,U,59596,999,317,.11065E-2,.75147E-3,0
+1998-067WQ,1998-067WQ,2024-05-08T14:28:37.834752,15.54780191,.0005199,51.6358,151.2866,73.4697,286.6865,0,U,59597,999,315,.11126E-2,.75697E-3,0
+SZ-17 MODULE,2023-164C,2024-05-08T19:41:15.555552,15.62250315,.0005736,41.4784,323.6878,330.8958,29.1561,0,U,59624,999,137,.39322E-3,.35514E-3,0
diff --git a/documentation/stations.json b/documentation/stations.json
new file mode 100644
index 000000000..c5c0280ed
--- /dev/null
+++ b/documentation/stations.json
@@ -0,0 +1 @@
+[{"OBJECT_NAME":"ISS (ZARYA)","OBJECT_ID":"1998-067A","EPOCH":"2024-05-09T08:48:19.938816","MEAN_MOTION":15.51043222,"ECCENTRICITY":0.0003466,"INCLINATION":51.6381,"RA_OF_ASC_NODE":147.859,"ARG_OF_PERICENTER":150.467,"MEAN_ANOMALY":338.5502,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":25544,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":45252,"BSTAR":0.00027777,"MEAN_MOTION_DOT":0.00016024,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"CSS (TIANHE)","OBJECT_ID":"2021-035A","EPOCH":"2024-05-09T00:08:21.259968","MEAN_MOTION":15.63450567,"ECCENTRICITY":0.0003879,"INCLINATION":41.4681,"RA_OF_ASC_NODE":322.4705,"ARG_OF_PERICENTER":310.0852,"MEAN_ANOMALY":49.9647,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":48274,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":17296,"BSTAR":0.00047178,"MEAN_MOTION_DOT":0.00044782,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"ISS (NAUKA)","OBJECT_ID":"2021-066A","EPOCH":"2024-05-08T19:52:52.426848","MEAN_MOTION":15.51025615,"ECCENTRICITY":0.0003349,"INCLINATION":51.6355,"RA_OF_ASC_NODE":150.5366,"ARG_OF_PERICENTER":149.2285,"MEAN_ANOMALY":210.8902,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":49044,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":15817,"BSTAR":0.00028217,"MEAN_MOTION_DOT":0.00016275,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"FREGAT DEB","OBJECT_ID":"2011-037PF","EPOCH":"2024-05-09T01:52:27.087456","MEAN_MOTION":12.14183725,"ECCENTRICITY":0.0970755,"INCLINATION":51.6682,"RA_OF_ASC_NODE":100.0873,"ARG_OF_PERICENTER":38.9529,"MEAN_ANOMALY":327.7334,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":49271,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":13044,"BSTAR":0.053806,"MEAN_MOTION_DOT":0.00022918,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"CSS (WENTIAN)","OBJECT_ID":"2022-085A","EPOCH":"2024-05-08T19:32:24.132480","MEAN_MOTION":15.63430646,"ECCENTRICITY":0.0003878,"INCLINATION":41.4681,"RA_OF_ASC_NODE":323.6416,"ARG_OF_PERICENTER":309.1053,"MEAN_ANOMALY":50.9442,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":53239,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":10185,"BSTAR":0.00044581,"MEAN_MOTION_DOT":0.0004225,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"CSS (MENGTIAN)","OBJECT_ID":"2022-143A","EPOCH":"2024-05-08T19:32:24.132480","MEAN_MOTION":15.63430646,"ECCENTRICITY":0.0003878,"INCLINATION":41.4681,"RA_OF_ASC_NODE":323.6416,"ARG_OF_PERICENTER":309.1053,"MEAN_ANOMALY":50.9442,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":54216,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":8633,"BSTAR":0.00044581,"MEAN_MOTION_DOT":0.0004225,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"ISS DEB [SPX-28 IPA FSE]","OBJECT_ID":"1998-067VP","EPOCH":"2024-05-08T15:57:19.131264","MEAN_MOTION":16.01941673,"ECCENTRICITY":0.0004565,"INCLINATION":51.6185,"RA_OF_ASC_NODE":113.906,"ARG_OF_PERICENTER":57.1524,"MEAN_ANOMALY":302.9926,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":57212,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":4985,"BSTAR":0.000725,"MEAN_MOTION_DOT":0.00424541,"MEAN_MOTION_DDOT":9.0482e-5},{"OBJECT_NAME":"ISS DEB","OBJECT_ID":"1998-067WC","EPOCH":"2024-05-08T12:13:05.124576","MEAN_MOTION":15.79259615,"ECCENTRICITY":0.0006542,"INCLINATION":51.6266,"RA_OF_ASC_NODE":136.3716,"ARG_OF_PERICENTER":42.3437,"MEAN_ANOMALY":317.8068,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":58229,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":2938,"BSTAR":0.00087658,"MEAN_MOTION_DOT":0.00163586,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"PROGRESS-MS 25","OBJECT_ID":"2023-184A","EPOCH":"2024-05-08T19:52:52.426848","MEAN_MOTION":15.51025615,"ECCENTRICITY":0.0003349,"INCLINATION":51.6355,"RA_OF_ASC_NODE":150.5366,"ARG_OF_PERICENTER":149.2285,"MEAN_ANOMALY":210.8902,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":58460,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":2447,"BSTAR":0.00028217,"MEAN_MOTION_DOT":0.00016275,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"BEAK","OBJECT_ID":"1998-067WD","EPOCH":"2024-05-09T03:41:34.279008","MEAN_MOTION":15.76580994,"ECCENTRICITY":0.0005919,"INCLINATION":51.6309,"RA_OF_ASC_NODE":136.9549,"ARG_OF_PERICENTER":51.332,"MEAN_ANOMALY":308.8208,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":58612,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":2238,"BSTAR":0.00095703,"MEAN_MOTION_DOT":0.00158289,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"TIANZHOU-7","OBJECT_ID":"2024-013A","EPOCH":"2024-05-08T19:32:24.132480","MEAN_MOTION":15.63430646,"ECCENTRICITY":0.0003878,"INCLINATION":41.4681,"RA_OF_ASC_NODE":323.6416,"ARG_OF_PERICENTER":309.1053,"MEAN_ANOMALY":50.9442,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":58811,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":17242,"BSTAR":0.00044581,"MEAN_MOTION_DOT":0.0004225,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"CYGNUS NG-20","OBJECT_ID":"2024-021A","EPOCH":"2024-05-08T19:52:52.426848","MEAN_MOTION":15.51025615,"ECCENTRICITY":0.0003349,"INCLINATION":51.6355,"RA_OF_ASC_NODE":150.5366,"ARG_OF_PERICENTER":149.2285,"MEAN_ANOMALY":210.8902,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":58898,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":1456,"BSTAR":0.00028217,"MEAN_MOTION_DOT":0.00016275,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"PROGRESS-MS 26","OBJECT_ID":"2024-029A","EPOCH":"2024-05-08T19:52:52.426848","MEAN_MOTION":15.51025615,"ECCENTRICITY":0.0003349,"INCLINATION":51.6355,"RA_OF_ASC_NODE":150.5366,"ARG_OF_PERICENTER":149.2285,"MEAN_ANOMALY":210.8902,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":58961,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":1262,"BSTAR":0.00028217,"MEAN_MOTION_DOT":0.00016275,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"CREW DRAGON 8","OBJECT_ID":"2024-042A","EPOCH":"2024-05-08T19:52:52.426848","MEAN_MOTION":15.51025615,"ECCENTRICITY":0.0003349,"INCLINATION":51.6355,"RA_OF_ASC_NODE":150.5366,"ARG_OF_PERICENTER":149.2285,"MEAN_ANOMALY":210.8902,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59097,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":983,"BSTAR":0.00028217,"MEAN_MOTION_DOT":0.00016275,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"SOYUZ-MS 25","OBJECT_ID":"2024-055A","EPOCH":"2024-05-08T19:52:52.426848","MEAN_MOTION":15.51025615,"ECCENTRICITY":0.0003349,"INCLINATION":51.6355,"RA_OF_ASC_NODE":150.5366,"ARG_OF_PERICENTER":149.2285,"MEAN_ANOMALY":210.8902,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59294,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":637,"BSTAR":0.00028217,"MEAN_MOTION_DOT":0.00016275,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"MICROORBITER-1","OBJECT_ID":"1998-067WF","EPOCH":"2024-05-09T05:26:00.617856","MEAN_MOTION":15.56086321,"ECCENTRICITY":0.000293,"INCLINATION":51.6338,"RA_OF_ASC_NODE":147.9454,"ARG_OF_PERICENTER":101.0748,"MEAN_ANOMALY":259.0573,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59483,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":439,"BSTAR":0.0011224,"MEAN_MOTION_DOT":0.00080169,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"CURTIS","OBJECT_ID":"1998-067WG","EPOCH":"2024-05-08T23:26:48.164640","MEAN_MOTION":15.55130715,"ECCENTRICITY":0.000163,"INCLINATION":51.6338,"RA_OF_ASC_NODE":149.2796,"ARG_OF_PERICENTER":101.4702,"MEAN_ANOMALY":258.6472,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59507,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":436,"BSTAR":0.00096605,"MEAN_MOTION_DOT":0.00066412,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"KASHIWA","OBJECT_ID":"1998-067WH","EPOCH":"2024-05-08T10:52:35.969376","MEAN_MOTION":15.56340642,"ECCENTRICITY":0.0002884,"INCLINATION":51.6337,"RA_OF_ASC_NODE":151.7802,"ARG_OF_PERICENTER":96.6079,"MEAN_ANOMALY":263.5241,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59508,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":428,"BSTAR":0.0012306,"MEAN_MOTION_DOT":0.00088878,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"1998-067WJ","OBJECT_ID":"1998-067WJ","EPOCH":"2024-05-08T23:50:31.532064","MEAN_MOTION":15.5421081,"ECCENTRICITY":0.0004934,"INCLINATION":51.6352,"RA_OF_ASC_NODE":149.3927,"ARG_OF_PERICENTER":75.1638,"MEAN_ANOMALY":284.9899,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59559,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":323,"BSTAR":0.00095997,"MEAN_MOTION_DOT":0.00063795,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"1998-067WK","OBJECT_ID":"1998-067WK","EPOCH":"2024-05-08T23:33:34.532352","MEAN_MOTION":15.55918508,"ECCENTRICITY":0.0007105,"INCLINATION":51.6349,"RA_OF_ASC_NODE":149.3135,"ARG_OF_PERICENTER":85.4197,"MEAN_ANOMALY":274.7606,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59560,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":315,"BSTAR":0.0014945,"MEAN_MOTION_DOT":0.00106686,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"1998-067WL","OBJECT_ID":"1998-067WL","EPOCH":"2024-05-08T08:42:20.237472","MEAN_MOTION":15.52592728,"ECCENTRICITY":0.000278,"INCLINATION":51.6357,"RA_OF_ASC_NODE":152.6792,"ARG_OF_PERICENTER":98.5651,"MEAN_ANOMALY":261.5654,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59561,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":310,"BSTAR":0.00051054,"MEAN_MOTION_DOT":0.00031623,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"BURSTCUBE","OBJECT_ID":"1998-067WM","EPOCH":"2024-05-08T23:52:55.364736","MEAN_MOTION":15.54147491,"ECCENTRICITY":0.0002632,"INCLINATION":51.6351,"RA_OF_ASC_NODE":149.4024,"ARG_OF_PERICENTER":117.5195,"MEAN_ANOMALY":242.6063,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59562,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":321,"BSTAR":0.00095233,"MEAN_MOTION_DOT":0.00063112,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"1998-067WN","OBJECT_ID":"1998-067WN","EPOCH":"2024-05-08T23:58:11.728704","MEAN_MOTION":15.53686017,"ECCENTRICITY":0.0003114,"INCLINATION":51.6355,"RA_OF_ASC_NODE":149.4273,"ARG_OF_PERICENTER":129.054,"MEAN_ANOMALY":231.0728,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59563,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":320,"BSTAR":0.00076265,"MEAN_MOTION_DOT":0.00049516,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"SHENZHOU-18 (SZ-18)","OBJECT_ID":"2024-078A","EPOCH":"2024-05-08T13:24:27.582912","MEAN_MOTION":15.63411877,"ECCENTRICITY":0.0003902,"INCLINATION":41.4681,"RA_OF_ASC_NODE":325.2027,"ARG_OF_PERICENTER":308.5199,"MEAN_ANOMALY":51.5291,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59591,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":17289,"BSTAR":0.0004696,"MEAN_MOTION_DOT":0.00044503,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"1998-067WP","OBJECT_ID":"1998-067WP","EPOCH":"2024-05-08T12:56:24.246528","MEAN_MOTION":15.54734172,"ECCENTRICITY":0.0005166,"INCLINATION":51.635,"RA_OF_ASC_NODE":151.608,"ARG_OF_PERICENTER":73.0758,"MEAN_ANOMALY":287.0799,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59596,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":317,"BSTAR":0.0011065,"MEAN_MOTION_DOT":0.00075147,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"1998-067WQ","OBJECT_ID":"1998-067WQ","EPOCH":"2024-05-08T14:28:37.834752","MEAN_MOTION":15.54780191,"ECCENTRICITY":0.0005199,"INCLINATION":51.6358,"RA_OF_ASC_NODE":151.2866,"ARG_OF_PERICENTER":73.4697,"MEAN_ANOMALY":286.6865,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59597,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":315,"BSTAR":0.0011126,"MEAN_MOTION_DOT":0.00075697,"MEAN_MOTION_DDOT":0},{"OBJECT_NAME":"SZ-17 MODULE","OBJECT_ID":"2023-164C","EPOCH":"2024-05-08T19:41:15.555552","MEAN_MOTION":15.62250315,"ECCENTRICITY":0.0005736,"INCLINATION":41.4784,"RA_OF_ASC_NODE":323.6878,"ARG_OF_PERICENTER":330.8958,"MEAN_ANOMALY":29.1561,"EPHEMERIS_TYPE":0,"CLASSIFICATION_TYPE":"U","NORAD_CAT_ID":59624,"ELEMENT_SET_NO":999,"REV_AT_EPOCH":137,"BSTAR":0.00039322,"MEAN_MOTION_DOT":0.00035514,"MEAN_MOTION_DDOT":0}]
diff --git a/documentation/stations.tle b/documentation/stations.tle
new file mode 100644
index 000000000..5e877f73b
--- /dev/null
+++ b/documentation/stations.tle
@@ -0,0 +1,81 @@
+ISS (ZARYA)
+1 25544U 98067A 24130.36689744 .00016024 00000+0 27777-3 0 9992
+2 25544 51.6381 147.8590 0003466 150.4670 338.5502 15.51043222452521
+CSS (TIANHE)
+1 48274U 21035A 24130.00580162 .00044782 00000+0 47178-3 0 9992
+2 48274 41.4681 322.4705 0003879 310.0852 49.9647 15.63450567172966
+ISS (NAUKA)
+1 49044U 21066A 24129.82838457 .00016275 00000+0 28217-3 0 9992
+2 49044 51.6355 150.5366 0003349 149.2285 210.8902 15.51025615158179
+FREGAT DEB
+1 49271U 11037PF 24130.07809129 .00022918 00000+0 53806-1 0 9995
+2 49271 51.6682 100.0873 0970755 38.9529 327.7334 12.14183725130446
+CSS (WENTIAN)
+1 53239U 22085A 24129.81416820 .00042250 00000+0 44581-3 0 9994
+2 53239 41.4681 323.6416 0003878 309.1053 50.9442 15.63430646101857
+CSS (MENGTIAN)
+1 54216U 22143A 24129.81416820 .00042250 00000+0 44581-3 0 9995
+2 54216 41.4681 323.6416 0003878 309.1053 50.9442 15.63430646 86338
+ISS DEB [SPX-28 IPA FSE]
+1 57212U 98067VP 24129.66480476 .00424541 90482-4 72500-3 0 9990
+2 57212 51.6185 113.9060 0004565 57.1524 302.9926 16.01941673 49854
+ISS DEB
+1 58229U 98067WC 24129.50908709 .00163586 00000+0 87658-3 0 9997
+2 58229 51.6266 136.3716 0006542 42.3437 317.8068 15.79259615 29386
+PROGRESS-MS 25
+1 58460U 23184A 24129.82838457 .00016275 00000+0 28217-3 0 9997
+2 58460 51.6355 150.5366 0003349 149.2285 210.8902 15.51025615 24476
+BEAK
+1 58612U 98067WD 24130.15386897 .00158289 00000+0 95703-3 0 9998
+2 58612 51.6309 136.9549 0005919 51.3320 308.8208 15.76580994 22381
+TIANZHOU-7
+1 58811U 24013A 24129.81416820 .00042250 00000+0 44581-3 0 9998
+2 58811 41.4681 323.6416 0003878 309.1053 50.9442 15.63430646172429
+CYGNUS NG-20
+1 58898U 24021A 24129.82838457 .00016275 00000+0 28217-3 0 9993
+2 58898 51.6355 150.5366 0003349 149.2285 210.8902 15.51025615 14560
+PROGRESS-MS 26
+1 58961U 24029A 24129.82838457 .00016275 00000+0 28217-3 0 9992
+2 58961 51.6355 150.5366 0003349 149.2285 210.8902 15.51025615 12626
+CREW DRAGON 8
+1 59097U 24042A 24129.82838457 .00016275 00000+0 28217-3 0 9998
+2 59097 51.6355 150.5366 0003349 149.2285 210.8902 15.51025615 9836
+SOYUZ-MS 25
+1 59294U 24055A 24129.82838457 .00016275 00000+0 28217-3 0 9991
+2 59294 51.6355 150.5366 0003349 149.2285 210.8902 15.51025615 6371
+MICROORBITER-1
+1 59483U 98067WF 24130.22639604 .00080169 00000+0 11224-2 0 9996
+2 59483 51.6338 147.9454 0002930 101.0748 259.0573 15.56086321 4390
+CURTIS
+1 59507U 98067WG 24129.97694635 .00066412 00000+0 96605-3 0 9990
+2 59507 51.6338 149.2796 0001630 101.4702 258.6472 15.55130715 4367
+KASHIWA
+1 59508U 98067WH 24129.45319409 .00088878 00000+0 12306-2 0 9992
+2 59508 51.6337 151.7802 0002884 96.6079 263.5241 15.56340642 4280
+1998-067WJ
+1 59559U 98067WJ 24129.99342051 .00063795 00000+0 95997-3 0 9995
+2 59559 51.6352 149.3927 0004934 75.1638 284.9899 15.54210810 3236
+1998-067WK
+1 59560U 98067WK 24129.98164968 .00106686 00000+0 14945-2 0 9995
+2 59560 51.6349 149.3135 0007105 85.4197 274.7606 15.55918508 3156
+1998-067WL
+1 59561U 98067WL 24129.36273423 .00031623 00000+0 51054-3 0 9996
+2 59561 51.6357 152.6792 0002780 98.5651 261.5654 15.52592728 3107
+BURSTCUBE
+1 59562U 98067WM 24129.99508524 .00063112 00000+0 95233-3 0 9994
+2 59562 51.6351 149.4024 0002632 117.5195 242.6063 15.54147491 3216
+1998-067WN
+1 59563U 98067WN 24129.99874686 .00049516 00000+0 76265-3 0 9996
+2 59563 51.6355 149.4273 0003114 129.0540 231.0728 15.53686017 3205
+SHENZHOU-18 (SZ-18)
+1 59591U 24078A 24129.55865258 .00044503 00000+0 46960-3 0 9995
+2 59591 41.4681 325.2027 0003902 308.5199 51.5291 15.63411877172898
+1998-067WP
+1 59596U 98067WP 24129.53916952 .00075147 00000+0 11065-2 0 9990
+2 59596 51.6350 151.6080 0005166 73.0758 287.0799 15.54734172 3177
+1998-067WQ
+1 59597U 98067WQ 24129.60321568 .00075697 00000+0 11126-2 0 9990
+2 59597 51.6358 151.2866 0005199 73.4697 286.6865 15.54780191 3155
+SZ-17 MODULE
+1 59624U 23164C 24129.82031893 .00035514 00000+0 39322-3 0 9993
+2 59624 41.4784 323.6878 0005736 330.8958 29.1561 15.62250315 1375
diff --git a/examples/satellite_download.py b/examples/satellite_download.py
new file mode 100644
index 000000000..87ba8dc2a
--- /dev/null
+++ b/examples/satellite_download.py
@@ -0,0 +1,10 @@
+from skyfield.api import load
+
+max_days = 7.0 # download again once 7 days old
+name = 'stations.csv' # custom filename, not 'gp.php'
+
+base = 'https://celestrak.org/NORAD/elements/gp.php'
+url = base + '?GROUP=stations&FORMAT=csv'
+
+if not load.exists(name) or load.days_old(name) >= max_days:
+ load.download(url, filename=name)
diff --git a/skyfield/iokit.py b/skyfield/iokit.py
index f5ba0da57..6dd1495fd 100644
--- a/skyfield/iokit.py
+++ b/skyfield/iokit.py
@@ -158,7 +158,7 @@ def days_old(self, filename):
seconds = time() - mtime
return seconds / 86400.0
- def _exists(self, filename):
+ def exists(self, filename):
return os.path.exists(self.path_to(filename))
def __call__(self, filename, reload=False, backup=False, builtin=False):
@@ -346,7 +346,7 @@ def timescale(self, delta_t=None, builtin=True):
Service. For details, see :ref:`downloading-timescale-files`.
"""
- e = self._exists
+ e = self.exists
if builtin:
# See "build_arrays.py" for a notes on how these are stored.
arrays = load_bundled_npy('iers.npz')
diff --git a/skyfield/sgp4lib.py b/skyfield/sgp4lib.py
index 400cd4564..e118c74e2 100644
--- a/skyfield/sgp4lib.py
+++ b/skyfield/sgp4lib.py
@@ -4,6 +4,7 @@
from numpy import (
array, concatenate, identity, multiply, ones_like, repeat,
)
+from sgp4 import omm
from sgp4.api import SGP4_ERRORS, Satrec
from .constants import AU_KM, DAY_S, T0, tau
@@ -136,6 +137,23 @@ def from_satrec(cls, satrec, ts):
self._setup(satrec)
return self
+ @classmethod
+ def from_omm(cls, ts, element_dict):
+ """Build an EarthSatellite from OMM text fields.
+
+ Provide a ``ts`` timescale object, and a Python dict of OMM
+ field names and values. The timescale is used to build the
+ satellite's ``.epoch`` time.
+
+ """
+ self = cls.__new__(cls)
+ self.name = element_dict.get('OBJECT_NAME', None)
+ self.model = satrec = Satrec()
+ omm.initialize(satrec, element_dict)
+ self.epoch = ts._utc_jd(satrec.jdsatepoch, satrec.jdsatepochF)
+ self._setup(satrec)
+ return self
+
def __str__(self):
return self.target_name
diff --git a/skyfield/tests/test_timelib.py b/skyfield/tests/test_timelib.py
index 941862e14..00a2b1dd5 100644
--- a/skyfield/tests/test_timelib.py
+++ b/skyfield/tests/test_timelib.py
@@ -276,6 +276,20 @@ def test_building_time_from_python_date(ts):
t = ts.utc(d)
assert t.utc == (2020, 7, 22, 0, 0, 0.0)
+def test_building_time_from_utc_julian_date(ts):
+ t = ts._utc_jd(2457754.5, - one_second)
+ assert t.utc == (2016, 12, 31, 23, 59, 59.0) # no JD corresponds to s=60.0
+
+ t = ts._utc_jd(2457754.5, 0.0)
+ assert t.utc == (2017, 1, 1, 0, 0, 0.0)
+
+ t = ts._utc_jd(2457754.5, one_second)
+ assert t.utc == (2017, 1, 1, 0, 0, 1.0)
+
+def test_utc_julian_date_accuracy(ts):
+ t = ts._utc_jd(2460439.5, 0.36689744000250357)
+ assert t.utc_strftime('%H:%M:%S.%f') == '08:48:19.938816'
+
def test_timescale_linspace(ts):
t0 = ts.tt(2021, 11, 3, 6)
t1 = ts.tt(2021, 11, 5, 18)
diff --git a/skyfield/timelib.py b/skyfield/timelib.py
index 3cc3c65ea..332f6cf3e 100644
--- a/skyfield/timelib.py
+++ b/skyfield/timelib.py
@@ -263,6 +263,21 @@ def _strftime(self, format, jd, fraction, seconds_bump=None):
return [strftime(format, item) for item in zip(*tup)]
return strftime(format, tup)
+ def _utc_jd(self, whole, fraction):
+ # Switch from days to seconds.
+ seconds = whole * DAY_S
+ seconds2, fraction_s = divmod(fraction * DAY_S, 1.0)
+ seconds += seconds2
+
+ # Add an integer number of leap seconds.
+ seconds += interp(seconds, self._leap_utc, self._leap_offsets)
+
+ # Switch back to days.
+ whole, fraction = divmod(seconds, DAY_S)
+ fraction += fraction_s
+ fraction /= DAY_S
+ return self.tai_jd(whole, fraction)
+
def tai(self, year=None, month=1, day=1, hour=0, minute=0, second=0.0,
jd=None):
"""Build a `Time` from an International Atomic Time `calendar date`.