"""Timing model parameters encapsulated as objects.
Defines Parameter class for timing model parameters. These objects keep
track of values, uncertainties, and units. They can hold a variety of
types, both numeric - python floats and numpy longdoubles - and other -
string, angles, times.
These classes also contain code to allow them to read and write values
in both exact and human-readable forms, as well as detecting when they
have occurred in ``.par`` files.
One major complication is that timing models can often have variable
numbers of parameters: for example the ``DMX`` family of parameters
can have one parameter for each group of TOAs in the input, allowing
potentially very many. These are handled in two separate ways, as "prefix
parameters" (:class:`pint.models.parameter.prefixParameter`) and
"mask parameters" (:class:`pint.models.parameter.maskParameter`)
depending on how they occur in the ``.par`` and ``.tim`` files.
See :ref:`Supported Parameters` for an overview, including a table of all the
parameters PINT understands.
"""
import numbers
from warnings import warn
import astropy.time as time
import astropy.units as u
import numpy as np
from astropy.coordinates.angles import Angle
from uncertainties import ufloat
from loguru import logger as log
from pint import pint_units
from pint.models import priors
from pint.observatory import get_observatory
from pint.pulsar_mjd import (
Time,
data2longdouble,
quantity2longdouble_withunit,
fortran_float,
str2longdouble,
time_from_longdouble,
time_to_longdouble,
time_to_mjd_string,
)
from pint.toa_select import TOASelect
from pint.utils import split_prefixed_name
# potential parfile formats
# in one place for consistency
_parfile_formats = ["pint", "tempo", "tempo2"]
def _identity_function(x):
"""A function to just return the input argument
A replacement for::
lambda x: x
which is needed below.
Parameters
----------
x
Returns
-------
x
"""
return x
def _get_observatory_name(o):
"""Return observatory name only from an telescope code
Parameters
----------
o : str or unicode
Input telescope code
Returns
-------
str
"""
return get_observatory(str(o)).name
def _return_frequency_asquantity(f):
"""Return frequency as a quantity (MHz assumed)
Parameters
----------
f : float
Returns
-------
astropy.units.Quantity
"""
return u.Quantity(f, u.MHz, copy=False)
[docs]class Parameter:
"""A single timing model parameter.
Subclasses of this class can represent parameters of various types. They
can record units, a description of the parameter's meaning, a default value
in some cases, whether the parameter has ever been set, and they can
keep track of whether a parameter is to be fit or not.
Parameters can also come in families, either in the form of numbered
:class:`~pint.models.parameter.prefixParameter` or with associated
selection criteria in the form of
:class:`~pint.models.parameter.maskParameter`.
A parameter's current value will be stored at ``.quantity``, which will
have associated units (:class:`astropy.quantity.Quantity`) or other special
type machinery, or can also be accessed through ``.value``, which provides
the raw value (stripped of units if applicable). Both of these can be
assigned to to change the parameter's value. If the parameter has units,
they will be accessible through the ``.units`` property (an
:class:`astropy.units.Unit`). A parameter that has not been set will have
the value None.
Parameters also support uncertainties; these are available including units
through the ``.uncertainty`` attribute. Parameters can also be set as
``.frozen=True`` to indicate that they should not be modified as part of a
fit.
Parameters
----------
name : str, optional
The name of the parameter.
value : number, str, astropy.units.Quantity, or other data type or object
The input parameter value. Quantities are accepted here, but when the
corresponding property is read the value will never have units.
units : str or astropy.units.Unit, optional
Parameter default unit. Parameter .value and .uncertainty_value attribute
will associate with the default units.
description : str, optional
A short description of what this parameter means.
uncertainty : float
Current uncertainty of the value.
frozen : bool, optional
A flag specifying whether :class:`~pint.fitter.Fitter` objects should
adjust the value of this parameter or leave it fixed.
aliases : list, optional
An optional list of strings specifying alternate names that can also
be accepted for this parameter.
continuous : bool, optional
A flag specifying whether derivatives with respect to this
parameter exist.
use_alias : str or None
Alias to use on write; normally whatever alias was in the par
file it was read from
parent: pint.models.timing_model.Component, optional
The parent timing model component
Attributes
----------
quantity : astropy.units.Quantity or astropy.time.Time or bool or int
The parameter's value
"""
def __init__(
self,
name=None,
value=None,
units=None,
description=None,
uncertainty=None,
frozen=True,
aliases=None,
continuous=True,
prior=priors.Prior(priors.UniformUnboundedRV()),
use_alias=None,
parent=None,
):
self.name = name # name of the parameter
# The input parameter from parfile, which can be an alias of the parameter
# TODO give a better name and make it easy to access.
self._parfile_name = name
self.units = units # Default unit
self.quantity = value # The value of parameter, internal storage
self.prior = prior
self.description = description
self.uncertainty = uncertainty
self.frozen = frozen
self.continuous = continuous
self.aliases = [] if aliases is None else aliases
self.is_prefix = False
self.paramType = "Not specified" # Type of parameter. Here is general type
self.valueType = None
self.special_arg = []
self.use_alias = use_alias
self._parent = parent
@property
def quantity(self):
"""Value including units (if appropriate)."""
return self._quantity
@quantity.setter
def quantity(self, val):
"""General wrapper method to set .quantity.
For different type of
parameters, the setter method is stored at ._set_quantity attribute.
"""
if val is None:
if hasattr(self, "quantity") and self.quantity is not None:
raise ValueError("Setting an existing value to None is not allowed.")
self._quantity = val
return
self._quantity = self._set_quantity(val)
@property
def value(self):
"""Return the value (without units) of a parameter.
This value is assumed to be in units of ``self.units``. Upon setting, a
a :class:`~astropy.units.Quantity` can be provided, which will be converted
to ``self.units``.
"""
return None if self._quantity is None else self._get_value(self._quantity)
@value.setter
def value(self, val):
if val is None:
if (
not isinstance(self.quantity, (str, bool))
and self._quantity is not None
):
raise ValueError(
"Setting .value to None will lose the parameter value."
)
else:
self.value = val
self._quantity = self._set_quantity(val)
@property
def units(self):
"""Units associated with this parameter.
Should be a :class:`astropy.units.Unit` object, or None if never set.
"""
return self._units
@units.setter
def units(self, unt):
# Check if this is the first time set units and check compatibility
if hasattr(self, "quantity") and self.units is not None:
if unt != self.units:
wmsg = f"Parameter {self.name} default units has been "
wmsg += f" reset to {str(unt)} from {str(self.units)}"
log.warning(wmsg)
try:
if hasattr(self.quantity, "unit"):
self.quantity.to(unt)
except ValueError:
log.warning(
"The value unit is not compatible with"
" parameter units right now."
)
if unt is None:
self._units = None
# Always compare a string to pint_units.keys()
# If search an astropy unit object with a sting list
# If the string does not match astropy unit, astropy will guess what
# does the string mean. It will take a lot of time.
elif isinstance(unt, str) and unt in pint_units.keys():
# These are special-case unit strings in in PINT
self._units = pint_units[unt]
else:
# Try to use it as an astropy unit. If this fails,
# ValueError will be raised.
self._units = u.Unit(unt)
if hasattr(self, "quantity") and hasattr(self.quantity, "unit"):
# Change quantity unit to new unit
self.quantity = self.quantity.to(self._units)
if hasattr(self, "uncertainty") and hasattr(self.uncertainty, "unit"):
# Change uncertainty unit to new unit
self.uncertainty = self.uncertainty.to(self._units)
@property
def uncertainty(self):
"""Parameter uncertainty value with units."""
return self._uncertainty
@uncertainty.setter
def uncertainty(self, val):
if val is None:
if hasattr(self, "uncertainty") and self.uncertainty is not None:
raise ValueError(
"Setting an existing uncertainty to None is not allowed."
)
self._uncertainty = self._uncertainty_value = None
return
val = self._set_uncertainty(val)
if val < 0:
raise ValueError(f"Uncertainties cannot be negative but {val} was supplied")
# self.uncertainty_value = np.abs(self.uncertainty_value)
self._uncertainty = val.to(self.units)
@property
def uncertainty_value(self):
"""Return a pure value from .uncertainty.
This will be interpreted as having units ``self.units``.
"""
# FIXME: is this worth having when p.uncertainty.value does the same thing?
if self._uncertainty is None:
return None
else:
return self._get_value(self._uncertainty)
@uncertainty_value.setter
def uncertainty_value(self, val):
if val is None:
if (
not isinstance(self.uncertainty, (str, bool))
and self._uncertainty_value is not None
):
log.warning(
"This parameter has uncertainty value. "
"Change it to None will lost information."
)
else:
self.uncertainty_value = val
self._uncertainty = self._set_uncertainty(val)
def _get_value(self, quan):
"""Extract a raw value from internal representation.
Generally just returns the internal representation, but some subclasses
may override this to, say, convert to the correct units and then discard
them.
"""
return quan
def _set_quantity(self, val):
"""Convert value to internal representation.
Subclasses may override this to, for example, parse Fortran-format strings into
long doubles.
"""
return val
def _set_uncertainty(self, val):
"""Convert value to internal representation for use in uncertainty."""
if val != 0:
raise NotImplementedError()
@property
def repeatable(self):
return False
@property
def prior(self):
"""prior distribution for this parameter.
This should be a :class:`~pint.models.priors.Prior` object describing the prior
distribution of the quantity, for use in Bayesian fitting.
"""
return self._prior
@prior.setter
def prior(self, p):
if not isinstance(p, priors.Prior):
raise ValueError("prior must be an instance of Prior()")
self._prior = p
[docs] def prior_pdf(self, value=None, logpdf=False):
"""Return the prior probability density.
Evaluated at the current value of the parameter, or at a proposed value.
Parameters
----------
value : array-like or float, optional
Where to evaluate the priors; should be a unitless number.
If not provided the prior is evaluated at ``self.value``.
logpdf : bool
If True, return the logarithm of the PDF instead of the PDF;
this can help with densities too small to represent in floating-point.
"""
if value is None:
value = self.value
return self.prior.logpdf(value) if logpdf else self.prior.pdf(value)
[docs] def str_quantity(self, quan):
"""Format the argument in an appropriate way as a string."""
return str(quan)
def _print_uncertainty(self, uncertainty):
"""Represent uncertainty in the form of a string.
This converts the :class:`~astropy.units.Quantity` provided to the
appropriate units, extracts the value, and converts that to a string.
"""
return str(uncertainty.to(self.units).value)
def __repr__(self):
out = "{0:16s}{1:20s}".format(f"{self.__class__.__name__}(", self.name)
if self.quantity is None:
out += "UNSET"
return out
out += "{:17s}".format(self.str_quantity(self.quantity))
if self.units is not None:
out += f" ({str(self.units)})"
if self.uncertainty is not None and isinstance(self.value, numbers.Number):
out += f" +/- {str(self.uncertainty.to(self.units))}"
out += f" frozen={self.frozen}"
out += ")"
return out
[docs] def help_line(self):
"""Return a help line containing parameter name, description and units."""
out = "%-12s %s" % (self.name, self.description)
if self.units is not None:
out += f" ({str(self.units)})"
return out
[docs] def as_parfile_line(self, format="pint"):
"""Return a parfile line giving the current state of the parameter.
Parameters
----------
format : str, optional
Parfile output format. PINT outputs in 'tempo', 'tempo2' and 'pint'
formats. The default format is `pint`.
Returns
-------
str
Notes
-----
Format differences between tempo, tempo2, and pint at [1]_
.. [1] https://github.com/nanograv/PINT/wiki/PINT-vs.-TEMPO%282%29-par-file-changes
"""
assert (
format.lower() in _parfile_formats
), "parfile format must be one of %s" % ", ".join(
[f'"{x}"' for x in _parfile_formats]
)
# Don't print unset parameters
if self.quantity is None:
return ""
name = self.name if self.use_alias is None else self.use_alias
# special cases for parameter names that change depending on format
if self.name in ["DMRES"] and format.lower() not in ["pint"]:
# DMRES only for PINT
return ""
elif self.name == "SWM" and format.lower() != "pint":
# no SWM for TEMPO/TEMPO2
return ""
elif self.name == "A1DOT" and format.lower() != "pint":
# change to XDOT for TEMPO/TEMPO2
name = "XDOT"
elif self.name == "STIGMA" and format.lower() != "pint":
# change to VARSIGMA for TEMPO/TEMPO2
name = "VARSIGMA"
# standard output formatting
line = "%-15s %25s" % (name, self.str_quantity(self.quantity))
# special cases for parameter values that change depending on format
if self.name == "ECL" and format.lower() == "tempo2":
if self.value != "IERS2003":
log.warning(
f"Changing ECL from '{self.value}' to 'IERS2003'; please refit for consistent results"
)
# change ECL value to IERS2003 for TEMPO2
line = "%-15s %25s" % (name, "IERS2003")
elif self.name == "NHARMS" and format.lower() != "pint":
# convert NHARMS value to int
line = "%-15s %25d" % (name, self.value)
elif self.name == "KIN" and format.lower() == "tempo":
# convert from DT92 convention to IAU
line = "%-15s %25s" % (name, self.str_quantity(180 * u.deg - self.quantity))
log.warning(
"Changing KIN from DT92 convention to IAU: this will not be readable by PINT"
)
elif self.name == "KOM" and format.lower() == "tempo":
# convert from DT92 convention to IAU
line = "%-15s %25s" % (name, self.str_quantity(90 * u.deg - self.quantity))
log.warning(
"Changing KOM from DT92 convention to IAU: this will not be readable by PINT"
)
elif self.name == "DMDATA" and format.lower() != "pint":
line = "%-15s %d" % (self.name, int(self.value))
if self.uncertainty is not None:
line += " %d %s" % (
0 if self.frozen else 1,
self._print_uncertainty(self.uncertainty),
)
elif not self.frozen:
line += " 1"
if self.name == "T2CMETHOD" and format.lower() == "tempo2":
# comment out T2CMETHOD for TEMPO2
line = f"#{line}"
return line + "\n"
[docs] def from_parfile_line(self, line):
"""Parse a parfile line into the current state of the parameter.
Returns True if line was successfully parsed, False otherwise.
Note
----
The accepted formats:
* NAME value
* NAME value fit_flag
* NAME value fit_flag uncertainty
* NAME value uncertainty
"""
try:
k = line.split()
name = k[0]
except IndexError:
return False
# Test that name matches
if not self.name_matches(name.upper()):
return False
if len(k) < 2:
return False
self.value = k[1]
if name != self.name:
# FIXME: what about prefix/mask parameters?
self.use_alias = name
if len(k) >= 3:
try:
# FIXME! this is not right
fit_flag = int(k[2])
if fit_flag == 0:
self.frozen = True
ucty = 0.0
elif fit_flag == 1:
self.frozen = False
ucty = 0.0
else:
ucty = fit_flag
except ValueError:
try:
str2longdouble(k[2])
ucty = k[2]
except ValueError as e:
errmsg = f"Unidentified string '{k[2]}' in"
errmsg += " parfile line " + " ".join(k)
raise ValueError(errmsg) from e
if len(k) >= 4:
ucty = k[3]
self.uncertainty = self._set_uncertainty(ucty)
return True
def value_as_latex(self):
return f"${self.as_ufloat():.1uSL}$" if not self.frozen else f"{self.value:f}"
def as_latex(self):
try:
unit_latex = (
""
if self.units == "" or self.units is None
else f" ({self.units.to_string(format='latex', fraction=False)})"
)
except TypeError:
# to deal with old astropy
unit_latex = (
""
if self.units == "" or self.units is None
else f" ({self.units.to_string(format='latex')})"
)
value_latex = self.value_as_latex()
return f"{self.name}, {self.description}{unit_latex}", value_latex
[docs] def add_alias(self, alias):
"""Add a name to the list of aliases for this parameter."""
self.aliases.append(alias)
[docs] def name_matches(self, name):
"""Whether or not the parameter name matches the provided name"""
return (
(name == self.name.upper())
or (name in [x.upper() for x in self.aliases])
or (split_prefixed_name(name) == split_prefixed_name(self.name.upper()))
)
[docs] def set(self, value):
"""Deprecated - just assign to .value."""
warn(
"The .set() function is deprecated. Set self.value directly instead.",
category=DeprecationWarning,
)
self.value = value
[docs]class floatParameter(Parameter):
"""Parameter with float or long double value.
``.quantity`` stores current parameter value and its unit in an
:class:`~astropy.units.Quantity`. Upon storage in ``.quantity``
the input is converted to ``self.units``.
Parameters
----------
name : str
The name of the parameter.
value : number, str, or astropy.units.Quantity
The input parameter float value.
units : str or astropy.units.Quantity
Parameter default unit. Parameter .value and .uncertainty_value attribute
will associate with the default units. If unit is dimensionless, use
"''" as its unit.
description : str, optional
A short description of what this parameter means.
uncertainty : number
Current uncertainty of the value.
frozen : bool, optional
A flag specifying whether "fitters" should adjust the value of this
parameter or leave it fixed.
aliases : list, optional
An optional list of strings specifying alternate names that can also
be accepted for this parameter.
continuous : bool, optional, default True
A flag specifying whether phase derivatives with respect to this
parameter exist.
long_double : bool, optional, default False
A flag specifying whether value is float or long double.
Example
-------
>>> from parameter import floatParameter
>>> test = floatParameter(name='test1', value=100.0, units='second')
>>> print(test)
test1 (s) 100.0
"""
def __init__(
self,
name=None,
value=None,
units=None,
description=None,
uncertainty=None,
frozen=True,
aliases=None,
continuous=True,
long_double=False,
unit_scale=False,
scale_factor=None,
scale_threshold=None,
**kwargs,
):
self.long_double = long_double
self.scale_factor = scale_factor
self.scale_threshold = scale_threshold
self._unit_scale = False
if units is None:
units = ""
super().__init__(
name=name,
value=value,
units=units,
frozen=frozen,
aliases=aliases,
continuous=continuous,
description=description,
uncertainty=uncertainty,
)
self.paramType = "floatParameter"
self.special_arg += [
"long_double",
"unit_scale",
"scale_threshold",
"scale_factor",
]
self.unit_scale = unit_scale
@property
def long_double(self):
"""Whether the parameter has long double precision."""
# FIXME: why not just always keep long double precision?
return self._long_double
@long_double.setter
def long_double(self, val):
"""long double setter, if a floatParameter's longdouble flag has been
changed, `.quantity` will get reset in order to get to the right data
type.
"""
if not isinstance(val, bool):
raise ValueError("long_double property can only be set as boolean" " type")
if hasattr(self, "long_double"):
if self.long_double != val and hasattr(self, "quantity"):
if not val:
log.warning(
"Setting floatParameter from long double to float,"
" precision will be lost."
)
# Reset quantity to its asked type
self._long_double = val
self.quantity = self.quantity
else:
self._long_double = val
@property
def unit_scale(self):
"""If True, the parameter can automatically scale some values upon assignment."""
return self._unit_scale
@unit_scale.setter
def unit_scale(self, val):
self._unit_scale = val
if self._unit_scale:
if self.scale_factor is None:
raise ValueError(
"The scale factor should be given if unit_scale"
" is set to be True."
)
if self.scale_threshold is None:
raise ValueError(
"The scale threshold should be given if unit_scale"
" is set to be True."
)
def _set_quantity(self, val):
"""Convert input to floating-point format.
accept format
1. Astropy quantity
2. float
3. string
"""
# Check long_double
if not self._long_double:
setfunc_with_unit = _identity_function
setfunc_no_unit = fortran_float
else:
setfunc_with_unit = quantity2longdouble_withunit
setfunc_no_unit = data2longdouble
# First try to use astropy unit conversion
try:
# If this fails, it will raise UnitConversionError
val.to(self.units)
result = setfunc_with_unit(val)
except AttributeError:
# This will happen if the input value did not have units
num_value = setfunc_no_unit(val)
# For some parameters, if the value is above a threshold, it is assumed to be in units of scale_factor
# e.g. "PBDOT 7.2" is interpreted as "PBDOT 7.2E-12", since the scale_factor is 1E-12 and the scale_threshold is 1E-7
if self.unit_scale and np.abs(num_value) > np.abs(self.scale_threshold):
log.info(
f"Parameter {self.name}'s value will be scaled by {str(self.scale_factor)}"
)
num_value *= self.scale_factor
result = num_value * self.units
return result
def _set_uncertainty(self, val):
return self._set_quantity(val)
[docs] def str_quantity(self, quan):
"""Quantity as a string (for floating-point values)."""
v = quan.to(self.units).value
if self._long_double and not isinstance(v, np.longdouble):
raise ValueError(
"Parameter is supposed to contain long double values but contains a float"
)
return str(v)
def _get_value(self, quan):
"""Convert to appropriate units and extract value."""
if quan is None:
return None
elif isinstance(quan, (float, np.longdouble)):
return quan
elif isinstance(quan, list):
# for pairParamters
return [x.to(self.units).value for x in quan]
else:
return quan.to(self.units).value
[docs] def as_ufloat(self, units=None):
"""Return the parameter as a :class:`uncertainties.ufloat`
Will cast to the specified units, or the default
If the uncertainty is not set will be returned as 0
Parameters
----------
units : astropy.units.core.Unit, optional
Units to cast the value
Returns
-------
uncertainties.ufloat
Notes
-----
Currently :class:`~uncertainties.ufloat` does not support double precision values,
so some precision may be lost.
"""
if units is None:
units = self.units
value = self.quantity.to_value(units) if self.quantity is not None else 0
error = self.uncertainty.to_value(units) if self.uncertainty is not None else 0
return ufloat(value, error)
[docs] def from_ufloat(self, value, units=None):
"""Set the parameter from the value of a :class:`uncertainties.ufloat`
Will cast to the specified units, or the default
If the uncertainty is 0 it will be set to ``None``
Parameters
----------
value : uncertainties.ufloat
units : astropy.units.core.Unit, optional
Units to cast the value
"""
if units is None:
units = self.units
self.quantity = value.n * units
self.uncertainty = value.s * units if value.s > 0 else None
[docs]class strParameter(Parameter):
"""String-valued parameter.
``strParameter`` is not fittable.
Parameters
----------
name : str
The name of the parameter.
value : str
The input parameter string value.
description : str, optional
A short description of what this parameter means.
aliases : list, optional
An optional list of strings specifying alternate names that can also
be accepted for this parameter.
Example
-------
>>> from parameter import strParameter
>>> test = strParameter(name='test1', value='This is a test',)
>>> print(test)
test1 This is a test
"""
def __init__(self, name=None, value=None, description=None, aliases=None, **kwargs):
# FIXME: where did kwargs go?
super().__init__(
name=name,
value=value,
description=description,
frozen=True,
aliases=aliases,
)
self.paramType = "strParameter"
self.value_type = str
def _set_quantity(self, val):
"""Convert to string."""
return str(val)
def value_as_latex(self):
return self.value
[docs]class boolParameter(Parameter):
"""Boolean-valued parameter.
Boolean parameters support ``1``/``0``, ``T``/``F``, ``Y``/``N``,
``True``/``False``, or ``Yes``/``No`` in any combination of upper and lower
case. They always output ``Y`` or ``N`` in a par file.
Parameters
----------
name : str
The name of the parameter.
value : str, bool, [0,1]
The input parameter boolean value.
description : str, optional
A short description of what this parameter means.
aliases : list, optional
An optional list of strings specifying alternate names that can also
be accepted for this parameter.
Example
-------
>>> from parameter import boolParameter
>>> test = boolParameter(name='test1', value='N')
>>> print(test)
test1 N
"""
def __init__(
self,
name=None,
value=None,
description=None,
frozen=True,
aliases=None,
**kwargs,
):
# FIXME: where did kwargs go?
super().__init__(
name=name,
value=value,
description=description,
frozen=True,
aliases=aliases,
)
self.value_type = bool
self.paramType = "boolParameter"
[docs] def str_quantity(self, quan):
return "Y" if quan else "N"
def _set_quantity(self, val):
"""Get boolean value for boolParameter class"""
# First try strings
try:
if val.upper() in ["Y", "YES", "T", "TRUE"]:
return True
elif val.upper() in ["N", "NO", "F", "FALSE"]:
return False
except AttributeError:
# Will get here on non-string types
pass
else:
# String not in the list
return bool(float(val))
return bool(val)
def value_as_latex(self):
return "Y" if self.value else "N"
[docs]class intParameter(Parameter):
"""Integer parameter values.
Parameters
----------
name : str
The name of the parameter.
value : int
The parameter value.
description : str, optional
A short description of what this parameter means.
aliases : list, optional
An optional list of strings specifying alternate names that can also
be accepted for this parameter.
Example
-------
>>> from parameter import intParameter
>>> test = intParameter(name='test1', value=7)
>>> print(test)
test1 7
"""
def __init__(
self,
name=None,
value=None,
description=None,
frozen=True,
aliases=None,
**kwargs,
):
# FIXME: where did kwargs go?
super().__init__(
name=name,
value=value,
description=description,
frozen=True,
aliases=aliases,
)
self.value_type = int
self.paramType = "intParameter"
def _set_quantity(self, val):
"""Convert a string or other value to an integer."""
if isinstance(val, str):
try:
ival = int(val)
except ValueError as e:
fval = float(val)
ival = int(fval)
if ival != fval and abs(fval) < 2**52:
raise ValueError(
f"Value {val} does not appear to be an integer "
f"but parameter {self.name} stores only integers."
) from e
else:
ival = int(val)
fval = float(val)
if ival != fval and abs(fval) < 2**52:
raise ValueError(
f"Value {val} does not appear to be an integer "
f"but parameter {self.name} stores only integers."
)
return ival
def value_as_latex(self):
return str(self.value)
[docs]class MJDParameter(Parameter):
"""Parameters for MJD quantities.
``.quantity`` stores current parameter information in an
:class:`astropy.time.Time` type in the format of MJD. ``.value`` returns
the pure long double MJD value. ``.units`` is in day as default unit. Note
that you can't make an :class:`astropy.time.Time` object just by
multiplying a number by ``u.day``; there are complexities in constructing
times.
Parameters
----------
name : str
The name of the parameter.
value : astropy Time, str, float in mjd, str in mjd.
The input parameter MJD value.
description : str, optional
A short description of what this parameter means.
uncertainty : number
Current uncertainty of the value.
frozen : bool, optional
A flag specifying whether "fitters" should adjust the value of this
parameter or leave it fixed.
continuous : bool, optional, default True
A flag specifying whether phase derivatives with respect to this
parameter exist.
aliases : list, optional
An optional list of strings specifying alternate names that can also
be accepted for this parameter.
time_scale : str, optional, default 'tdb'
MJD parameter time scale.
Example
-------
>>> from parameter import MJDParameter
>>> test = MJDParameter(name='test1', value='54000', time_scale='utc')
>>> print(test)
test1 (d) 54000.000000000000000
"""
def __init__(
self,
name=None,
value=None,
description=None,
uncertainty=None,
frozen=True,
continuous=True,
aliases=None,
time_scale="tdb",
**kwargs,
):
self._time_scale = time_scale
# FIXME: where did kwargs go?
super().__init__(
name=name,
value=value,
units="MJD",
description=description,
uncertainty=uncertainty,
frozen=frozen,
continuous=continuous,
aliases=aliases,
)
self.value_type = time.Time
self.paramType = "MJDParameter"
self.special_arg += ["time_scale"]
[docs] def str_quantity(self, quan):
return time_to_mjd_string(quan)
def _get_value(self, quan):
return time_to_longdouble(quan)
@property
def time_scale(self):
return self._time_scale
@time_scale.setter
def time_scale(self, val):
self._time_scale = val
mjd = self.value
self.quantity = mjd
@property
def uncertainty_value(self):
"""Return a pure value from .uncertainty.
The unit will associate with .units
"""
if self._uncertainty is None:
return None
else:
return self._uncertainty.to_value(self.units)
@uncertainty_value.setter
def uncertainty_value(self, val):
if val is None:
if (
not isinstance(self.uncertainty, (str, bool))
and self._uncertainty_value is not None
):
log.warning(
"This parameter has uncertainty value. "
"Change it to None will lost information."
)
else:
self.uncertainty_value = val
self._uncertainty = self._set_uncertainty(val)
def _set_quantity(self, val):
"""Value setter for MJD parameter,
Accepted format:
Astropy time object
mjd float
mjd string (in pulsar_mjd format)
"""
if isinstance(val, numbers.Number):
val = np.longdouble(val)
result = time_from_longdouble(val, self.time_scale)
elif isinstance(val, (str, bytes)):
result = Time(val, scale=self.time_scale, format="pulsar_mjd_string")
elif isinstance(val, time.Time):
result = val
else:
raise ValueError(
f"MJD parameter can not accept {type(val).__name__}format."
)
return result
def _set_uncertainty(self, val):
# First try to use astropy unit conversion
try:
# If this fails, it will raise UnitConversionError
val.to(self.units)
result = data2longdouble(val.value) * self.units
except AttributeError:
# This will happen if the input value did not have units
result = data2longdouble(val) * self.units
return result
def _print_uncertainty(self, uncertainty):
return str(self.uncertainty_value)
[docs] def as_ufloats(self):
"""Return the parameter as a pair of :class:`uncertainties.ufloat`
values representing the integer and fractional Julian dates.
The uncertainty is carried by the latter.
If the uncertainty is not set will be returned as 0
Returns
-------
uncertainties.ufloat
uncertainties.ufloat
"""
value1 = self.quantity.jd1 if self.quantity is not None else 0
value2 = self.quantity.jd2 if self.quantity is not None else 0
error = self.uncertainty.to_value(u.d) if self.uncertainty is not None else 0
return ufloat(value1, 0), ufloat(value2, error)
[docs] def as_ufloat(self):
"""Return the parameter as a :class:`uncertainties.ufloat`
value.
If the uncertainty is not set will be returned as 0
Returns
-------
uncertainties.ufloat
"""
return ufloat(self.value, self.uncertainty_value)
[docs]class AngleParameter(Parameter):
"""Parameter in angle units.
``.quantity`` stores current parameter information in an :class:`astropy.units.Angle` type.
``AngleParameter`` can accept angle format ``{'h:m:s': u.hourangle,
'd:m:s': u.deg, 'rad': u.rad, 'deg': u.deg}``
Parameters
----------
name : str
The name of the parameter.
value : angle string, float, astropy angle object
The input parameter angle value.
description : str, optional
A short description of what this parameter means.
uncertainty : number
Current uncertainty of the value.
frozen : bool, optional
A flag specifying whether "fitters" should adjust the value of this
parameter or leave it fixed.
continuous : bool, optional, default True
A flag specifying whether phase derivatives with respect to this
parameter exist.
aliases : list, optional
An optional list of strings specifying alternate names that can also
be accepted for this parameter.
Example
-------
>>> from parameter import AngleParameter
>>> test = AngleParameter(name='test1', value='12:20:10', units='H:M:S')
>>> print(test)
test1 (hourangle) 12:20:10.00000000
"""
def __init__(
self,
name=None,
value=None,
description=None,
units="rad",
uncertainty=None,
frozen=True,
continuous=True,
aliases=None,
**kwargs,
):
self._str_unit = units
self.unit_identifier = {
"h:m:s": (u.hourangle, "h", pint_units["hourangle_second"]),
"d:m:s": (u.deg, "d", u.arcsec),
"rad": (u.rad, "rad", u.rad),
"deg": (u.deg, "deg", u.deg),
}
# Check unit format
if units.lower() not in self.unit_identifier.keys():
raise ValueError(f"Unidentified unit {units}")
self.unitsuffix = self.unit_identifier[units.lower()][1]
self.value_type = Angle
self.paramType = "AngleParameter"
# FIXME: where did kwargs go?
super().__init__(
name=name,
value=value,
units=units,
description=description,
uncertainty=uncertainty,
frozen=frozen,
continuous=continuous,
aliases=aliases,
)
def _get_value(self, quan):
# return Angle(x * self.unit_identifier[units.lower()][0])
return quan.value
def _set_quantity(self, val):
"""This function is to set value to angle parameters.
Accepted format:
1. Astropy angle object
2. float
3. number string
"""
if isinstance(val, numbers.Number):
result = Angle(np.longdouble(val) * self.units)
elif isinstance(val, str):
# FIXME: what if the user included a unit suffix?
result = Angle(val + self.unitsuffix)
elif hasattr(val, "unit"):
result = Angle(val.to(self.units))
else:
raise ValueError(
f"Angle parameter can not accept {type(val).__name__}format."
)
return result
def _set_uncertainty(self, val):
"""This function is to set the uncertainty for an angle parameter."""
if isinstance(val, numbers.Number):
result = Angle(val * self.unit_identifier[self._str_unit.lower()][2])
elif isinstance(val, str):
result = Angle(
str2longdouble(val) * self.unit_identifier[self._str_unit.lower()][2]
)
elif hasattr(val, "unit"):
result = Angle(val.to(self.unit_identifier[self._str_unit.lower()][2]))
else:
raise ValueError(
f"Angle parameter can not accept {type(val).__name__}format."
)
return result
[docs] def str_quantity(self, quan):
"""This is a function to print out the angle parameter."""
if ":" in self._str_unit:
return quan.to_string(sep=":", precision=8)
else:
return quan.to_string(decimal=True, precision=15)
def _print_uncertainty(self, unc):
"""This is a function for printing out the uncertainty"""
if ":" not in self._str_unit:
return unc.to_string(decimal=True, precision=20)
angle_arcsec = unc.to(u.arcsec)
if self.units == u.hourangle:
# Traditionally, hourangle uncertainty is in hourangle seconds
angle_arcsec /= 15.0
return angle_arcsec.to_string(decimal=True, precision=20)
[docs] def as_ufloat(self, units=None):
"""Return the parameter as a :class:`uncertainties.ufloat`
Will cast to the specified units, or the default
If the uncertainty is not set will be returned as 0
Parameters
----------
units : astropy.units.core.Unit, optional
Units to cast the value
Returns
-------
uncertainties.ufloat
Notes
-----
Currently :class:`~uncertainties.ufloat` does not support double precision values,
so some precision may be lost.
"""
if units is None:
units = self.units
value = self.quantity.to_value(units) if self.quantity is not None else 0
error = self.uncertainty.to_value(units) if self.uncertainty is not None else 0
return ufloat(value, error)
[docs]class prefixParameter:
"""Families of parameters identified by a prefix like ``DMX_0123``.
Creating a ``prefixParameter`` is like creating a normal parameter, except that the
name should be in the format of prefix and index. For example, ``DMX_0001`` or
``F22``. Appropriate units will be inferred.
To create a prefix parameter with the same prefix but different index, just
use the :meth:`pint.models.parameter.prefixParameter.new_param` method. It will return a new ``prefixParameter`` with the
same setup but a new index. Some units and descriptions will
be changed once the index has been changed. The new parameter will not inherit the ``frozen`` status of its parent by default. In order to get the right units
and description, ``.unit_template`` and ``.description_template`` should be provided. If
not the new prefix parameter will use the same units and description with
the old one. A typical description and units template is like::
>>> description_template = lambda x: 'This is the description of parameter %d'%x
>>> unit_template = lambda x: 'second^%d'%x
Although it is best to avoid using lambda functions
Parameters
----------
parameter_type : str, optional
Example parameter class template for quantity and value setter
name : str optional
The name of the parameter. It has to be in the format of prefix + index.
value
Initial parameter value
units : str, optional
Units that the value is expressed in
unit_template : callable
The unit template for prefixed parameter
description : str, optional
Description for the parameter
description_template : callable
Description template for prefixed parameters
prefix_aliases : list of str, optional
Alias for the prefix
frozen : bool, optional
A flag specifying whether "fitters" should adjust the value of this
parameter or leave it fixed.
continuous : bool
Whether derivatives with respect to this parameter make sense.
parameter_type : str, optional
Example parameter class template for quantity and value setter
long_double : bool, optional
Set float type quantity and value in numpy long doubles.
time_scale : str, optional
Time scale for MJDParameter class.
"""
def __init__(
self,
parameter_type="float",
name=None,
value=None,
units=None,
unit_template=None,
description=None,
description_template=None,
uncertainty=None,
frozen=True,
continuous=True,
prefix_aliases=None,
long_double=False,
unit_scale=False,
scale_factor=None,
scale_threshold=None,
time_scale="utc",
**kwargs,
):
# Split prefixed name, if the name is not in the prefixed format, error
# will be raised
self.name = name
self.prefix, self.idxfmt, self.index = split_prefixed_name(name)
# Type identifier
self.type_mapping = {
"float": floatParameter,
"str": strParameter,
"bool": boolParameter,
"mjd": MJDParameter,
"angle": AngleParameter,
"pair": pairParameter,
}
self.parameter_type = parameter_type
try:
self.param_class = self.type_mapping[self.parameter_type.lower()]
except KeyError as e:
raise ValueError(f"Unknown parameter type '{parameter_type}' ") from e
# Set up other attributes in the wrapper class
self.unit_template = unit_template
self.description_template = description_template
input_units = units
input_description = description
self.prefix_aliases = [] if prefix_aliases is None else prefix_aliases
# set templates, the templates should be a named function and input is
# the index of prefix parameter.
# Set the description and units for the parameter composition.
if self.unit_template is not None:
real_units = self.unit_template(self.index)
else:
real_units = input_units
if self.description_template is not None:
real_description = self.description_template(self.index)
else:
real_description = input_description
aliases = [pa + self.idxfmt for pa in self.prefix_aliases]
self.long_double = long_double
# initiate parameter class
self.param_comp = self.param_class(
name=self.name,
value=value,
units=real_units,
description=real_description,
uncertainty=uncertainty,
frozen=frozen,
continuous=continuous,
aliases=aliases,
long_double=long_double,
time_scale=time_scale,
unit_scale=unit_scale,
scale_factor=scale_factor,
scale_threshold=scale_threshold,
)
self.is_prefix = True
self.time_scale = time_scale
@property
def repeatable(self):
return self.param_comp.repeatable
@property
def units(self):
return self.param_comp.units
@units.setter
def units(self, unt):
self.param_comp.units = unt
@property
def quantity(self):
return self.param_comp.quantity
@quantity.setter
def quantity(self, qnt):
self.param_comp.quantity = qnt
@property
def value(self):
return self.param_comp.value
@value.setter
def value(self, val):
self.param_comp.value = val
@property
def uncertainty(self):
return self.param_comp.uncertainty
@uncertainty.setter
def uncertainty(self, ucty):
self.param_comp.uncertainty = ucty
@property
def uncertainty_value(self):
return self.param_comp.uncertainty_value
@uncertainty_value.setter
def uncertainty_value(self, val):
self.param_comp.uncertainty_value = val
@property
def prior(self):
return self.param_comp.prior
@prior.setter
def prior(self, p):
self.param_comp.prior = p
@property
def aliases(self):
return self.param_comp.aliases
@aliases.setter
def aliases(self, a):
self.param_comp.aliases = a
@property
def use_alias(self):
return self.param_comp.use_alias
@use_alias.setter
def use_alias(self, a):
self.param_comp.use_alias = a
@property
def continuous(self):
return self.param_comp.continuous
@continuous.setter
def continuous(self, val):
self.param_comp.continuous = val
@property
def frozen(self):
return self.param_comp.frozen
@frozen.setter
def frozen(self, val):
self.param_comp.frozen = val
@property
def description(self):
return self.param_comp.description
@description.setter
def description(self, val):
self.param_comp.description = val
@property
def special_arg(self):
return self.param_comp.special_arg
def __repr__(self):
return self.param_comp.__repr__()
def from_parfile_line(self, line):
return self.param_comp.from_parfile_line(line)
def prior_pdf(self, value=None, logpdf=False):
return self.param_comp.prior_pdf(value, logpdf)
def str_quantity(self, quan):
return self.param_comp.str_quantity(quan)
def _print_uncertainty(self, uncertainty):
return str(uncertainty.to(self.units).value)
def name_matches(self, name):
return self.param_comp.name_matches(name)
def as_parfile_line(self, format="pint"):
return self.param_comp.as_parfile_line(format=format)
def as_latex(self):
return self.param_comp.as_latex()
def help_line(self):
return self.param_comp.help_line()
def prefix_matches(self, prefix):
return (prefix == self.prefix) or (prefix in self.prefix_aliases)
[docs] def new_param(self, index, inheritfrozen=False):
"""Get one prefix parameter with the same type.
Parameters
----------
index : int
index of prefixed parameter.
inheritfrozen : bool, optional
whether or not the parameter should inherit the "frozen" status of the base parameter
Returns
-------
A prefixed parameter with the same type of instance.
"""
new_name = self.prefix + format(index, f"0{len(self.idxfmt)}")
kws = {
key: getattr(self, key)
for key in [
"units",
"unit_template",
"description",
"description_template",
"frozen",
"continuous",
"prefix_aliases",
"long_double",
"time_scale",
"parameter_type",
]
if hasattr(self, key) and (key != "frozen" or inheritfrozen)
}
return prefixParameter(name=new_name, **kws)
[docs] def as_ufloat(self, units=None):
"""Return the parameter as a :class:`uncertainties.ufloat`
Will cast to the specified units, or the default
If the uncertainty is not set will be returned as 0
Parameters
----------
units : astropy.units.core.Unit, optional
Units to cast the value
Returns
-------
uncertainties.ufloat
"""
if units is None:
units = self.units
value = self.quantity.to_value(units) if self.quantity is not None else 0
error = self.uncertainty.to_value(units) if self.uncertainty is not None else 0
return ufloat(value, error)
[docs]class maskParameter(floatParameter):
"""Parameter that applies to a subset of TOAs.
A maskParameter applies to a subset of the TOAs, for example JUMP specifies
that their arrival times should be adjusted by the value associated with
this JUMP. The criterion is based on either one of the standard fields
(telescope, frequency, et cetera) or a flag; and the selection can be on an
exact match or on a range.
Upon creation of a maskParameter, an index part will be added, so that the
parameters can be distinguished within the
:class:`pint.models.timing_model.TimingModel` object. For example::
>>> p = maskParameter(name='JUMP', index=2, key="-fe", key_value="G430")
>>> p.name
'JUMP2'
The selection criterion can be one of the parameters ``mjd``, ``freq``,
``name``, ``tel`` representing the required columns of a ``.tim`` file, or
the name of a flag, starting with ``-``. If the selection criterion is
based on ``mjd`` or ``freq`` it is expected to be accompanied by a pair of
values that define a range; other criteria are expected to be accompanied
by a string that is matched exactly.
Parameters
----------
name : str
The name of the parameter.
index : int, optional
The index number for the prefixed parameter.
key : str, optional
The key words/flag for the selecting TOAs
key_value : list/single value optional
The value for key words/flags. Value can take one value as a flag value.
or two value as a range.
e.g. ``JUMP freq 430.0 1440.0``. or ``JUMP -fe G430``
value : float or np.longdouble, optional
Toas/phase adjust value
long_double : bool, optional
Set float type quantity and value in long double
units : str, optional
Unit for the offset value
description : str, optional
Description for the parameter
uncertainty: float or np.longdouble
uncertainty of the parameter.
frozen : bool, optional
A flag specifying whether "fitters" should adjust the value of this
parameter or leave it fixed.
continuous : bool, optional
Whether derivatives with respect to this parameter make sense.
aliases : list, optional
List of aliases for parameter name.
"""
# TODO: Is mask parameter provide some other type of parameters other then floatParameter?
def __init__(
self,
name,
index=1,
key=None,
key_value=[],
value=None,
long_double=False,
units=None,
description=None,
uncertainty=None,
frozen=True,
continuous=False,
aliases=[],
):
self.is_mask = True
# {key_name: (keyvalue parse function, keyvalue length)}
# Move this to some other places.
self.key_identifier = {
"mjd": (float, 2),
"freq": (_return_frequency_asquantity, 2),
"name": (str, 1),
"tel": (_get_observatory_name, 1),
}
if not isinstance(key_value, (list, tuple)):
key_value = [key_value]
# Check key and key value
key_value_parser = str
if key is not None:
if key.lower() in self.key_identifier:
key_info = self.key_identifier[key.lower()]
if len(key_value) != key_info[1]:
errmsg = f"key {key} takes {key_info[1]} element(s)."
raise ValueError(errmsg)
key_value_parser = key_info[0]
elif not key.startswith("-"):
raise ValueError(
"A key to a TOA flag requires a leading '-'."
" Legal keywords that don't require a leading '-' "
"are MJD, FREQ, NAME, TEL."
)
self.key = key
self.key_value = [
key_value_parser(k) for k in key_value
] # retains string format from .par file to ensure correct data type for comparison
self.key_value.sort()
self.index = index
name_param = name + str(index)
self.origin_name = name
self.prefix = self.origin_name
idx_aliases = [al + str(self.index) for al in aliases]
self.prefix_aliases = aliases
super().__init__(
name=name_param,
value=value,
units=units,
description=description,
uncertainty=uncertainty,
frozen=frozen,
continuous=continuous,
aliases=idx_aliases + aliases,
long_double=long_double,
)
# For the first mask parameter, add name to aliases for the reading
# first mask parameter from parfile.
if index == 1:
self.aliases.append(name)
self.is_prefix = True
self._parfile_name = self.origin_name
def __repr__(self):
out = f"{self.__class__.__name__}({self.name}"
if self.key is not None:
out += f" {self.key}"
if self.key_value is not None:
for kv in self.key_value:
out += f" {str(kv)}"
if self.quantity is not None:
out += f" {self.str_quantity(self.quantity)}"
else:
out += " UNSET"
return out
if self.uncertainty is not None and isinstance(self.value, numbers.Number):
out += f" +/- {str(self.uncertainty.to(self.units))}"
if self.units is not None:
out += f" ({str(self.units)})"
out += ")"
return out
@property
def repeatable(self):
return True
[docs] def name_matches(self, name):
if super().name_matches(name):
return True
elif self.index == 1:
name_idx = name + str(self.index)
return super().name_matches(name_idx)
[docs] def from_parfile_line(self, line):
"""Read mask parameter line (e.g. JUMP).
Returns
-------
bool
Whether the parfile line is meaningful to this class
Notes
-----
The accepted format::
NAME key key_value parameter_value
NAME key key_value parameter_value fit_flag
NAME key key_value parameter_value fit_flag uncertainty
NAME key key_value parameter_value uncertainty
NAME key key_value1 key_value2 parameter_value
NAME key key_value1 key_value2 parameter_value fit_flag
NAME key key_value1 key_value2 parameter_value fit_flag uncertainty
NAME key key_value1 key_value2 parameter_value uncertainty
where NAME is the name for this class as reported by ``self.name_matches``.
"""
k = line.split()
if not k:
return False
# Test that name matches
name = k[0]
if not self.name_matches(name):
return False
try:
self.key = k[1]
except IndexError as e:
raise ValueError(
"{}: No key found on timfile line {!r}".format(self.name, line)
) from e
key_value_info = self.key_identifier.get(self.key.lower(), (str, 1))
len_key_v = key_value_info[1]
if len(k) < 3 + len_key_v:
raise ValueError(
"{}: Expected at least {} entries on timfile line {!r}".format(
self.name, 3 + len_key_v, line
)
)
for ii in range(len_key_v):
if key_value_info[0] != str:
try:
kval = float(k[2 + ii])
except ValueError:
kval = k[2 + ii]
else:
kval = k[2 + ii]
if ii > len(self.key_value) - 1:
self.key_value.append(key_value_info[0](kval))
else:
self.key_value[ii] = key_value_info[0](kval)
if len(k) >= 3 + len_key_v:
self.value = k[2 + len_key_v]
if len(k) >= 4 + len_key_v:
try:
fit_flag = int(k[3 + len_key_v])
if fit_flag == 0:
self.frozen = True
ucty = 0.0
elif fit_flag == 1:
self.frozen = False
ucty = 0.0
else:
ucty = fit_flag
except ValueError:
try:
str2longdouble(k[3 + len_key_v])
ucty = k[3 + len_key_v]
except ValueError as exc:
errmsg = f"Unidentified string {k[3 + len_key_v]} in"
errmsg += f" parfile line {k}"
raise ValueError(errmsg) from exc
if len(k) >= 5 + len_key_v:
ucty = k[4 + len_key_v]
self.uncertainty = self._set_uncertainty(ucty)
return True
[docs] def as_parfile_line(self, format="pint"):
assert (
format.lower() in _parfile_formats
), "parfile format must be one of %s" % ", ".join(
[f'"{x}"' for x in _parfile_formats]
)
if self.quantity is None:
return ""
name = self.origin_name if self.use_alias is None else self.use_alias
# special cases for parameter names that change depending on format
if name == "EFAC" and format.lower() != "pint":
# change to T2EFAC for TEMPO/TEMPO2
name = "T2EFAC"
elif name == "EQUAD" and format.lower() != "pint":
# change to T2EQUAD for TEMPO/TEMPO2
name = "T2EQUAD"
line = "%-15s %s " % (name, self.key)
for kv in self.key_value:
if isinstance(kv, time.Time):
line += f"{time_to_mjd_string(kv)} "
elif isinstance(kv, u.Quantity):
line += f"{kv.value} "
else:
line += f"{kv} "
line += "%25s" % self.str_quantity(self.quantity)
if self.uncertainty is not None:
line += " %d %s" % (0 if self.frozen else 1, str(self.uncertainty_value))
elif not self.frozen:
line += " 1"
return line + "\n"
def as_latex(self):
try:
unit_latex = (
""
if self.units == "" or self.units is None
else f" ({self.units.to_string(format='latex', fraction=False)})"
)
except TypeError:
# `fraction` option is not available in old astropy versions.
unit_latex = (
""
if self.units == "" or self.units is None
else f" ({self.units.to_string(format='latex')})"
)
return (
f"{self.prefix} {self.key} {' '.join(self.key_value)}, {self.description}{unit_latex}",
self.value_as_latex(),
)
[docs] def new_param(self, index, copy_all=False):
"""Create a new but same style mask parameter"""
return (
maskParameter(
name=self.origin_name,
index=index,
key=self.key,
key_value=self.key_value,
value=self.value,
long_double=self.long_double,
units=self.units,
description=self.description,
uncertainty=self.uncertainty,
frozen=self.frozen,
continuous=self.continuous,
aliases=self.prefix_aliases,
)
if copy_all
else maskParameter(
name=self.origin_name,
index=index,
long_double=self.long_double,
units=self.units,
aliases=self.prefix_aliases,
)
)
[docs] def select_toa_mask(self, toas):
"""Select the toas that match the mask.
Parameters
----------
toas: :class:`pint.toas.TOAs`
Returns
-------
array
An array of TOA indices selected by the mask.
"""
if len(self.key_value) == 1:
if not hasattr(self, "toa_selector"):
self.toa_selector = TOASelect(is_range=False, use_hash=True)
condition = {self.name: self.key_value[0]}
elif len(self.key_value) == 2:
if not hasattr(self, "toa_selector"):
self.toa_selector = TOASelect(is_range=True, use_hash=True)
condition = {self.name: tuple(self.key_value)}
elif len(self.key_value) == 0:
return np.array([], dtype=int)
else:
raise ValueError(
f"Parameter {self.name} has more key values than expected.(Expect 1 or 2 key values)"
)
# get the table columns
# TODO Right now it is only supports mjd, freq, tel, and flagkeys,
# We need to consider some more complicated situation
key = self.key[1::] if self.key.startswith("-") else self.key
tbl = toas.table
column_match = {"mjd": "mjd_float", "freq": "freq", "tel": "obs"}
if (
self.key.lower() not in column_match
): # This only works for the one with flags.
# The flags are recomputed every time. If don't
# recompute, flags can only be added to the toa table once and then never update,
# making it impossible to add additional jump parameters after the par file is read in (pintk)
flag_col = [x.get(key, None) for x in tbl["flags"]]
tbl[key] = flag_col
col = tbl[key]
else:
col = tbl[column_match[key.lower()]]
select_idx = self.toa_selector.get_select_index(condition, col)
return select_idx[self.name]
[docs] def compare_key_value(self, other_param):
"""Compare if the key and value are the same with the other parameter.
Parameters
----------
other_param: maskParameter
The parameter to compare.
Returns
-------
bool:
If the key and value are the same, return True, otherwise False.
Raises
------
ValueError:
If the parameter to compare does not have 'key' or 'key_value'.
"""
if not hasattr(other_param, "key") and not hasattr(other_param, "key_value"):
raise ValueError("Parameter to compare does not have `key` or `key_value`.")
if self.key != other_param.key:
return False
return self.key_value == other_param.key_value
[docs]class pairParameter(floatParameter):
"""Parameter type for parameters that need two input floats.
One example are WAVE parameters.
Parameters
----------
name : str
The name of the parameter.
value : astropy Time, str, float in mjd, str in mjd.
The input parameter MJD value.
description : str, optional
A short description of what this parameter means.
uncertainty : number
Current uncertainty of the value.
frozen : bool, optional
A flag specifying whether "fitters" should adjust the value of this
parameter or leave it fixed.
continuous : bool, optional, default True
A flag specifying whether phase derivatives with respect to this
parameter exist.
aliases : str, optional
List of aliases for the current parameter
"""
def __init__(
self,
name=None,
index=None,
value=None,
long_double=False,
units=None,
description=None,
uncertainty=None,
frozen=True,
continuous=False,
aliases=[],
**kwargs,
):
self.index = index
name_param = name
self.origin_name = name
self.prefix = self.origin_name
self.prefix_aliases = aliases
super().__init__(
name=name_param,
value=value,
units=units,
description=description,
uncertainty=uncertainty,
frozen=frozen,
continuous=continuous,
aliases=aliases,
long_double=long_double,
**kwargs,
)
self.is_prefix = True
[docs] def name_matches(self, name):
if super().name_matches(name):
return True
name_idx = name + str(self.index)
return super().name_matches(name_idx)
[docs] def from_parfile_line(self, line):
"""Read mask parameter line (e.g. JUMP).
Notes
-----
The accepted format:
NAME value_a value_b
"""
try:
k = line.split()
name = k[0].upper()
except IndexError:
return False
# Test that name matches
if not self.name_matches(name):
return False
try:
self.value = (k[1], k[2])
except IndexError:
return False
if name != self.name:
# FIXME: what about prefix/mask parameters?
self.use_alias = name
return True
[docs] def as_parfile_line(self, format="pint"):
quantity = self.quantity
if self.quantity is None:
return ""
name = self.name if self.use_alias is None else self.use_alias
line = "%-15s " % name
line += "%25s" % self.str_quantity(quantity[0])
line += " %25s" % self.str_quantity(quantity[1])
return line + "\n"
[docs] def new_param(self, index):
"""Create a new but same style mask parameter."""
return pairParameter(
name=self.origin_name,
index=index,
long_double=self.long_double,
units=self.units,
aliases=self.prefix_aliases,
)
def _set_quantity(self, vals):
vals = [floatParameter._set_quantity(self, val) for val in vals]
return vals
def _set_uncertainty(self, vals):
return self._set_quantity(vals)
@property
def value(self):
"""Return the pure value of a parameter.
This value will associate with parameter default value, which is .units attribute.
"""
return None if self._quantity is None else self._get_value(self._quantity)
@value.setter
def value(self, val):
"""Method to set .value.
Setting .value attribute will change the .quantity attribute other than .value attribute.
"""
if val is None:
if (
not isinstance(self.quantity, (str, bool))
and self._quantity is not None
):
raise ValueError(
"Setting .value to None will lose the parameter value."
)
else:
self.value = val
self._quantity = self._set_quantity(val)
[docs] def str_quantity(self, quan):
"""Return quantity as a string."""
try:
# Maybe it's a singleton quantity
return floatParameter.str_quantity(self, quan)
except AttributeError:
# Not a quantity, let's hope it's a list of length two?
if len(quan) != 2:
raise ValueError(f"Don't know how to print this as a pair: {quan}")
v0 = quan[0].to(self.units).value
v1 = quan[1].to(self.units).value
if self._long_double:
if not isinstance(v0, np.longdouble):
raise TypeError(
f"Parameter {self} is supposed to contain long doubles but contains a float"
)
if not isinstance(v1, np.longdouble):
raise TypeError(
f"Parameter {self} is supposed to contain long doubles but contains a float"
)
quan0 = str(v0)
quan1 = str(v1)
return f"{quan0} {quan1}"
[docs]class funcParameter(floatParameter):
"""Parameter defined as a read-only function operating on other parameters that returns a float or long double value.
Can access the result of the function through the ``.quantity`` attribute,
and the value without units through the ``.value`` attribute.
On its own this parameter will not be useful,
but when inserted into a :class:`pint.models.timing_model.Component` object
it can operate on any parameters within that component or others in the same
:class:`pint.models.timing_model.TimingModel`.
Parameters
----------
name : str
The name of the parameter.
func : function
Returns the desired value
params : iterable
List or tuple of parameter names.
Each can optionally also be a tuple including the attribute to access (default is ``quantity``)
units : str or astropy.units.Quantity
Parameter default unit. Parameter .value and .uncertainty_value attribute
will associate with the default units. If unit is dimensionless, use
"''" as its unit.
description : str, optional
A short description of what this parameter means.
inpar : bool, optional
Whether to include in par-file printouts, or to comment out
long_double : bool, optional, default False
A flag specifying whether value is float or long double.
aliases : list, optional
An optional list of strings specifying alternate names that can also
be accepted for this parameter.
Examples
-------
>>> import pint.models.parameter
>>> p = pint.models.parameter.funcParameter(
name="AGE",
description="Spindown age",
params=("F0", "F1"),
func=lambda f0, f1: -f0 / 2 / f1,
units="yr",
)
>>> m.components["Spindown"].add_param(p)
>>> print(m.AGE)
>>> import pint.models.parameter
>>> import pint.derived_quantities
>>> p2 = pint.models.parameter.funcParameter(
name="PSREDOT",
description="Spindown luminosity",
params=("F0", "F1"),
func=pint.derived_quantities.pulsar_edot,
units="erg/s",
)
>>> m.components["Spindown"].add_param(p2)
>>> print(m.PSREDOT)
Notes
-----
Defining functions through ``lambda`` functions may result in unpickleable models
Future versions may include derivative functions to calculate uncertainties.
"""
def __init__(
self,
name=None,
description=None,
func=None,
params=None,
units=None,
inpar=False,
long_double=False,
unit_scale=False,
scale_factor=None,
scale_threshold=None,
aliases=None,
**kwargs,
):
self.paramType = "funcParameter"
self.name = name
self.description = description
self._func = func
if self._func.__name__ == "<lambda>":
log.warning(
f"May not be able to pickle function {self._func} in definition of funcParameter '{name}': use a named function if this is required"
)
self._set_params(params)
self.units = "" if units is None else units
self.long_double = long_double
self.scale_factor = scale_factor
self.scale_threshold = scale_threshold
self._unit_scale = False
self.unit_scale = unit_scale
self.inpar = inpar
self.aliases = [] if aliases is None else aliases
for arg in kwargs:
setattr(self, arg, kwargs[arg])
# these should be fixed
self.uncertainty = None
self.frozen = True
self.use_alias = None
self.is_prefix = False
self.continuous = True
# for each parameter determine how many levels of parentage to check
self._parentlevel = []
self._parent = None
def _set_params(self, params):
"""Split the input parameter list into tuples of parameter and attribute
Parameters
----------
params : : iterable
List or tuple of parameter names.
Each can optionally also be a tuple including the attribute to access (default is ``quantity``)
"""
self._params = []
self._attrs = []
for p in params:
if isinstance(p, str):
self._params.append(p)
# assume quantity
self._attrs.append("quantity")
else:
self._params.append(p[0])
self._attrs.append(p[1])
def _get_parentage(self, max_level=2):
"""Determine parentage level for each parameter
Parameters
----------
max_level : int, optional
Maximum parentage level to search
Raises
------
AttributeError :
If the parameter cannot be located in any parent object
"""
if self._parent is None:
return
self._parentlevel = []
for i, p in enumerate(self._params):
parent = self._parent
for _ in range(max_level):
if hasattr(parent, p):
self._parentlevel.append(parent)
break
if hasattr(parent, "_parent"):
parent = getattr(parent, "_parent")
else:
break
if len(self._parentlevel) < i + 1:
raise AttributeError(
f"Cannot find parameter '{p}' in parent objects of parameter '{self.name}'"
)
def _get(self):
"""Run the function and return the result
Returns
-------
astropy.units.Quantity or None
If any input value is ``None`` or if the parentage is not yet specified, will return ``None``
Otherwise will return the result of the function
"""
if self._parent is None:
return None
if self._parentlevel == []:
self._get_parentage()
args = []
for l, p, a in zip(self._parentlevel, self._params, self._attrs):
args.append(getattr(getattr(l, p), a))
if args[-1] is None:
return None
return self._func(*args)
@property
def quantity(self):
"""The result of the function"""
return self._get()
@quantity.setter
def quantity(self, value):
raise AttributeError("Cannot set funcParameter")
@property
def value(self):
"""The result of the function without units."""
return self._get().value if self._get() is not None else None
@value.setter
def value(self, value):
raise AttributeError("Cannot set funcParameter")
@property
def params(self):
"""Return a list of tuples of parameter names and attributes"""
return list(zip(self._params, self._attrs))
@params.setter
def params(self, params):
self._set_params(params)
[docs] def from_parfile_line(self, line):
"""Ignore reading from par file
For :class:`~pint.models.parameter.funcParameter` ,
it is for information only so is ignored on reading
"""
return True
[docs] def as_parfile_line(self, format="pint"):
return (
super().as_parfile_line(format=format)
if self.inpar
else f"# {super().as_parfile_line(format=format)}"
)