Skip to content

parfile.py: add ability to add aliases to PulsarParameters

Karl Wette requested to merge karl-wette/cwinpy:PulsarParameters-aliases into master

This MR adds an "alias" feature to the Python PulsarParameters class. An alias gives another way of setting a "real" parameter of the underlying SWIG-wrapped lalpulsar.PulsarParameters struct.

As a practical example, the MR adds the following alias for the braking index n = f \ddot{f} / \dot{f}^2:

add_alias("ALIAS_N", lambda pp: pp["F0"] * pp["F2"] / pp["F1"]**2, "F2", lambda n, pp: n * pp["F1"]**2 / pp["F0"])

Parameters are:

  1. The alias parameter name, which must start with ALIAS_ so that alias parameters can be easily distinguished.
  2. A function which computes the values of ALIAS_ from a Python PulsarParameters class pp.
  3. The real parameter the alias maps to; here the braking index is considered an alias of the 2nd spindown parameter.
  4. A function which compute the value of the real parameter (i.e. 2nd spindown) in terms of the alias parameter (i.e. braking index) and other parameters in a Python PulsarParameters class pp.

The motivation of this MR is a CW PE project where we're trying to sample over braking index. So far we've been doing this by sampling over 2nd spindown and setting the braking index as a constraint, which works when signals are relatively strong. For weaker signals, however, the breaking index constraint 3 < n < 5 is so strict that very few 2nd spindown samples satisfy it, which leads to long runtimes, large memory usage (not sure why) or even failure to converge (in the sense of small dlogz) at all within a few days running. By using the braking index alias to sample directly from 3 < n < 5 and convert that into the right 2nd spindown, we can get converged, reliable results in a reasonable time.

New functions in parfile.py are:

  • add_alias(): Adds a new alias.
  • is_alias_param(): Convenience function which checks if a parameter name represents an alias (i.e. if it starts with ALIAS_).
  • get_real_param_from_alias(): Convenience function which returns the real parameter name of an alias (or just the name if it's not an alias).

Modifications to existing code:

  • parfile.py:
    • PulsarParameters.__getitem__(): If an alias parameter is being accessed, compute its value using the 1st function passed to add_alias().
    • PulsarParameters.__setitem__(): If an alias parameter is being set, instead compute the real parameter value using the 2nd function passed to add_alias() and set the real parameter to that value.
  • likelihood.py:
    • TargetedPulsarLikelihood.__init__(): Recognise alias parameters in the for key in self.priors loop. It is currently assumed that any alias parameter will include sampling over the phase, and so the self.include_phase = True block of the if statement inside this loop is triggered. get_real_param_from_alias() is used to check the underlying real parameter in case self.include_binary, etc. need to be set.
    • TargetedPulsarLikelihood.log_likelihood(): A subtlety with the aliases is that, when assigning values to a PulsarParameters class from elsewhere (e.g. samples from Bilby) the real parameters must be set first, then any aliases, since the aliases depend on the real parameters. To address this, the loop over pname, pval is sorted by: a) is_alias_param(pname), which sorts False (real parameters) before True (aliases), and then b) alphabetically. (If one needed to have alias-of-alias parameters, one could name them so that they're set correctly in alphabetical order; I've not tested this works though.) This function is the only place I could find where a PulsarParameters class is initialised in this way, but I may have missed other ones.

Tests: I've not written a test of this feature specifically, although I have tested that it works for our project. I'd be happy to write a test if needed (preferably by adapting an existing test).

Merge request reports