"""Command-line interface for the ``ell1fit`` script.
Argument parsing and nothing else: every option here maps onto a parameter of
:func:`ell1fit.pipeline.ell1fit`, which is where the work happens and where the
behaviour is documented.
"""
import argparse
from .likelihoods import pletsch_clarke_likelihood
from .likelihoods import rayleigh_as_likelihood
from .logging import configure_logging
from .pipeline import ell1fit
from .plotting import add_figure_format_argument, set_figure_format
from .priors import parse_prior_specs
__all__ = [
"main",
]
[docs]
def main(args=None):
"""Main function called by the `ell1fit` script"""
configure_logging()
description = "Fit an ELL1 model and frequency derivatives to an X-ray pulsar observation."
parser = argparse.ArgumentParser(description=description)
parser.add_argument("files", help="List of files", nargs="+")
parser.add_argument(
"-p",
"--parfile",
type=str,
nargs="+",
default=None,
help=(
"Input parameter files, one per event file. Must contain a simple ELL1 binary model, "
"with no orbital derivatives, and a number of spin derivatives (F0, F1, ...). "
"All other models will be ignored."
),
)
parser.add_argument("-o", "--outroot", type=str, default=None, help="Root of output file names")
parser.add_argument(
"-N",
"--nharm",
type=int,
help="Number of harmonics to describe the pulse profile",
default=1,
)
parser.add_argument(
"--deorb-tolerance",
type=float,
help="Tolerance of deorbit operation, in seconds",
default=1e-8,
)
parser.add_argument(
"-E",
"--erange",
nargs=2,
type=float,
help="Energy range",
default=None,
)
parser.add_argument(
"--nsteps",
type=int,
help="Maximum number of MCMC steps",
default=100_000,
)
parser.add_argument(
"-P",
"--parameters",
type=str,
help="Comma-separated list of parameters to fit",
default="F0,F1",
)
parser.add_argument(
"--likelihood",
type=str,
help="Can be PC (Pletsch & Clarke, default) or Rayleigh",
default="PC",
)
parser.add_argument(
"--minimize-first",
action="store_true",
default=False,
help="Minimize first, then MCMC (don't trust the solution in the par file)",
)
parser.add_argument(
"--use-weight",
action="store_true",
default=False,
help="Use pulse energy dependence of profile as weight",
)
parser.add_argument(
"--use-pi",
action="store_true",
default=False,
help=(
"Base pulsed-fraction weighting (--use-weight) on PI channels instead of "
"calibrated energy. No effect without --use-weight."
),
)
parser.add_argument(
"--template-iterations",
type=int,
default=1,
help=(
"Maximum passes of iterative template refinement. Each pass refolds "
"with the current best solution and rebuilds the pulse template, so "
"the template is not the one smeared by errors in the input parfile. "
"1 (default) disables refinement."
),
)
parser.add_argument("--ignore-uncertainties", action="store_true", default=False)
parser.add_argument(
"--prior",
action="append",
default=None,
metavar="NAME:SHAPE:ARGS",
help=(
"Override the prior on one parameter; repeat for more. SHAPE is "
"uniform or normal. ARGS is either two numbers -- bounds for "
"uniform, mean and sigma for normal -- or +-WIDTH to centre the "
"prior on the parameter's input value. Values are in the same "
"units as the parfile. A bare spin-parameter name covers every "
"file (F1 means F1_0, F1_1, ...). Examples: "
"--prior F1:uniform:-1e-10,1e-10 --prior TASC:normal:+-1e-6"
),
)
parser.add_argument(
"--sampler",
type=str,
choices=["emcee", "nuts", "nested"],
default="emcee",
help=(
"Posterior-exploration backend. 'emcee' (default) is always available. "
"'nuts' and 'nested' need extra dependencies: pip install ell1fit[nuts] "
"or ell1fit[nested]. Only 'nested' produces log_evidence, which is what "
"a model-comparison Bayes factor needs."
),
)
parser.add_argument(
"--nlive",
type=int,
default=1000,
help=(
"Live points for --sampler nested. Ignored otherwise. Nested sampling "
"can miss a narrow mode and still report a confident, wrong evidence, "
"so raise this well past the default on anything but a small fit."
),
)
parser.add_argument(
"--dlogz",
type=float,
default=0.1,
help="Evidence-remaining stopping criterion for --sampler nested. Ignored otherwise.",
)
parser.add_argument(
"--workers",
type=int,
default=0,
help=(
"Worker processes for --sampler nested. Ignored otherwise. 0 (the "
"default) runs single-process; the prior transform always does, "
"regardless of this setting -- only likelihood evaluations spread "
"across workers."
),
)
add_figure_format_argument(parser)
args = parser.parse_args(args)
# Parsed here rather than deep in the pipeline so a typo is reported as a
# usage error in the first second, not after the events are loaded.
try:
prior_specs = parse_prior_specs(args.prior)
except ValueError as error:
parser.error(str(error))
set_figure_format(args.figure_format)
files = args.files
parfiles = args.parfile
like = pletsch_clarke_likelihood
if args.likelihood.lower() == "rayleigh":
like = rayleigh_as_likelihood
ell1fit(
files,
parfiles,
nsteps=args.nsteps,
nharm=args.nharm,
tolerance=args.deorb_tolerance,
energy_range=args.erange,
fit_parameters=args.parameters.split(","),
minimize_first=args.minimize_first,
general_outroot=args.outroot,
likelihood_func=like,
use_weight=args.use_weight,
use_pi=args.use_pi,
ignore_uncertainties=args.ignore_uncertainties,
template_iterations=args.template_iterations,
priors=prior_specs,
sampler=args.sampler,
nlive=args.nlive,
dlogz=args.dlogz,
workers=args.workers,
)
if __name__ == "__main__": # pragma: no cover
# Without this, "python -m ell1fit.cli ..." silently does nothing at all.
main()