1
2=====================
3 Almanac Computation
4=====================
5
6The highest-level routines in Skyfield let you search back and forward
7through time for the exact moments when the Earth, Sun, and Moon are in
8special configurations.
9
10They all require you to start by loading up a timescale object and also
11an ephemeris file that provides positions from the planets:
12
13.. testcode::
14
15    from skyfield import api
16
17    ts = api.load.timescale()
18    eph = api.load('de421.bsp')
19
20Then, load the “almanac” module.
21
22.. testcode::
23
24    from skyfield import almanac
25
26Note that almanac computation can be slow and expensive.  To determine
27the moment of sunrise, for example, Skyfield has to search back and
28forth through time asking for the altitude of the Sun over and over
29until it finally works out the moment at which it crests the horizon.
30
31Rounding time to the nearest minute
32===================================
33
34If you compare almanac results to official sources like the `United
35States Naval Observatory <http://aa.usno.navy.mil/data/index.php>`_, the
36printed time will often differ because the Naval Observatory results are
37rounded to the nearest minute — any time with ``:30`` or more seconds at
38the end gets named as the next minute.
39
40If you try to display a date that needs to be rounded to the nearest
41minute by simply stopping at ``%M`` and leaving off the ``%S`` seconds,
42the output will be one minute too early.  For example, the Naval
43Observatory would round ``14:59`` up to ``:15`` in the following date.
44
45.. testcode::
46
47    t = ts.utc(2018, 9, 10, 5, 14, 59)
48    dt = t.utc_datetime()
49    print(dt.strftime('%Y-%m-%d %H:%M'))
50
51.. testoutput::
52
53    2018-09-10 05:14
54
55To do the same rounding yourself, simply add 30 seconds to the time
56before truncating the seconds.
57
58.. testcode::
59
60    from datetime import timedelta
61
62    def nearest_minute(dt):
63        return (dt + timedelta(seconds=30)).replace(second=0, microsecond=0)
64
65    dt = nearest_minute(t.utc_datetime())
66    print(dt.strftime('%Y-%m-%d %H:%M'))
67
68.. testoutput::
69
70    2018-09-10 05:15
71
72The results should then agree with the tables produced by the USNO.
73
74The Seasons
75===========
76
77Create a start time and an end time to ask for all of the equinoxes and
78solstices that fall in between.
79
80.. testcode::
81
82    t0 = ts.utc(2018, 1, 1)
83    t1 = ts.utc(2018, 12, 31)
84    t, y = almanac.find_discrete(t0, t1, almanac.seasons(eph))
85
86    for yi, ti in zip(y, t):
87        print(yi, almanac.SEASON_EVENTS[yi], ti.utc_iso(' '))
88
89.. testoutput::
90
91    0 Vernal Equinox 2018-03-20 16:15:27Z
92    1 Summer Solstice 2018-06-21 10:07:18Z
93    2 Autumnal Equinox 2018-09-23 01:54:06Z
94    3 Winter Solstice 2018-12-21 22:22:44Z
95
96The result ``t`` will be an array of times, and ``y`` will be ``0``
97through ``3`` for the Vernal Equinox through the Winter Solstice.
98
99If you or some of your users live in the Southern Hemisphere,
100you can use the ``SEASON_EVENTS_NEUTRAL`` array.
101Instead of naming specific seasons,
102it names the equinoxes and solstices by the month in which they occur —
103so the ``March Equinox``, for example, is followed by the ``June Solstice``.
104
105Phases of the Moon
106==================
107
108The phases of the Moon are the same for everyone on Earth, so you don’t
109need to specify the longitude and latitude of your location.  Simply ask
110for the current phase of the Moon as an angle, where 0° is New Moon and
111180° is Full:
112
113.. testcode::
114
115    t = ts.utc(2020, 11, 19)
116    phase = almanac.moon_phase(eph, t)
117    print('Moon phase: {:.1f} degrees'.format(phase.degrees))
118
119.. testoutput::
120
121    Moon phase: 51.3 degrees
122
123Or you can have Skyfield search over a range of dates for the moments
124when the Moon reaches First Quarter, Full, Last Quarter, and New:
125
126.. testcode::
127
128    t0 = ts.utc(2018, 9, 1)
129    t1 = ts.utc(2018, 9, 10)
130    t, y = almanac.find_discrete(t0, t1, almanac.moon_phases(eph))
131
132    print(t.utc_iso())
133    print(y)
134    print([almanac.MOON_PHASES[yi] for yi in y])
135
136.. testoutput::
137
138    ['2018-09-03T02:37:24Z', '2018-09-09T18:01:28Z']
139    [3 0]
140    ['Last Quarter', 'New Moon']
141
142The result ``t`` will be an array of times, and ``y`` will be a
143corresponding array of Moon phases with 0 for New Moon and 3 for Last
144Quarter.  You can use the array ``MOON_PHASES`` to retrieve names for
145each phase.
146
147.. _lunar-nodes:
148
149Lunar Nodes
150===========
151
152The Moon’s ascending node and descending node are the moments each lunar
153month when the Moon crosses the plane of Earth’s orbit and eclipses are
154possible.
155
156.. testcode::
157
158    t0 = ts.utc(2020, 4, 22)
159    t1 = ts.utc(2020, 5, 22)
160    t, y = almanac.find_discrete(t0, t1, almanac.moon_nodes(eph))
161
162    print(t.utc_iso())
163    print(y)
164    print([almanac.MOON_NODES[yi] for yi in y])
165
166.. testoutput::
167
168    ['2020-04-27T17:54:17Z', '2020-05-10T09:01:42Z']
169    [1 0]
170    ['ascending', 'descending']
171
172.. _oppositions-conjunctions:
173
174Opposition and Conjunction
175==========================
176
177The moment at which a planet is in opposition with the Sun or in
178conjunction with the Sun is when their ecliptic longitudes are at 0° or
179180° difference.
180
181.. testcode::
182
183    t0 = ts.utc(2019, 1, 1)
184    t1 = ts.utc(2021, 1, 1)
185    f = almanac.oppositions_conjunctions(eph, eph['mars'])
186    t, y = almanac.find_discrete(t0, t1, f)
187
188    print(t.utc_iso())
189    print(y)
190
191.. testoutput::
192
193    ['2019-09-02T10:42:14Z', '2020-10-13T23:25:47Z']
194    [0 1]
195
196The result ``t`` will be an array of times, and ``y`` will be an array
197of integers indicating which half of the sky the body has just entered:
1980 means the half of the sky west of the Sun along the ecliptic, and 1
199means the half of the sky east of the Sun.  This means different things
200for different bodies:
201
202* For the outer planets Mars, Jupiter, Saturn, Uranus, and all other
203  bodies out beyond our orbit, 0 means the moment of conjunction with
204  the Sun and 1 means the moment of opposition.
205
206* Because the Moon moves eastward across our sky relative to the Sun,
207  not westward, the output is reversed compared to the outer planets: 0
208  means the moment of opposition or Full Moon, while 1 means the moment
209  of conjunction or New Moon.
210
211* The inner planets Mercury and Venus only ever experience conjunctions
212  with the Sun from our point of view, never oppositions, with 0
213  indicating an inferior conjunction and 1 a superior conjunction.
214
215.. _transits:
216
217Meridian Transits
218=================
219
220Every day the Earth’s rotation
221swings the sky through nearly 360°,
222leaving the celestial poles stationary
223while bringing each star and planet in turn
224across your *meridian* —
225the “line of longitude” in the sky above you
226that runs from the South Pole to the North Pole
227through the zenith point directly above your location on Earth.
228You can ask Skyfield for the times at which a body
229crosses your meridian,
230and then the antimeridian on the opposite side of the celestial globe:
231
232.. testcode::
233
234    bluffton = api.wgs84.latlon(+40.8939, -83.8917)
235
236    t0 = ts.utc(2020, 11, 6)
237    t1 = ts.utc(2020, 11, 7)
238    f = almanac.meridian_transits(eph, eph['Mars'], bluffton)
239    t, y = almanac.find_discrete(t0, t1, f)
240
241    print(t.utc_strftime('%Y-%m-%d %H:%M'))
242    print(y)
243    print([almanac.MERIDIAN_TRANSITS[yi] for yi in y])
244
245.. testoutput::
246
247    ['2020-11-06 03:32', '2020-11-06 15:30']
248    [1 0]
249    ['Meridian transit', 'Antimeridian transit']
250
251Some astronomers call these moments
252“upper culmination” and “lower culmination” instead.
253
254Observers often think of transit as the moment
255when an object is highest in the sky,
256which is roughly true.
257But at very high precision,
258if the body has any north or south velocity
259then its moment of highest altitude will be slightly earlier or later.
260
261Bodies near the poles are exceptions to the general rule
262that a body is visible at transit but below the horizon at antitransit.
263For a body that’s circumpolar from your location,
264transit and antitransit are both moments of visibility,
265when it stands above and below the pole;
266and objects close to the opposite pole will always be below the horizon,
267even as they invisibly transit your line of longitude
268down below your horizon.
269
270Sunrise and Sunset
271==================
272
273Because sunrise and sunset differ depending on your location on the
274Earth’s surface, you will need to specify a latitude and longitude.
275
276Then you can create a start time and an end time and ask for all of the
277sunrises and sunsets in between.
278Skyfield uses the
279`official definition of sunrise and sunset
280<http://aa.usno.navy.mil/faq/docs/RST_defs.php>`_
281from the United States Naval Observatory,
282which defines them as the moment when the center — not the limb —
283of the sun is 0.8333 degrees below the horizon,
284to account for both the average radius of the Sun itself
285and for the average refraction of the atmosphere at the horizon.
286
287.. testcode::
288
289    t0 = ts.utc(2018, 9, 12, 4)
290    t1 = ts.utc(2018, 9, 13, 4)
291    t, y = almanac.find_discrete(t0, t1, almanac.sunrise_sunset(eph, bluffton))
292
293    print(t.utc_iso())
294    print(y)
295
296.. testoutput::
297
298    ['2018-09-12T11:13:13Z', '2018-09-12T23:49:38Z']
299    [1 0]
300
301The result ``t`` will be an array of times, and ``y`` will be ``1`` if
302the sun rises at the corresponding time and ``0`` if it sets.
303
304If you need to provide your own custom value for refraction, adjust the
305estimate of the Sun’s radius, or account for a vantage point above the
306Earth’s surface, see :ref:`risings-and-settings` to learn about the more
307versatile :func:`~skyfield.almanac.risings_and_settings()` routine.
308
309Note that a location near one of the poles during polar summer or polar
310winter will not experience sunrise and sunset.  To learn whether the sun
311is up or down, call the sunrise-sunset function at the time that
312interests you, and the return value will indicate whether the sun is up.
313
314.. testcode::
315
316    far_north = api.wgs84.latlon(89, -80)
317    f = almanac.sunrise_sunset(eph, far_north)
318    t, y = almanac.find_discrete(t0, t1, f)
319
320    print(t.utc_iso())  # Empty list: no sunrise or sunset
321    print(f(t0))        # But we can ask if the sun is up
322
323    print('polar day' if f(t0) else 'polar night')
324
325.. testoutput::
326
327    []
328    True
329    polar day
330
331Twilight
332========
333
334An expanded version of the sunrise-sunset routine
335named :func:`~skyfield.almanac.dark_twilight_day()`
336returns a separate code for each of the phases of twilight:
337
3380. Dark of night.
3391. Astronomical twilight.
3402. Nautical twilight.
3413. Civil twilight.
3424. Daytime.
343
344You can find a full example of its use
345at the :ref:`dark_twilight_day() example`.
346
347.. _risings-and-settings:
348
349Risings and Settings
350====================
351
352Skyfield can compute when a given body rises and sets.
353The routine is designed for bodies at the Moon’s distance or farther,
354that tend to rise and set about once a day.
355But it might be caught off guard
356if you pass it an Earth satellite
357that rises several times a day;
358for that case, see :ref:`satellite-rising-and-setting`.
359
360Rising and setting predictions can be generated
361using the :func:`~skyfield.almanac.risings_and_settings()` routine:
362
363.. testcode::
364
365    t0 = ts.utc(2020, 2, 1)
366    t1 = ts.utc(2020, 2, 2)
367    f = almanac.risings_and_settings(eph, eph['Mars'], bluffton)
368    t, y = almanac.find_discrete(t0, t1, f)
369
370    for ti, yi in zip(t, y):
371        print(ti.utc_iso(), 'Rise' if yi else 'Set')
372
373.. testoutput::
374
375    2020-02-01T09:29:16Z Rise
376    2020-02-01T18:42:57Z Set
377
378As with sunrise and sunset above,
379``1`` means the moment of rising and ``0`` means the moment of setting.
380
381The routine also offers some optional parameters,
382whose several uses are covered in the following sections.
383
384Computing your own refraction angle
385-----------------------------------
386
387Instead of accepting the standard estimate of 34 arcminutes
388for the angle by which refraction will raise the image
389of a body at the horizon,
390you can compute atmospheric refraction yourself
391and supply the resulting angle to ``horizon_degrees``.
392Note that the value passed should be a small negative angle.
393In this example it makes a 3 second difference
394in both the rising and setting time:
395
396.. testcode::
397
398    from skyfield.earthlib import refraction
399
400    r = refraction(0.0, temperature_C=15.0, pressure_mbar=1030.0)
401    print('Arcminutes refraction for body seen at horizon: %.2f\n' % (r * 60.0))
402
403    f = almanac.risings_and_settings(eph, eph['Mars'], bluffton, horizon_degrees=-r)
404    t, y = almanac.find_discrete(t0, t1, f)
405
406    for ti, yi in zip(t, y):
407        print(ti.utc_iso(), 'Rise' if yi else 'Set')
408
409.. testoutput::
410
411    Arcminutes refraction for body seen at horizon: 34.53
412
413    2020-02-01T09:29:13Z Rise
414    2020-02-01T18:43:00Z Set
415
416Adjusting for apparent radius
417-----------------------------
418
419Planets and especially the Sun and Moon have an appreciable radius,
420and we usually consider the moment of sunrise
421to be the moment when its bright limb crests the horizon —
422not the later moment when its center finally rises into view.
423Set the parameter ``radius_degrees`` to the body’s apparent radius
424to generate an earlier rising and later setting;
425the value ``0.25``, for example,
426would be a rough estimate for the Sun or Moon.
427
428The difference in rising time can be a minute or more:
429
430.. testcode::
431
432    f = almanac.risings_and_settings(eph, eph['Sun'], bluffton, radius_degrees=0.25)
433    t, y = almanac.find_discrete(t0, t1, f)
434    print(t[0].utc_iso(' '), 'Limb of the Sun crests the horizon')
435
436    f = almanac.risings_and_settings(eph, eph['Sun'], bluffton)
437    t, y = almanac.find_discrete(t0, t1, f)
438    print(t[0].utc_iso(' '), 'Center of the Sun reaches the horizon')
439
440.. testoutput::
441
442    2020-02-01 12:46:27Z Limb of the Sun crests the horizon
443    2020-02-01 12:47:53Z Center of the Sun reaches the horizon
444
445Elevated vantage points
446-----------------------
447
448Rising and setting predictions usually assume a flat local horizon
449that does not vary with elevation.
450Yes, Denver is the Mile High City,
451but it sees the sun rise against a local horizon that’s also a mile high.
452Since the city’s high altitude
453is matched by the high altitude of the terrain around it,
454the horizon winds up in the same place it would be for a city at sea level.
455
456But sometimes you need to account not only for local elevation,
457but for *altitude* above the surrounding terrain.
458Some observatories, for example, are located on mountaintops
459that are much higher than the elevation of the terrain
460that forms their horizon.
461And Earth satellites can be hundreds of kilometers
462above the surface of the Earth that produces their sunrises and sunsets.
463
464You can account for high altitude above the horizon’s terrain
465by setting an artificially negative value for ``horizon_degrees``.
466If we consider the Earth to be approximately a sphere,
467then we can use a bit of trigonometry
468to estimate the position of the horizon for an observer at altitude:
469
470.. testcode::
471
472    from numpy import arccos
473    from skyfield.units import Angle
474
475    # When does the Sun rise in the ionosphere’s F-layer, 300km up?
476    altitude_m = 300e3
477
478    earth_radius_m = 6378136.6
479    side_over_hypotenuse = earth_radius_m / (earth_radius_m + altitude_m)
480    h = Angle(radians = -arccos(side_over_hypotenuse))
481    print('The horizon from 300km up is at %.2f degrees' % h.degrees)
482
483    f = almanac.risings_and_settings(
484        eph, eph['Sun'], bluffton, horizon_degrees=h.degrees,
485        radius_degrees=0.25,
486    )
487    t, y = almanac.find_discrete(t0, t1, f)
488    print(t[0].utc_iso(' '), 'Limb of the Sun crests the horizon')
489
490.. testoutput::
491
492    The horizon from 300km up is at -17.24 degrees
493    2020-02-01 00:22:42Z Limb of the Sun crests the horizon
494
495When writing code for this situation,
496we need to be very careful to keep straight
497the two different meanings of *altitude*.
498
4991. The *altitude above sea level* is a linear distance measured in meters
500   between the ground and the location at which
501   we want to compute rises and settings.
502
5032. The *altitude of the horizon* names a quite different measure.
504   It’s an angle measured in degrees
505   that is one of the two angles
506   of the altitude-azimuth (“altazimuth”) system
507   oriented around an observer on a planet’s surface.
508   While azimuth measures horizontally around the horizon
509   from north through east, south, and west,
510   the altitude angle measures up towards the zenith (positive)
511   and down towards the nadir (negative).
512   The altitude is zero all along the great circle between zenith and nadir.
513
514The problem of an elevated observer
515unfortunately involves both kinds of altitude at the same time:
516for each extra meter of “altitude” above the ground,
517there is a slight additional depression in the angular “altitude”
518of the horizon on the altazimuth globe.
519
520When a right ascension and declination rises and sets
521-----------------------------------------------------
522
523If you are interested in finding the times
524when a fixed point in the sky rises and sets,
525simply create a star object with the coordinates
526of the position you are interested in
527(see :doc:`stars`).
528Here, for example, are rising and setting times for the Galactic Center:
529
530.. testcode::
531
532    galactic_center = api.Star(ra_hours=(17, 45, 40.04),
533                               dec_degrees=(-29, 0, 28.1))
534
535    f = almanac.risings_and_settings(eph, galactic_center, bluffton)
536    t, y = almanac.find_discrete(t0, t1, f)
537
538    for ti, yi in zip(t, y):
539        verb = 'rises above' if yi else 'sets below'
540        print(ti.utc_iso(' '), '- Galactic Center', verb, 'the horizon')
541
542.. testoutput::
543
544    2020-02-01 10:29:00Z - Galactic Center rises above the horizon
545    2020-02-01 18:45:46Z - Galactic Center sets below the horizon
546
547Solar terms
548===========
549
550The solar terms are widely used in East Asian calendars.
551
552.. testcode::
553
554    from skyfield import almanac_east_asia as almanac_ea
555
556    t0 = ts.utc(2019, 12, 1)
557    t1 = ts.utc(2019, 12, 31)
558    t, tm = almanac.find_discrete(t0, t1, almanac_ea.solar_terms(eph))
559
560    for tmi, ti in zip(tm, t):
561        print(tmi, almanac_ea.SOLAR_TERMS_ZHS[tmi], ti.utc_iso(' '))
562
563.. testoutput::
564
565    17 大雪 2019-12-07 10:18:28Z
566    18 冬至 2019-12-22 04:19:26Z
567
568The result ``t`` will be an array of times, and ``y`` will be integers
569in the range 0–23 which are each the index of a solar term.  Localized
570names for the solar terms in different East Asia languages are provided
571as ``SOLAR_TERMS_JP`` for Japanese, ``SOLAR_TERMS_VN`` for Vietnamese,
572``SOLAR_TERMS_ZHT`` for Traditional Chinese, and (as shown above)
573``SOLAR_TERMS_ZHS`` for Simplified Chinese.
574
575.. _lunar-eclipses:
576
577Lunar eclipses
578==============
579
580Skyfield can find the dates of lunar eclipses.
581
582.. testcode::
583
584    from skyfield import eclipselib
585
586    t0 = ts.utc(2019, 1, 1)
587    t1 = ts.utc(2020, 1, 1)
588    t, y, details = eclipselib.lunar_eclipses(t0, t1, eph)
589
590    for ti, yi in zip(t, y):
591        print(ti.utc_strftime('%Y-%m-%d %H:%M'),
592              'y={}'.format(yi),
593              eclipselib.LUNAR_ECLIPSES[yi])
594
595.. testoutput::
596
597    2019-01-21 05:12 y=2 Total
598    2019-07-16 21:31 y=1 Partial
599
600Note that any eclipse forecast
601is forced to make arbitrary distinctions
602when eclipses fall very close to the boundary
603between the categories “partial”, “penumbral”, and “total”.
604Skyfield searches for lunar eclipses using the techniques described
605in the *Explanatory Supplement to the Astronomical Almanac.*
606
607* Some disagreement is inevitable —
608  for example,
609  because Skyfield uses a modern ephemeris
610  while the *Supplement* used the old VSOP87 theory.
611
612* However,
613  Skyfield does currently find every one of the 7,238 lunar eclipses
614  between the years 1 and 2999
615  that are listed in NASA’s
616  `Five Millennium Canon of Lunar Eclipses
617  <https://eclipse.gsfc.nasa.gov/SEpubs/5MCLE.html>`_
618  by Espenak and Meeus.
619  While tweaks might be made to Skyfield’s routine in the future,
620  it is expected to always at least return every eclipse
621  listed in the *Canon*.
622
623* Skyfield tends to return eclipse times
624  that are a few seconds earlier than those given by the *Canon*.
625  For decades near the present the disagreement
626  rarely exceeds 2 seconds,
627  but for eclipses 2,000 years ago the difference
628  can be as large as 20 seconds.
629
630* Skyfield also finds 71 barely partial eclipses
631  beyond those listed in the *Canon*.
632
633* Skyfield agrees with the *Canon*\ ’s category
634  (partial, penumbral, total) for more than 99.6% of the eclipses.
635
636To help you study each eclipse in greater detail,
637Skyfield returns a ``details`` dictionary of extra arrays
638that provide the dimensions of the Moon and Earth’s shadow
639at the height of the eclipse.
640The dictionary currently offers the following arrays,
641whose meanings are hopefully self-explanatory:
642
643* ``closest_approach_radians``
644* ``moon_radius_radians``
645* ``penumbra_radius_radians``
646* ``umbra_radius_radians``
647
648By combining these dimensions
649with the position of the Moon at the height of the eclipse
650(which you can generate using Skyfield’s usual approach
651to computing a position),
652you should be able to produce a detailed diagram of each eclipse.
653
654For a review of the parameters that differ between eclipse forecasts,
655see NASA’s
656`Enlargement of Earth's shadows
657<https://eclipse.gsfc.nasa.gov/LEcat5/shadow.html>`_
658page on their Five Millennium Canon site.
659If you need lunar eclipse forecasts
660generated by a very specific set of parameters,
661try cutting and pasting Skyfield’s ``lunar_eclipses()`` function
662into your own code
663and making your adjustments there —
664you will have complete control of the outcome,
665and your application will be immune
666to any tweaking that takes place in Skyfield in the future
667if it’s found that Skyfield’s eclipse accuracy can become even better.
668