parfile.py: add ability to add aliases to PulsarParameters
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:
- The alias parameter name, which must start with
ALIAS_
so that alias parameters can be easily distinguished. - A function which computes the values of
ALIAS_
from a PythonPulsarParameters
classpp
. - The real parameter the alias maps to; here the braking index is considered an alias of the 2nd spindown parameter.
- 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
classpp
.
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 withALIAS_
). -
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 toadd_alias()
. -
PulsarParameters.__setitem__()
: If an alias parameter is being set, instead compute the real parameter value using the 2nd function passed toadd_alias()
and set the real parameter to that value.
-
-
likelihood.py
:-
TargetedPulsarLikelihood.__init__()
: Recognise alias parameters in thefor key in self.priors
loop. It is currently assumed that any alias parameter will include sampling over the phase, and so theself.include_phase = True
block of theif
statement inside this loop is triggered.get_real_param_from_alias()
is used to check the underlying real parameter in caseself.include_binary
, etc. need to be set. -
TargetedPulsarLikelihood.log_likelihood()
: A subtlety with the aliases is that, when assigning values to aPulsarParameters
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 overpname, pval
is sorted by: a)is_alias_param(pname)
, which sortsFalse
(real parameters) beforeTrue
(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 aPulsarParameters
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).