# Licensed under a 3-clause BSD style license - see LICENSE.rst
# -*- coding: utf-8 -*-
"""
=============
desiutil.iers
=============
Utilities for overriding astropy IERS functionality (:mod:`astropy.utils.iers`),
especially for preventing unnecessary downloads of IERS data files in a
high performance computing environment.
The frozen files come from `astropy-iers-data 0.2023.6.15.21.10.12`_, which is
the very first version, and thus closest in time to the start of the DESI survey.
If even older files are desired, in principle, they could be derived from files
available in the `IERS archive`_. However, this could also require a considerable
investment of time.
.. _`astropy-iers-data 0.2023.6.15.21.10.12`: https://pypi.org/project/astropy-iers-data/0.2023.6.15.21.10.12/
.. _`IERS archive`: https://www.iers.org/IERS/EN/DataProducts/EarthOrientationData/eop
"""
import os
import warnings
from importlib.resources import files
import numpy as np
from astropy.time import Time
import astropy.utils.iers
from .log import get_logger
#
# Global flag for frozen state.
#
_iers_is_frozen = False
[docs]
def freeze_iers(name='iers_frozen.ecsv', ignore_warnings=True):
"""Use a frozen IERS table saved with this package.
This should be called at the beginning of a script that calls
astropy time and coordinates functions which refer to the UT1-UTC
and polar motions tabulated by IERS. The purpose is to ensure
identical results across systems and astropy releases, to avoid a
potential network download, and to eliminate some astropy warnings.
After this call, all :mod:`astropy.utils.iers` functions will read
from frozen tables and updates will not be downloaded.
See https://docs.astropy.org/en/stable/utils/iers.html for details.
This function returns immediately after the first time it is called,
so it it safe to insert anywhere that consistent IERS models are
required, and subsequent calls with different arguments will have no
effect.
The :func:`desiutil.plots.plot_iers` function is useful for inspecting
IERS tables and how they are extrapolated to DESI survey dates.
Parameters
----------
name : :class:`str`, optional
This argument is ignored, but retained for backward-compatibility.
ignore_warnings : :class:`bool`, optional
Ignore ERFA and IERS warnings about future dates generated by
astropy time and coordinates functions. Specifically, ERFA warnings
containing the string "dubious year" are filtered out, as well
as AstropyWarnings related to IERS table extrapolation. Set this
to ``False`` to see the warnings.
Notes
-----
In detail this function performs these operations::
astropy.utils.iers.conf.auto_download = False
astropy.utils.iers.conf.auto_max_age = None
astropy.utils.iers.conf.iers_auto_url = 'frozen'
astropy.utils.iers.conf.iers_auto_url_mirror = 'frozen'
if ignore_warnings:
astropy.utils.iers.conf.iers_degraded_accuracy = 'ignore'
# Filter various warnings.
else:
astropy.utils.iers.conf.iers_degraded_accuracy = 'warn'
# DATA is internal to this package.
astropy.utils.iers.IERS_A_FILE = str(DATA / "finals2000A.all")
astropy.utils.iers.IERS_A_URL = "frozen"
astropy.utils.iers.IERS_A_URL_MIRROR = "frozen"
astropy.utils.iers.IERS_A_README = str(DATA / "ReadMe.finals2000A")
# Similar steps for IERS_B and Leap Second file.
"""
global _iers_is_frozen
log = get_logger()
if _iers_is_frozen:
log.debug('IERS table already frozen.')
return
log.info('Freezing IERS table used by astropy time, coordinates.')
# Prevent any attempts to automatically download updated IERS-A tables.
astropy.utils.iers.conf.auto_download = False
astropy.utils.iers.conf.auto_max_age = None
astropy.utils.iers.conf.iers_auto_url = 'frozen'
astropy.utils.iers.conf.iers_auto_url_mirror = 'frozen'
if ignore_warnings:
astropy.utils.iers.conf.iers_degraded_accuracy = 'ignore'
try:
warnings.filterwarnings('ignore',
category=astropy._erfa.core.ErfaWarning,
message=r'ERFA function \"[a-z0-9_]+\" yielded [0-9]+ of \"dubious year')
except AttributeError:
# Astropy >= 4.2
from erfa import ErfaWarning
warnings.filterwarnings('ignore',
category=ErfaWarning,
message=r'ERFA function \"[a-z0-9_]+\" yielded [0-9]+ of \"dubious year')
warnings.filterwarnings('ignore',
category=astropy.utils.exceptions.AstropyWarning,
message=r'Tried to get polar motions for times after IERS data')
warnings.filterwarnings('ignore',
category=astropy.utils.exceptions.AstropyWarning,
message=r'\(some\) times are outside of range covered by IERS')
else:
astropy.utils.iers.conf.iers_degraded_accuracy = 'warn'
#
# Redirect the table files to frozen copies internal to desiutil.
#
DATA = files('desiutil').joinpath('data')
# IERS-A default file name, URL, and ReadMe with content description
astropy.utils.iers.IERS_A_FILE = str(DATA / "finals2000A.all")
astropy.utils.iers.IERS_A_URL = "frozen"
astropy.utils.iers.IERS_A_URL_MIRROR = "frozen"
astropy.utils.iers.IERS_A_README = str(DATA / "ReadMe.finals2000A")
# IERS-B default file name, URL, and ReadMe with content description
astropy.utils.iers.IERS_B_FILE = str(DATA / "eopc04.1962-now")
astropy.utils.iers.IERS_B_URL = "frozen"
astropy.utils.iers.IERS_B_README = str(DATA / "ReadMe.eopc04")
# LEAP SECONDS default file name, URL, and alternative format/URL
astropy.utils.iers.IERS_LEAP_SECOND_FILE = str(DATA / "Leap_Second.dat")
astropy.utils.iers.IERS_LEAP_SECOND_URL = "frozen"
astropy.utils.iers.IERS_LEAP_SECOND_URL_MIRROR = "frozen"
# Shortcircuit any subsequent calls to this function.
_iers_is_frozen = True
[docs]
def update_iers(save_name='iers_frozen.ecsv', num_avg=1000):
"""Update the IERS table used by astropy time, coordinates.
Downloads the current IERS-A table, replaces the last entry (which is
repeated for future times) with the average of the last ``num_avg``
entries, and saves the table in ECSV format.
This should only be called every few months, *e.g.*, with major releases.
The saved file should then be copied to this package's data/ directory
and committed to the git repository.
Requires a network connection in order to download the current IERS-A table.
Prints information about the update process.
The :func:`desiutil.plots.plot_iers` function is useful for inspecting
IERS tables and how they are extrapolated to DESI survey dates.
Parameters
----------
save_name : :class:`str`, optional
Name where frozen IERS table should be saved. Must end with the
.ecsv extension.
num_avg : :class:`int`, optional
Number of rows from the end of the current table to average and
use for calculating UT1-UTC offsets and polar motion at times
beyond the table.
"""
log = get_logger()
# Validate the save_name extension.
_, ext = os.path.splitext(save_name)
if ext != '.ecsv':
raise ValueError('Expected .ecsv extension for {0}.'.format(save_name))
# Download the latest IERS_A table
if astropy.utils.iers.conf.iers_auto_url == 'frozen':
raise ValueError("Attempting to update a frozen IERS A table!")
iers = astropy.utils.iers.IERS_A.open(astropy.utils.iers.conf.iers_auto_url)
last = Time(iers['MJD'][-1], format='mjd').datetime
log.info('Updating to current IERS-A table with coverage up to %s.',
last.date())
# Loop over the columns used by the astropy IERS routines.
for name in 'UT1_UTC', 'PM_x', 'PM_y':
# Replace the last entry with the mean of recent samples.
mean_value = np.mean(iers[name][-num_avg:].value)
unit = iers[name].unit
iers[name][-1] = mean_value * unit
log.info('Future %7s = %.3f', name, mean_value * unit)
# Strip the original table metadata since ECSV cannot handle it.
# We only need a single keyword that is checked by IERS_Auto.open().
iers.meta = dict(data_url='frozen')
# Save the table. The IERS-B table provided with astropy uses the
# ascii.cds format but astropy cannot write this format.
iers.write(save_name, format='ascii.ecsv', overwrite=True)
log.info('Wrote updated table to %s.', save_name)