.. _`Explanation`: Explanation =========== This section is to explain pulsar timing, how PINT works, and why it is built the way it is. PINT is used for pulsar timing and related activities. Some of it may make a lot more sense if you know something about the science it is used for. You can find an excellent introduction in the Handbook of Pulsar Astronomy, by Lorimer and Kramer. This document is aimed at using PINT specifically, and may also be more understandable if you have used other pulsar timing software, TEMPO_ or TEMPO2_, though we hope that you will find PINT sufficient for all your needs! .. _TEMPO: http://tempo.sourceforge.net/ .. _TEMPO2: https://www.atnf.csiro.au/research/pulsar/tempo2/ Time ---- With modern instrumentation, we are able to measure time - both time intervals and an absolute time scale - to stupendous accuracy. Pulsar timing is a powerful tool in large part because it takes advantage of that accuracy. Getting time measurements and calculations right to this level of accuracy does require a certain amount of care, in general and while using (and writing) PINT. Precision ''''''''' The first challenge that arises is numerical precision. Computers necessarily represent real numbers to finite precision. Python, in particular, uses floating-point numbers that occupy 64 bits, 11 of which encode the exponent and 53 of which encode the mantissa. This means that numbers are represented with a little less than 16 decimal digits of precision:: >>> import numpy as np >>> np.finfo(float).eps 2.220446049250313e-16 >>> 1 + np.finfo(float).eps 1.0000000000000002 >>> 1 + np.finfo(float).eps/2 1.0 >>> 1 + np.finfo(float).eps/2 == 1 True Unfortunately, we have observations spanning decades and we would often like to work with time measurements at the nanosecond level. It turns out that python's floating-point numbers simply don't have the precision we need for this:: >>> import astropy.units as u >>> (10*u.year*np.finfo(float).eps).to(u.ns) That is, if I want to represent a ten-year span, the smallest increment python's floating-point can cope with is about 70 nanoseconds - not enough for accurate pulsar timing work! There are a number of ways to approach this problem, all somewhat awkward in python. One approach of interest is that ``numpy`` provides floating-point types, for example, ``numpy.longdouble``, with more precision:: >>> np.finfo(np.longdouble).eps 1.084202172485504434e-19 >>> (10*u.year*np.finfo(np.longdouble).eps).to(u.ns) On typical x86_64 machines these values use 80-bit extended precision, with hardware support so they are not much slower than ordinary ("double-precision", 64-bit) floating-point numbers. Modern CPUs have complicated cache alignment preferences, so ``numpy`` often pads these numbers in memory; you will therefore often see ``np.float96`` or ``np.float128`` *dtypes* that still hold only 80-bit precision. True IEEE binary128 (about 33 decimal digits) *is* available as ``numpy.longdouble`` on some platforms — notably Linux on aarch64 (including Apple Silicon hosts running a native ``linux/arm64`` container). Those operations are usually implemented in software and can be slower per operation than 80-bit hardware ``longdouble``, but they provide more precision. By contrast, native macOS ARM Python builds typically make ``numpy.longdouble`` an alias of ``float64``, which is not enough for PINT; use a Linux arm64 container or an x86_64 (Rosetta) environment instead (see :ref:`Installation`). An alternative approach to dealing with more precision than your machine's floating-point numbers natively support is to represent numbers as a pair of double-precision values, with the second providing additional digits of precision to the first. These are generically called double-double numbers, and can be faster than "proper" 128-bit floating-point numbers. Sadly these are not implemented in ``numpy`` either. But because it is primarily *time* that requires such precision, ``astropy`` provides a type ``astropy.time.Time`` (and ``astropy.time.TimeDelta``) that uses a similar representation internally: two floating-point numbers, one of which is the integer number of days (in the Julian-Day_ system) and one of which is the fractional day. This allows very satisfactory precision:: >>> (1*u.day*np.finfo(float).eps).to(u.ns) >>> t = astropy.time.Time("2019-08-19", format="iso") >>> t