IERS
Access to Earth orientation parameters and time scale values from the International Earth Rotation and Reference Systems Service (IERS).
About IERS and Earth Orientation Parameters
The IERS is an international service that monitors the irregularities of Earth's rotation and orientation in space. Because Earth's rotation is not perfectly uniform, precise timekeeping, satellite navigation, and telescope pointing all depend on regularly updated measurements.
The key quantities tracked by the IERS are known as Earth Orientation Parameters (EOP):
- Polar motion (x, y) — the position of Earth's rotational pole relative to its crust, expressed in arcseconds. The pole wanders in a roughly circular path of a few tenths of an arcsecond over ~14 months (the Chandler wobble).
- UT1−UTC — the difference between astronomical time (UT1, tied to Earth's actual rotation angle) and coordinated universal time (UTC, maintained by atomic clocks). This difference drifts by up to ~0.9 s before a leap second is introduced to keep them close.
- Leap seconds — occasional one-second adjustments applied to UTC so that it stays within 0.9 s of UT1. Since 1972, 27 leap seconds have been added.
Data files
This library works with two data files published by the IERS:
- finals2000A — a daily table spanning from 1973 to the present (plus predictions ~1 year ahead). Each row contains polar motion, UT1−UTC, and other EOP for one Modified Julian Date. Recent rows carry rapid "Series A" values; older rows also include refined "Bulletin B" values.
- Leap_Second.dat — the complete history of leap seconds with their effective dates and the cumulative TAI−UTC offset.
Both files are downloaded automatically by IERS::Data.update! and cached
locally.
Installation
Install the gem and add to the application's Gemfile by executing:
$ bundle add iers
If Bundler is not being used to manage dependencies, install the gem by executing:
$ gem install iers
Usage
Bundled data
The gem ships with a snapshot of the IERS data files taken at release time, so queries work immediately without any download:
require "iers"
IERS::UT1.at(Time.utc(2020, 6, 15)) # works out of the box
The bundled snapshot includes predictions roughly one year into the future from the release date. As time passes those predictions expire, so you should download fresh data periodically:
result = IERS::Data.update!
result.success? # => true
You can also update a single source:
IERS::Data.update!(:finals)
IERS::Data.update!(:leap_seconds)
Downloaded files are cached in ~/.cache/iers/ by default and take precedence
over the bundled snapshot.
The bundled data files are sourced from the IERS Earth Orientation Center and the USNO Rapid Service/Prediction Center.
Polar motion
Query the pole position at any point in time:
pm = IERS::PolarMotion.at(Time.utc(2020, 6, 15))
pm.x # => 0.070979... (arcseconds)
pm.y # => 0.456571... (arcseconds)
pm.observed? # => true
Retrieve daily grid values over a date range. between returns a lazy
Enumerator, so entries are computed on demand:
entries = IERS::PolarMotion.between(
Date.new(2020, 1, 1),
Date.new(2020, 1, 31)
)
entries.count # => 31
Rotation matrix
Compute the polar motion rotation matrix W (IERS Conventions 2010, §5.4.1):
w = IERS::PolarMotion.rotation_matrix_at(Time.utc(2020, 6, 15))
w.length # => 3
w[0].length # => 3
Returns a nested Array (3×3, row-major).
The matrix is also available on any PolarMotion::Entry:
pm = IERS::PolarMotion.at(Time.utc(2020, 6, 15))
pm.rotation_matrix # => same nested Array
UT1−UTC
Query the difference between UT1 and UTC:
entry = IERS::UT1.at(Time.utc(2020, 6, 15))
entry.ut1_utc # => -0.178182...
entry.observed? # => true
Daily grid values:
entries = IERS::UT1.between(
Date.new(2020, 1, 1),
Date.new(2020, 1, 31)
)
Celestial pole offsets
Query the celestial pole offset corrections (dX, dY):
cpo = IERS::CelestialPoleOffset.at(Time.utc(2020, 6, 15))
cpo.x # => dX correction (milliarcseconds)
cpo.y # => dY correction (milliarcseconds)
Length of day
Query the excess length of day:
entry = IERS::LengthOfDay.at(Time.utc(2020, 6, 15))
entry.length_of_day # => excess LOD (seconds)
entry.observed? # => true
Delta T
Compute Delta T (TT − UT1). From 1972 onward the value is derived from IERS data; before 1972 (back to 1800) it uses Espenak & Meeus polynomial approximations:
entry = IERS::DeltaT.at(Time.utc(2020, 6, 15))
entry.delta_t # => ~69.36 (seconds)
entry.measured? # => true
entry = IERS::DeltaT.at(Time.utc(1900, 6, 15))
entry.delta_t # => ~-2.12 (seconds)
entry.estimated? # => true
Earth Rotation Angle
Compute ERA (IERS Conventions 2010, eq. 5.15). The UT1-UTC correction is looked up internally:
IERS::EarthRotationAngle.at(Time.utc(2020, 6, 15)) # => radians, in [0, 2π)
Greenwich Mean Sidereal Time
Compute GMST (IERS Conventions 2010, eq. 5.32). Uses ERA internally and adds the equinox-based polynomial evaluated at TT:
IERS::GMST.at(Time.utc(2020, 6, 15)) # => radians, in [0, 2π)
Terrestrial rotation matrix
Compute R(ERA) × W, the rotation from ITRS to TIRS (IERS Conventions 2010, eq. 5.1), combining the Earth Rotation Angle and the polar motion matrix (W):
r = IERS::TerrestrialRotation.at(Time.utc(2020, 6, 15))
r.length # => 3
r[0].length # => 3
Returns a 3×3 nested Array (row-major).
Earth Orientation Parameters (unified)
Query all EOP components at once:
eop = IERS::EOP.at(Time.utc(2020, 6, 15))
eop.polar_motion_x # => arcseconds
eop.polar_motion_y # => arcseconds
eop.ut1_utc # => seconds
eop.length_of_day # => seconds
eop.celestial_pole_x # => milliarcseconds
eop.celestial_pole_y # => milliarcseconds
eop.observed? # => true
eop.date # => #<Date: 2020-06-15>
Retrieve daily EOP over a date range:
entries = IERS::EOP.between(
Date.new(2020, 1, 1),
Date.new(2020, 1, 31)
)
Leap seconds
Look up TAI−UTC at a given date:
IERS::LeapSecond.at(Time.utc(2017, 1, 1)) # => 37 (seconds)
at covers 1961-01-01 onward. From 1972 it returns a whole number of seconds
as an Integer. Between 1961 and 1972, UTC was steered by small rate
adjustments rather than whole leap seconds, so TAI−UTC was a fraction of a
second that changed daily. at returns those values as exact Rationals:
IERS::LeapSecond.at(mjd: 38_900) # => (1910137/500000)
IERS::LeapSecond.at(mjd: 38_900).to_f # => 3.820274 (seconds, 1965-05-20)
The coefficients for that era are fixed and will never change, so they are
bundled in the gem rather than downloaded. Anything before 1961-01-01 has no
published UTC and raises OutOfRangeError.
List all leap seconds:
IERS::LeapSecond.all
# => [#<data IERS::LeapSecond::Entry effective_date=#<Date: 1972-01-01>, tai_utc=10>, ...]
Check for a future scheduled leap second:
IERS::LeapSecond.next_scheduled # => #<data IERS::LeapSecond::Entry ...> or nil
Leap second file validity
Leap_Second.dat states when it stops being authoritative and which IERS
bulletin it was updated through. Both are read from the file that is actually
loaded, so a custom or cached file reports its own header:
IERS::LeapSecond.expires_on # => #<Date: 2026-12-28> or nil
IERS::LeapSecond.expired? # => false
IERS::LeapSecond.updated_through # => "IERS Bulletin 71 issued in January 2026"
The stated date is itself still valid, so a file expiring on 28 December 2026 is expired on the 29th. An expired file is not wrong: it means the IERS has published a newer one and a leap second may have been announced since.
This is unrelated to Data.status.cache_age, which measures when the file was
downloaded rather than how long it stays valid. A bundled snapshot can be a year
old and still valid while a file downloaded this morning can be a week from
expiry.
TAI
Convert between UTC and TAI time scales:
tai_mjd = IERS::TAI.utc_to_tai(Time.utc(2017, 1, 1)) # => MJD in TAI
utc_mjd = IERS::TAI.tai_to_utc(mjd: tai_mjd) # => MJD in UTC
Time input
All query methods accept Ruby Time, Date, and DateTime objects as
positional arguments. You can also use keyword arguments for numeric Julian
Dates:
IERS::UT1.at(mjd: 58849.0) # Modified Julian Date
IERS::UT1.at(jd: 2458849.5) # Julian Date
IERS::UT1.at(Time.utc(2020, 1, 1)) # Ruby Time
Data freshness
Check that predictions cover enough of the future before relying on query results:
begin
IERS::Data.ensure_fresh!(coverage_days_ahead: 90)
rescue IERS::StaleDataError => e
puts "Predictions end #{e.predicted_until}, need #{e.required_until}"
IERS::Data.update!
end
Without coverage_days_ahead, the check ensures predictions cover today.
ensure_fresh! is about prediction coverage in finals2000A.all only. Leap
second file validity is a separate question, answered by
IERS::LeapSecond.expired?.
Data status and cache management
status = IERS::Data.status
status.cached? # => true if downloaded data exists
status.cache_age # => age in seconds, or nil
IERS::Data.clear_cache! # remove downloaded files
Custom data paths
Point the gem at your own copies of the IERS data files:
IERS.configure do |config|
config.finals_path = "/path/to/finals2000A.all"
config.leap_second_path = "/path/to/Leap_Second.dat"
end
Configuration
IERS.configure do |config|
config.cache_dir = "/path/to/cache"
config.interpolation = :linear # default: :lagrange
config.lagrange_order = 6 # default: 4
config.download_timeout = 60 # default: 30 (seconds)
end
To fully reset configuration and cached data:
IERS.reset!
Error handling
All errors inherit from IERS::Error:
-
IERS::DataError: base for data-related errorsIERS::ParseError: malformed data fileIERS::FileNotFoundError: data file not foundIERS::StaleDataError: predictions don't extend far enough
-
IERS::DownloadError: base for download-related errorsIERS::NetworkError: HTTP or connection failureIERS::ValidationError: downloaded file failed validation
IERS::OutOfRangeError— query outside data coverageIERS::ConfigurationError— invalid configuration
Development
After checking out the repo, run bin/setup to install dependencies. Then, run
rake test to run the tests. You can also run bin/console for an interactive
prompt that will allow you to experiment.
To install this gem onto your local machine, run bundle exec rake install. To
release a new version, update the version number in version.rb, and then run
bundle exec rake release, which will create a git tag for the version, push
git commits and the created tag, and push the .gem file to rubygems.org.
To refresh the bundled snapshot in data/, run bundle exec rake data:update.
A monthly workflow runs this and opens a pull request automatically.
License
The gem is available as open source under the terms of the MIT License.
Code of Conduct
Everyone interacting in the IERS Ruby project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.