"""Tools for building chi-squared grids."""
import concurrent.futures
import copy
import multiprocessing
import subprocess
import sys
from typing import Dict, List, Optional, Tuple
import numpy as np
from loguru import logger as log
try:
from tqdm import tqdm
except ModuleNotFoundError:
tqdm = None
from astropy import units as u
from astropy.utils.console import ProgressBar
from pint import fitter
__all__ = ["doonefit", "grid_chisq", "grid_chisq_derived"]
[docs]def hostinfo() -> str:
return subprocess.check_output("uname -a", shell=True)
[docs]def set_log(logger_: "loguru._logger.Logger") -> None:
global log
log = logger_
[docs]class WrappedFitter:
"""Worker class to compute one fit with specified parameters fixed but passing other parameters to fit_toas()"""
def __init__(self, ftr: fitter.Fitter, **fitargs) -> None:
"""Worker class to computes one fit with specified parameters fixed
Parameters
----------
ftr : pint.fitter.Fitter
fitargs :
additional arguments pass to fit_toas()
"""
self.ftr = ftr
self.fitargs = fitargs
[docs] def doonefit(
self, parnames: List[str], parvalues: List[str], extraparnames: List[str] = []
) -> Tuple[float, List[float]]:
"""Worker process that computes one fit with specified parameters fixed
Parameters
----------
parnames : list
Names of the parameters to grid over
parvalues : list
List of parameter values to grid over (each should be 1D array of astropy.units.Quantity)
extraparnames : list, optional
Names of other parameters to return
Returns
-------
chi2 : float
extraparvalues : list
"""
# Make a full copy of the fitter to work with
myftr = copy.deepcopy(self.ftr)
# copy the log to all imported modules
# this makes them respect the logger settings
for m in sys.modules:
if m.startswith("pint") and hasattr(sys.modules[m], "log"):
setattr(sys.modules[m], "log", log)
parstrings = []
for parname, parvalue in zip(parnames, parvalues):
# Freeze the params we are going to grid over and set their values
# All other unfrozen parameters will be fitted for at each grid point
getattr(myftr.model, parname).frozen = True
getattr(myftr.model, parname).quantity = parvalue
parstrings.append(f"{parname} = {parvalue}")
log.debug(f"Running for {','.join(parstrings)} on {hostinfo()}")
try:
myftr.fit_toas(**self.fitargs)
chi2 = myftr.resids.chi2
except (fitter.InvalidModelParameters, fitter.StepProblem):
log.warning(
f"Fit may not be converged for {','.join(parstrings)}, but returning anyway"
)
chi2 = myftr.resids.chi2
except fitter.MaxiterReached:
log.warning(
f"Max iterations reached for {','.join(parstrings)}: returning NaN"
)
chi2 = np.NaN
except Exception as e:
log.warning(
f"Unexpected exception {e} for {','.join(parstrings)}: returning NaN"
)
chi2 = np.NaN
log.debug(
f"Computed chi^2={myftr.resids.chi2} for {','.join(parstrings)} on {hostinfo()}"
)
extraparvalues = []
for extrapar in extraparnames:
extraparvalues.append(getattr(myftr.model, extrapar).quantity)
return chi2, extraparvalues
[docs]def doonefit(
ftr: fitter.Fitter,
parnames: List[str],
parvalues: List[u.Quantity],
extraparnames: List[str] = [],
) -> Tuple[float, List[float]]:
"""Worker process that computes one fit with specified parameters fixed
Parameters
----------
ftr : pint.fitter.Fitter
parnames : list
Names of the parameters to grid over
parvalues : list
List of parameter values to grid over (each should be 1D array of astropy.units.Quantity)
extraparnames : list, optional
Names of other parameters to return
Returns
-------
chi2 : float
extraparvalues : list
"""
# Make a full copy of the fitter to work with
myftr = copy.deepcopy(ftr)
parstrings = []
for parname, parvalue in zip(parnames, parvalues):
# Freeze the params we are going to grid over and set their values
# All other unfrozen parameters will be fitted for at each grid point
getattr(myftr.model, parname).frozen = True
getattr(myftr.model, parname).quantity = parvalue
parstrings.append(f"{parname} = {parvalue}")
log.debug(f"Running for {','.join(parstrings)} on {hostinfo()} at {log.name}")
try:
myftr.fit_toas()
except (fitter.InvalidModelParameters, fitter.StepProblem):
log.warning(
f"Fit may not be converged for {','.join(parstrings)}, but returning anyway"
)
except fitter.MaxiterReached:
log.warning(f"Max iterations reached for {','.join(parstrings)}: returning NaN")
return np.NaN
log.debug(
f"Computed chi^2={myftr.resids.chi2} for {','.join(parstrings)} on {hostinfo()}"
)
extraparvalues = []
for extrapar in extraparnames:
extraparvalues.append(getattr(myftr.model, extrapar).quantity)
return myftr.resids.chi2, extraparvalues
[docs]def grid_chisq(
ftr: fitter.Fitter,
parnames: List[str],
parvalues: List[u.Quantity],
extraparnames: List[str] = [],
executor: Optional[concurrent.futures.Executor] = None,
ncpu: Optional[int] = None,
chunksize: int = 1,
printprogress: bool = True,
**fitargs,
) -> Tuple[np.ndarray, Dict[str, np.ndarray]]:
"""Compute chisq over a grid of parameters
Parameters
----------
ftr : pint.fitter.Fitter
The base fitter to use.
parnames : list
Names of the parameters to grid over
parvalues : list
List of parameter values to grid over (each should be 1D array of :class:`astropy.units.Quantity`)
extraparnames : list, optional
Names of other parameters to return
executor : concurrent.futures.Executor or None, optional
Executor object to run multiple processes in parallel
If None, will use default :class:`concurrent.futures.ProcessPoolExecutor`, unless overridden by ``ncpu=1``
ncpu : int, optional
If an existing Executor is not supplied, one will be created with this number of workers.
If 1, will run single-processor version
If None, will use :func:`multiprocessing.cpu_count`
chunksize : int
Size of the chunks for :class:`concurrent.futures.ProcessPoolExecutor` parallel execution.
Ignored for :class:`concurrent.futures.ThreadPoolExecutor`
printprogress : bool, optional
Print indications of progress (requires :mod:`tqdm` for `ncpu`>1)
fitargs :
additional arguments pass to fit_toas()
Returns
-------
np.ndarray : array of chisq values
extraout : dict of np.ndarray
Parameter values computed at each grid point for `extraparnames`
Examples
-------
>>> import astropy.units as u
>>> import numpy as np
>>> import pint.config
>>> import pint.gridutils
>>> from pint.fitter import WLSFitter
>>> from pint.models.model_builder import get_model, get_model_and_toas
>>> # Load in a basic dataset
>>> parfile = pint.config.examplefile("NGC6440E.par")
>>> timfile = pint.config.examplefile("NGC6440E.tim")
>>> m, t = get_model_and_toas(parfile, timfile)
>>> f = WLSFitter(t, m)
>>> # find the best-fit
>>> f.fit_toas()
>>> bestfit = f.resids.chi2
>>> # We'll do something like 3-sigma around the best-fit values of F0
>>> F0 = np.linspace(f.model.F0.quantity - 3 * f.model.F0.uncertainty,f.model.F0.quantity + 3 * f.model.F0.uncertainty,25)
>>> chi2_F0,_ = pint.gridutils.grid_chisq(f, ("F0",), (F0,))
A 2D example with a plot:
>>> import astropy.units as u
>>> import numpy as np
>>> import pint.gridutils
>>> from pint.fitter import WLSFitter
>>> from pint.models.model_builder import get_model_and_toas
>>> import scipy.stats
>>> import matplotlib.pyplot as plt
>>> # Load in a basic dataset
>>> parfile = pint.config.examplefile("NGC6440E.par")
>>> timfile = pint.config.examplefile("NGC6440E.tim")
>>> m, t = get_model_and_toas(parfile, timfile)
>>> f = WLSFitter(t, m)
>>> # find the best-fit
>>> f.fit_toas()
>>> bestfit = f.resids.chi2
>>> F0 = np.linspace(f.model.F0.quantity - 3 * f.model.F0.uncertainty,f.model.F0.quantity + 3 * f.model.F0.uncertainty,25)
>>> F1 = np.linspace(f.model.F1.quantity - 3 * f.model.F1.uncertainty,f.model.F1.quantity + 3 * f.model.F1.uncertainty,27)
>>> chi2grid = pint.gridutils.grid_chisq(f, ("F0", "F1"), (F0, F1))[0]
>>> # 1, 2, and 3 sigma confidence limits
>>> nsigma = np.arange(1, 4)
>>> # these are the CDFs going from -infinity to nsigma. So subtract away 0.5 and double for the 2-sided values
>>> CIs = (scipy.stats.norm().cdf(nsigma) - 0.5) * 2
>>> print(f"Confidence intervals for {nsigma} sigma: {CIs}")
>>> # chi^2 random variable for 2 parameters
>>> rv = scipy.stats.chi2(2)
>>> # the ppf = Percent point function is the inverse of the CDF
>>> contour_levels = rv.ppf(CIs)
>>> fig, ax = plt.subplots(figsize=(16, 9))
>>> # just plot the values offset from the best-fit values
>>> twod = ax.contour(F0 - f.model.F0.quantity,F1 - f.model.F1.quantity,chi2grid - bestfit,levels=contour_levels,colors="b")
>>> ax.errorbar(0, 0, xerr=f.model.F0.uncertainty.value, yerr=f.model.F1.uncertainty.value, fmt="ro")
>>> ax.set_xlabel("$\Delta F_0$ (Hz)", fontsize=24)
>>> ax.set_ylabel("$\Delta F_1$ (Hz/s)", fontsize=24)
>>> plt.show()
Notes
-----
By default, it will create :class:`~concurrent.futures.ProcessPoolExecutor`
with ``max_workers`` equal to the desired number of cpus.
However, if you are running this as a script you may need something like::
import multiprocessing
if __name__ == "__main__":
multiprocessing.freeze_support()
...
grid_chisq(...)
If an instantiated :class:`~concurrent.futures.Executor` is passed instead, it will be used as-is.
The behavior for different combinations of `executor` and `ncpu` is:
+-----------------+--------+------------------------+
| `executor` | `ncpu` | result |
+=================+========+========================+
| existing object | any | uses existing executor |
+-----------------+--------+------------------------+
| None | 1 | uses single-processor |
+-----------------+--------+------------------------+
| None | None | creates default |
| | | executor with |
| | | ``cpu_count`` workers |
+-----------------+--------+------------------------+
| None | >1 | creates default |
| | | executor with desired |
| | | number of workers |
+-----------------+--------+------------------------+
Other ``Executors`` can be found for different computing environments:
* [1]_ for MPI
* [2]_ for SLURM or Condor
.. [1] https://mpi4py.readthedocs.io/en/stable/mpi4py.futures.html#mpipoolexecutor
.. [2] https://github.com/sampsyo/clusterfutures
"""
# Save the current model so we can tweak it for gridding, then restore it at the end
savemod = ftr.model
gridmod = copy.deepcopy(ftr.model)
ftr.model = gridmod
# Freeze the params we are going to grid over
for parname in parnames:
getattr(ftr.model, parname).frozen = True
wftr = WrappedFitter(ftr, **fitargs)
if isinstance(executor, concurrent.futures.Executor):
# the executor has already been created
executor = executor
elif executor is None and (ncpu is None or ncpu > 1):
# make the default type of Executor
if ncpu is None:
ncpu = multiprocessing.cpu_count()
executor = concurrent.futures.ProcessPoolExecutor(
max_workers=ncpu, initializer=set_log, initargs=(log,)
)
# All other unfrozen parameters will be fitted for at each grid point
out = np.meshgrid(*parvalues)
chi2 = np.zeros(out[0].shape)
extraout = {}
for extrapar in extraparnames:
extraout[extrapar] = (
np.zeros(out[0].shape, dtype=getattr(ftr.model, extrapar).quantity.dtype)
* getattr(ftr.model, extrapar).quantity.unit
)
# at this point, if the executor is None then run single-processor version
if executor is not None:
with executor as e:
if printprogress and tqdm is not None:
result = list(
tqdm(
e.map(
wftr.doonefit,
(parnames,) * len(out[0].flatten()),
list(zip(*[x.flatten() for x in out])),
(extraparnames,) * len(out[0].flatten()),
chunksize=chunksize,
),
total=len(out[0].flatten()),
ascii=True,
)
)
else:
result = e.map(
wftr.doonefit,
(parnames,) * len(out[0].flatten()),
list(zip(*[x.flatten() for x in out])),
(extraparnames,) * len(out[0].flatten()),
chunksize=chunksize,
)
it = np.ndindex(chi2.shape)
for i, r in zip(it, result):
chi2[i] = r[0]
for extrapar, extraparvalue in zip(extraparnames, r[1]):
extraout[extrapar][i] = extraparvalue
else:
indices = list(np.ndindex(out[0].shape))
if printprogress:
if tqdm is not None:
indices = tqdm(indices, ascii=True)
else:
indices = ProgressBar(indices)
for i in indices:
for parnum, parname in enumerate(parnames):
getattr(ftr.model, parname).quantity = out[parnum][i]
ftr.fit_toas(**fitargs)
chi2[i] = ftr.resids.chi2
for extrapar in extraparnames:
extraout[extrapar] = getattr(ftr.model, extrapar).quantity
# Restore saved model
ftr.model = savemod
return chi2, extraout
[docs]def grid_chisq_derived(
ftr: fitter.Fitter,
parnames: List[str],
parfuncs: List[callable],
gridvalues: List[np.ndarray],
extraparnames: List[str] = [],
executor: Optional[concurrent.futures.Executor] = None,
ncpu: Optional[int] = None,
chunksize: int = 1,
printprogress: bool = True,
**fitargs,
) -> Tuple[np.ndarray, List[np.ndarray], Dict[str, np.ndarray]]:
"""Compute chisq over a grid of derived parameters
Parameters
----------
ftr : pint.fitter.Fitter
The base fitter to use.
parnames : list
Names of the parameters (available in `ftr`) to grid over
parfuncs : list
List of functions to convert `gridvalues` to quantities accessed through `parnames`
gridvalues : list
List of underlying grid values to grid over (each should be 1D array of astropy.units.Quantity)
extraparnames : list, optional
Names of other parameters to return
executor : concurrent.futures.Executor or None, optional
Executor object to run multiple processes in parallel
If None, will use default :class:`concurrent.futures.ProcessPoolExecutor`, unless overridden by ``ncpu=1``
ncpu : int, optional
If an existing Executor is not supplied, one will be created with this number of workers.
If 1, will run single-processor version
If None, will use :func:`multiprocessing.cpu_count`
chunksize : int
Size of the chunks for :class:`concurrent.futures.ProcessPoolExecutor` parallel execution.
Ignored for :class:`concurrent.futures.ThreadPoolExecutor`
printprogress : bool, optional
Print indications of progress (requires :mod:`tqdm` for `ncpu`>1)
fitargs :
additional arguments pass to fit_toas()
Returns
-------
np.ndarray : array of chisq values
parvalues : list of np.ndarray
Parameter values computed from `gridvalues` and `parfuncs`
extraout : dict of np.ndarray
Parameter values computed at each grid point for `extraparnames`
Example
-------
>>> import astropy.units as u
>>> import numpy as np
>>> import pint.config
>>> import pint.gridutils
>>> from pint.fitter import WLSFitter
>>> from pint.models.model_builder import get_model, get_model_and_toas
# Load in a basic dataset
>>> parfile = pint.config.examplefile("NGC6440E.par")
>>> timfile = pint.config.examplefile("NGC6440E.tim")
>>> m, t = get_model_and_toas(parfile, timfile)
>>> f = WLSFitter(t, m)
# find the best-fit
>>> f.fit_toas()
>>> bestfit = f.resids.chi2
# do a grid for F0 and tau
>>> F0 = np.linspace(f.model.F0.quantity - 3 * f.model.F0.uncertainty,f.model.F0.quantity + 3 * f.model.F0.uncertainty,15,)
>>> tau = np.linspace(8.1, 8.3, 13) * 100 * u.Myr
>>> chi2grid_tau, params = pint.gridutils.grid_chisq_derived(f,("F0", "F1"),(lambda x, y: x, lambda x, y: -x / 2 / y),(F0, tau))
Notes
-----
By default, it will create :class:`~concurrent.futures.ProcessPoolExecutor`
with ``max_workers`` equal to the desired number of cpus.
However, if you are running this as a script you may need something like::
import multiprocessing
if __name__ == "__main__":
multiprocessing.freeze_support()
...
grid_chisq_derived(...)
If an instantiated :class:`~concurrent.futures.Executor` is passed instead, it will be used as-is.
The behavior for different combinations of `executor` and `ncpu` is:
+-----------------+--------+------------------------+
| `executor` | `ncpu` | result |
+=================+========+========================+
| existing object | any | uses existing executor |
+-----------------+--------+------------------------+
| None | 1 | uses single-processor |
+-----------------+--------+------------------------+
| None | None | creates default |
| | | executor with |
| | | ``cpu_count`` workers |
+-----------------+--------+------------------------+
| None | >1 | creates default |
| | | executor with desired |
| | | number of workers |
+-----------------+--------+------------------------+
Other ``Executors`` can be found for different computing environments:
* [1]_ for MPI
* [2]_ for SLURM or Condor
.. [1] https://mpi4py.readthedocs.io/en/stable/mpi4py.futures.html#mpipoolexecutor
.. [2] https://github.com/sampsyo/clusterfutures
"""
if isinstance(executor, concurrent.futures.Executor):
# the executor has already been created
executor = executor
elif executor is None and (ncpu is None or ncpu > 1):
# make the default type of Executor
if ncpu is None:
ncpu = multiprocessing.cpu_count()
executor = concurrent.futures.ProcessPoolExecutor(max_workers=ncpu)
# Save the current model so we can tweak it for gridding, then restore it at the end
savemod = ftr.model
gridmod = copy.deepcopy(ftr.model)
ftr.model = gridmod
# Freeze the params we are going to grid over
for parname in parnames:
getattr(ftr.model, parname).frozen = True
wftr = WrappedFitter(ftr, **fitargs)
# All other unfrozen parameters will be fitted for at each grid point
grid = np.meshgrid(*gridvalues)
chi2 = np.zeros(grid[0].shape)
out = []
# convert the gridded values to the actual parameter values
for j in range(len(parfuncs)):
out.append(parfuncs[j](*grid))
extraout = {}
for extrapar in extraparnames:
extraout[extrapar] = (
np.zeros(grid[0].shape, dtype=getattr(ftr.model, extrapar).quantity.dtype)
* getattr(ftr.model, extrapar).quantity.unit
)
# at this point, if the executor is None then run single-processor version
if executor is not None:
with executor as e:
if printprogress and tqdm is not None:
result = list(
tqdm(
e.map(
wftr.doonefit,
# (ftr,) * len(out[0].flatten()),
(parnames,) * len(out[0].flatten()),
list(zip(*[x.flatten() for x in out])),
(extraparnames,) * len(out[0].flatten()),
chunksize=chunksize,
),
total=len(out[0].flatten()),
ascii=True,
)
)
else:
result = e.map(
wftr.doonefit,
# (ftr,) * len(out[0].flatten()),
(parnames,) * len(out[0].flatten()),
list(zip(*[x.flatten() for x in out])),
(extraparnames,) * len(out[0].flatten()),
chunksize=chunksize,
)
it = np.ndindex(chi2.shape)
for i, r in zip(it, result):
chi2[i] = r[0]
for extrapar, extraparvalue in zip(extraparnames, r[1]):
extraout[extrapar][i] = extraparvalue
else:
indices = list(np.ndindex(grid[0].shape))
if printprogress:
if tqdm is not None:
indices = tqdm(indices, ascii=True)
else:
indices = ProgressBar(indices)
for i in indices:
for parnum, parname in enumerate(parnames):
getattr(ftr.model, parname).quantity = out[parnum][i]
ftr.fit_toas(**fitargs)
chi2[i] = ftr.resids.chi2
for extrapar in extraparnames:
extraout[extrapar] = getattr(ftr.model, extrapar).quantity
# Restore saved model
ftr.model = savemod
return chi2, out, extraout
[docs]def tuple_chisq(
ftr: fitter.Fitter,
parnames: List[str],
parvalues: List[np.ndarray],
extraparnames: List[str] = [],
executor: Optional[concurrent.futures.Executor] = None,
ncpu: Optional[int] = None,
chunksize: int = 1,
printprogress: bool = True,
**fitargs,
) -> Tuple[np.ndarray, Dict[str, np.ndarray]]:
"""Compute chisq over a list of parameter tuples
Parameters
----------
ftr : pint.fitter.Fitter
The base fitter to use.
parnames : list
Names of the parameters to grid over
parvalues : list
List of parameter values to grid over (each should be tuple of :class:`astropy.units.Quantity`)
extraparnames : list, optional
Names of other parameters to return
executor : concurrent.futures.Executor or None, optional
Executor object to run multiple processes in parallel
If None, will use default :class:`concurrent.futures.ProcessPoolExecutor`, unless overridden by ``ncpu=1``
ncpu : int, optional
If an existing Executor is not supplied, one will be created with this number of workers.
If 1, will run single-processor version
If None, will use :func:`multiprocessing.cpu_count`
chunksize : int
Size of the chunks for :class:`concurrent.futures.ProcessPoolExecutor` parallel execution.
Ignored for :class:`concurrent.futures.ThreadPoolExecutor`
printprogress : bool, optional
Print indications of progress (requires :mod:`tqdm` for `ncpu`>1)
fitargs :
additional arguments pass to fit_toas()
Returns
-------
np.ndarray : array of chisq values
extraout : dict of np.ndarray
Parameter values computed at each point for `extraparnames`
Example
-------
>>> import astropy.units as u
>>> import numpy as np
>>> import pint.config
>>> import pint.gridutils
>>> from pint.fitter import WLSFitter
>>> from pint.models.model_builder import get_model, get_model_and_toas
# Load in a basic dataset
>>> parfile = pint.config.examplefile("NGC6440E.par")
>>> timfile = pint.config.examplefile("NGC6440E.tim")
>>> m, t = get_model_and_toas(parfile, timfile)
>>> f = WLSFitter(t, m)
# find the best-fit
>>> f.fit_toas()
>>> bestfit = f.resids.chi2
# We'll do something like 3-sigma around the best-fit values of F0
>>> F0 = np.linspace(f.model.F0.quantity - 3 * f.model.F0.uncertainty,f.model.F0.quantity + 3 * f.model.F0.uncertainty,25)
>>> F1 = np.ones(len(F0))*f.model.F1.quantity
>>> chi2_F0,extra = pint.gridutils.tuple_chisq(f, ("F0","F1",), parvalues, extraparnames=("DM",))
Notes
-----
By default, it will create :class:`~concurrent.futures.ProcessPoolExecutor`
with ``max_workers`` equal to the desired number of cpus.
However, if you are running this as a script you may need something like::
import multiprocessing
if __name__ == "__main__":
multiprocessing.freeze_support()
...
tuple_chisq(...)
If an instantiated :class:`~concurrent.futures.Executor` is passed instead, it will be used as-is.
The behavior for different combinations of `executor` and `ncpu` is:
+-----------------+--------+------------------------+
| `executor` | `ncpu` | result |
+=================+========+========================+
| existing object | any | uses existing executor |
+-----------------+--------+------------------------+
| None | 1 | uses single-processor |
+-----------------+--------+------------------------+
| None | None | creates default |
| | | executor with |
| | | ``cpu_count`` workers |
+-----------------+--------+------------------------+
| None | >1 | creates default |
| | | executor with desired |
| | | number of workers |
+-----------------+--------+------------------------+
Other ``Executors`` can be found for different computing environments:
* [1]_ for MPI
* [2]_ for SLURM or Condor
.. [1] https://mpi4py.readthedocs.io/en/stable/mpi4py.futures.html#mpipoolexecutor
.. [2] https://github.com/sampsyo/clusterfutures
"""
if isinstance(executor, concurrent.futures.Executor):
# the executor has already been created
executor = executor
elif executor is None and (ncpu is None or ncpu > 1):
# make the default type of Executor
if ncpu is None:
ncpu = multiprocessing.cpu_count()
executor = concurrent.futures.ProcessPoolExecutor(max_workers=ncpu)
# Save the current model so we can tweak it for gridding, then restore it at the end
savemod = ftr.model
gridmod = copy.deepcopy(ftr.model)
ftr.model = gridmod
# Freeze the params we are going to grid over
for parname in parnames:
getattr(ftr.model, parname).frozen = True
wftr = WrappedFitter(ftr, **fitargs)
# All other unfrozen parameters will be fitted for at each grid point
chi2 = np.zeros(len(parvalues))
extraout = {}
for extrapar in extraparnames:
extraout[extrapar] = (
np.zeros(chi2.shape, dtype=getattr(ftr.model, extrapar).quantity.dtype)
* getattr(ftr.model, extrapar).quantity.unit
)
# at this point, if the executor is None then run single-processor version
if executor is not None:
with executor as e:
if printprogress and tqdm is not None:
result = list(
tqdm(
e.map(
wftr.doonefit,
(parnames,) * len(chi2),
parvalues,
(extraparnames,) * len(chi2),
chunksize=chunksize,
),
total=len(chi2),
ascii=True,
)
)
else:
result = e.map(
wftr.doonefit,
(parnames,) * len(chi2),
parvalues,
(extraparnames,) * len(chi2),
chunksize=chunksize,
)
it = np.ndindex(chi2.shape)
for i, r in zip(it, result):
chi2[i] = r[0]
for extrapar, extraparvalue in zip(extraparnames, r[1]):
extraout[extrapar][i] = extraparvalue
else:
indices = list(np.ndindex(chi2.shape))
if printprogress:
if tqdm is not None:
indices = tqdm(indices, ascii=True)
else:
indices = ProgressBar(indices)
for i in indices:
for parnum, parname in enumerate(parnames):
getattr(ftr.model, parname).quantity = parvalues[i[0]][parnum]
ftr.fit_toas(**fitargs)
chi2[i[0]] = ftr.resids.chi2
for extrapar in extraparnames:
extraout[extrapar][i[0]] = getattr(ftr.model, extrapar).quantity
# Restore saved model
ftr.model = savemod
return chi2, extraout
[docs]def tuple_chisq_derived(
ftr: fitter.Fitter,
parnames: List[str],
parfuncs: List[callable],
parvalues: List[np.ndarray],
extraparnames: List[str] = [],
executor: Optional[concurrent.futures.Executor] = None,
ncpu: Optional[int] = None,
chunksize: int = 1,
printprogress: bool = True,
**fitargs,
) -> Tuple[np.ndarray, List, Dict[str, np.ndarray]]:
"""Compute chisq over a list of derived parameter tuples
Parameters
----------
ftr : pint.fitter.Fitter
The base fitter to use.
parnames : list
Names of the parameters (available in `ftr`) to grid over
parfuncs : list
List of functions to convert `gridvalues` to quantities accessed through `parnames`
parvalues : list
List of underlying parameter values to fit (each should be a tuple of astropy.units.Quantity)
extraparnames : list, optional
Names of other parameters to return
executor : concurrent.futures.Executor or None, optional
Executor object to run multiple processes in parallel
If None, will use default :class:`concurrent.futures.ProcessPoolExecutor`, unless overridden by ``ncpu=1``
ncpu : int, optional
If an existing Executor is not supplied, one will be created with this number of workers.
If 1, will run single-processor version
If None, will use :func:`multiprocessing.cpu_count`
chunksize : int
Size of the chunks for :class:`concurrent.futures.ProcessPoolExecutor` parallel execution.
Ignored for :class:`concurrent.futures.ThreadPoolExecutor`
printprogress : bool, optional
Print indications of progress (requires :mod:`tqdm` for `ncpu`>1)
fitargs :
additional arguments pass to fit_toas()
Returns
-------
np.ndarray : array of chisq values
outparvalues : list of tuples
Parameter values computed from `parvalues` and `parfuncs`
extraout : dict of np.ndarray
Parameter values computed at each point for `extraparnames`
Example
-------
>>> import astropy.units as u
>>> import numpy as np
>>> import pint.config
>>> import pint.gridutils
>>> from pint.fitter import WLSFitter
>>> from pint.models.model_builder import get_model, get_model_and_toas
# Load in a basic dataset
>>> parfile = pint.config.examplefile("NGC6440E.par")
>>> timfile = pint.config.examplefile("NGC6440E.tim")
>>> m, t = get_model_and_toas(parfile, timfile)
>>> f = WLSFitter(t, m)
# find the best-fit
>>> f.fit_toas()
>>> bestfit = f.resids.chi2
# do a grid for F0 and tau
>>> F0 = np.linspace(f.model.F0.quantity - 3 * f.model.F0.uncertainty,f.model.F0.quantity + 3 * f.model.F0.uncertainty,15,)
# make sure it's the same length
>>> tau = np.linspace(8.1, 8.3, 15) * 100 * u.Myr
>>> parvalues = list(zip(F0,tau))
>>> chi2_tau, params, _ = pint.gridutils.tuple_chisq_derived(f,("F0", "F1"),(lambda x, y: x, lambda x, y: -x / 2 / y),(F0, tau))
Notes
-----
By default, it will create :class:`~concurrent.futures.ProcessPoolExecutor`
with ``max_workers`` equal to the desired number of cpus.
However, if you are running this as a script you may need something like::
import multiprocessing
if __name__ == "__main__":
multiprocessing.freeze_support()
...
tuple_chisq_derived(...)
If an instantiated :class:`~concurrent.futures.Executor` is passed instead, it will be used as-is.
The behavior for different combinations of `executor` and `ncpu` is:
+-----------------+--------+------------------------+
| `executor` | `ncpu` | result |
+=================+========+========================+
| existing object | any | uses existing executor |
+-----------------+--------+------------------------+
| None | 1 | uses single-processor |
+-----------------+--------+------------------------+
| None | None | creates default |
| | | executor with |
| | | ``cpu_count`` workers |
+-----------------+--------+------------------------+
| None | >1 | creates default |
| | | executor with desired |
| | | number of workers |
+-----------------+--------+------------------------+
Other ``Executors`` can be found for different computing environments:
* [1]_ for MPI
* [2]_ for SLURM or Condor
.. [1] https://mpi4py.readthedocs.io/en/stable/mpi4py.futures.html#mpipoolexecutor
.. [2] https://github.com/sampsyo/clusterfutures
"""
if isinstance(executor, concurrent.futures.Executor):
# the executor has already been created
executor = executor
elif executor is None and (ncpu is None or ncpu > 1):
# make the default type of Executor
if ncpu is None:
ncpu = multiprocessing.cpu_count()
executor = concurrent.futures.ProcessPoolExecutor(max_workers=ncpu)
# Save the current model so we can tweak it for gridding, then restore it at the end
savemod = ftr.model
gridmod = copy.deepcopy(ftr.model)
ftr.model = gridmod
# Freeze the params we are going to grid over
for parname in parnames:
getattr(ftr.model, parname).frozen = True
wftr = WrappedFitter(ftr, **fitargs)
# All other unfrozen parameters will be fitted for at each grid point
chi2 = np.zeros(len(parvalues))
out = []
# convert the tuples of values to the actual parameter values
for i in range(len(parvalues)):
out.append([f(*parvalues[i]) for f in parfuncs])
extraout = {}
for extrapar in extraparnames:
extraout[extrapar] = (
np.zeros(len(chi2), dtype=getattr(ftr.model, extrapar).quantity.dtype)
* getattr(ftr.model, extrapar).quantity.unit
)
# at this point, if the executor is None then run single-processor version
if executor is not None:
with executor as e:
if printprogress and tqdm is not None:
result = list(
tqdm(
e.map(
wftr.doonefit,
(parnames,) * len(chi2),
out,
(extraparnames,) * len(chi2),
chunksize=chunksize,
),
total=len(chi2),
ascii=True,
)
)
else:
result = e.map(
wftr.doonefit,
(parnames,) * len(chi2),
out,
(extraparnames,) * len(chi2),
chunksize=chunksize,
)
it = np.ndindex(chi2.shape)
for i, r in zip(it, result):
chi2[i] = r[0]
for extrapar, extraparvalue in zip(extraparnames, r[1]):
extraout[extrapar][i] = extraparvalue
else:
indices = list(np.ndindex(chi2.shape))
if printprogress:
if tqdm is not None:
indices = tqdm(indices, ascii=True)
else:
indices = ProgressBar(indices)
for i in indices:
for parnum, parname in enumerate(parnames):
getattr(ftr.model, parname).quantity = out[i[0]][parnum]
ftr.fit_toas(**fitargs)
chi2[i[0]] = ftr.resids.chi2
for extrapar in extraparnames:
extraout[extrapar][i[0]] = getattr(ftr.model, extrapar).quantity
# Restore saved model
ftr.model = savemod
return chi2, out, extraout