Skip to content

Commit 40060b1

Browse files
authored
Merge pull request #412 from nanograv/dev
Dev to Master
2 parents 447ddae + 7aa0ac2 commit 40060b1

32 files changed

+516
-78
lines changed

.coveragerc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ omit =
66
plugins =
77
coverage_conditional_plugin
88

9+
[report]
10+
exclude_also =
11+
no cover: start(?s:.)*?no cover: stop
12+
913
[coverage_conditional_plugin]
1014
rules =
1115
"sys_version_info >= (3, 8)": py-gte-38

.github/.codecov.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ coverage:
88
status:
99
project:
1010
default:
11-
target: 85%
11+
target: 70%
1212
threshold: 6%
1313
patch:
1414
default:
1515
target: auto
16-
threshold: 6%
16+
threshold: 25%
1717

1818
parsers:
1919
gcov:

.github/workflows/ci_test.yml

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ jobs:
1616
strategy:
1717
fail-fast: false
1818
matrix:
19-
os: [ubuntu-latest, macos-13]
19+
os: [ubuntu-latest, macos-latest]
2020
python-version: ['3.8', '3.9', '3.10', '3.11', '3.12']
2121

2222
steps:
@@ -31,12 +31,10 @@ jobs:
3131
run: |
3232
brew unlink gcc && brew link gcc
3333
brew install automake suite-sparse
34-
curl -sSL https://raw.githubusercontent.com/vallis/libstempo/master/install_tempo2.sh | sh
3534
- name: Install non-python dependencies on linux
3635
if: runner.os == 'Linux'
3736
run: |
3837
sudo apt-get install libsuitesparse-dev
39-
curl -sSL https://raw.githubusercontent.com/vallis/libstempo/master/install_tempo2.sh | sh
4038
- name: Install dependencies and package
4139
env:
4240
SUITESPARSE_INCLUDE_DIR: "/usr/local/opt/suite-sparse/include/suitesparse/"
@@ -76,7 +74,6 @@ jobs:
7674
- name: Install non-python dependencies on linux
7775
run: |
7876
sudo apt-get install libsuitesparse-dev
79-
curl -sSL https://raw.githubusercontent.com/vallis/libstempo/master/install_tempo2.sh | sh
8077
- name: Build
8178
run: |
8279
python -m pip install --upgrade pip setuptools wheel

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ clean-test: ## remove test and coverage artifacts
6464
rm -fr htmlcov/
6565
rm -rf coverage.xml
6666

67-
COV_COVERAGE_PERCENT ?= 85
67+
COV_COVERAGE_PERCENT ?= 70
6868
test: lint ## run tests quickly with the default Python
6969
pytest -v --durations=10 --full-trace --cov-report html --cov-report xml \
7070
--cov-config .coveragerc --cov-fail-under=$(COV_COVERAGE_PERCENT) \

docs/_static/notebooks/usage.ipynb

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,76 @@
7171
"psr = Pulsar(parfiles, timfiles)"
7272
]
7373
},
74+
{
75+
"cell_type": "markdown",
76+
"metadata": {},
77+
"source": [
78+
"## `feather` files are now supported\n",
79+
"\n",
80+
"`enterprise` now supports the use of `feather` files to store the `Pulsar` objects. These files are compressed, and therefore they are useful for saving and loading large pulsar datasets. Below we show how to save and load a `Pulsar` object using `feather` files with a corresponding noise dictionary. Saving Pulsar objects this way requires the `pyarrow` package and `libstempo` or `PINT` to be installed so that we can create a `Pulsar` object using `par` and `tim` files. Once the `feather` file exists, we can load the `Pulsar` object without the need for `libstempo` or `PINT`.\n",
81+
"\n",
82+
"`feather` files can also take in dictionaries of noise parameters for each pulsar to be used in `enterprise` models. Below, we show how to save and load a `Pulsar` object with a noise dictionary."
83+
]
84+
},
85+
{
86+
"cell_type": "code",
87+
"execution_count": null,
88+
"metadata": {},
89+
"outputs": [],
90+
"source": [
91+
"psr_name = 'J1909-3744'\n",
92+
"\n",
93+
"# Here is the noise dictionary for this pulsar\n",
94+
"params = {'J1909-3744_Rcvr_800_GASP_efac': 0.985523,\n",
95+
" 'J1909-3744_Rcvr1_2_GUPPI_efac': 1.03462,\n",
96+
" 'J1909-3744_Rcvr1_2_GASP_efac': 0.986438,\n",
97+
" 'J1909-3744_Rcvr_800_GUPPI_efac': 1.05208,\n",
98+
" 'J1909-3744_Rcvr1_2_GASP_log10_ecorr': -8.00662,\n",
99+
" 'J1909-3744_Rcvr1_2_GUPPI_log10_ecorr': -7.13828,\n",
100+
" 'J1909-3744_Rcvr_800_GASP_log10_ecorr': -7.86032,\n",
101+
" 'J1909-3744_Rcvr_800_GUPPI_log10_ecorr': -7.14764,\n",
102+
" 'J1909-3744_Rcvr_800_GASP_log10_equad': -6.6358,\n",
103+
" 'J1909-3744_Rcvr1_2_GUPPI_log10_equad': -8.31285,\n",
104+
" 'J1909-3744_Rcvr1_2_GASP_log10_equad': -7.97229,\n",
105+
" 'J1909-3744_Rcvr_800_GUPPI_log10_equad': -7.43842,\n",
106+
" 'J1909-3744_log10_A': -15.1073,\n",
107+
" 'J1909-3744_gamma': 2.88933}"
108+
]
109+
},
110+
{
111+
"cell_type": "markdown",
112+
"metadata": {},
113+
"source": [
114+
"### Save to feather file"
115+
]
116+
},
117+
{
118+
"cell_type": "code",
119+
"execution_count": null,
120+
"metadata": {},
121+
"outputs": [],
122+
"source": [
123+
"psr = Pulsar(datadir + f\"/{psr_name}_NANOGrav_9yv1.gls.par\", datadir + f\"/{psr_name}_NANOGrav_9yv1.tim\")\n",
124+
"\n",
125+
"psr.to_feather(datadir + f\"/{psr_name}_NANOGrav_9yv1.t2.feather\", noisedict=params)"
126+
]
127+
},
128+
{
129+
"cell_type": "markdown",
130+
"metadata": {},
131+
"source": [
132+
"### Load from feather file"
133+
]
134+
},
135+
{
136+
"cell_type": "code",
137+
"execution_count": null,
138+
"metadata": {},
139+
"outputs": [],
140+
"source": [
141+
"psr = Pulsar(datadir + f\"/{psr_name}_NANOGrav_9yv1.t2.feather\")"
142+
]
143+
},
74144
{
75145
"cell_type": "markdown",
76146
"metadata": {

enterprise/pulsar.py

Lines changed: 127 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@
77
import logging
88
import os
99
import pickle
10+
11+
from pyarrow import feather
12+
from pyarrow import Table
1013
from io import StringIO
1114

1215
import numpy as np
@@ -23,7 +26,9 @@
2326
try:
2427
import libstempo as t2
2528
except ImportError:
26-
logger.warning("libstempo not installed. Will use PINT instead.") # pragma: no cover
29+
logger.warning(
30+
"libstempo not installed. PINT or libstempo are required to use par and tim files."
31+
) # pragma: no cover
2732
t2 = None
2833

2934
try:
@@ -32,7 +37,7 @@
3237
from pint.residuals import Residuals as resids
3338
from pint.toa import TOAs
3439
except ImportError:
35-
logger.warning("PINT not installed. Will use libstempo instead.") # pragma: no cover
40+
logger.warning("PINT not installed. PINT or libstempo are required to use par and tim files.") # pragma: no cover
3641
pint = None
3742

3843
try:
@@ -42,10 +47,6 @@
4247
const = None
4348
u = None
4449

45-
if pint is None and t2 is None:
46-
err_msg = "Must have either PINT or libstempo timing package installed"
47-
raise ImportError(err_msg)
48-
4950

5051
def get_maxobs(timfile):
5152
"""Utility function to return number of lines in tim file.
@@ -161,6 +162,9 @@ def filter_data(self, start_time=None, end_time=None):
161162

162163
self.sort_data()
163164

165+
def to_feather(self, filename, noisedict=None):
166+
FeatherPulsar.save_feather(self, filename, noisedict=noisedict)
167+
164168
def drop_not_picklable(self):
165169
"""Drop all attributes that cannot be pickled.
166170
@@ -421,6 +425,8 @@ def _set_dm(self, model):
421425

422426
if dmx:
423427
self._dmx = dmx
428+
else:
429+
self._dmx = None
424430

425431
def _get_radec(self, model):
426432
if hasattr(model, "RAJ") and hasattr(model, "DECJ"):
@@ -565,6 +571,8 @@ def _set_dm(self, t2pulsar):
565571

566572
if dmx:
567573
self._dmx = dmx
574+
else:
575+
self._dmx = None
568576

569577
def _get_radec(self, t2pulsar):
570578
if "RAJ" in np.concatenate((t2pulsar.pars(which="fit"), t2pulsar.pars(which="set"))):
@@ -655,7 +663,120 @@ def destroy(psr): # pragma: py-lt-38
655663
psr._deflated = "destroyed"
656664

657665

666+
class FeatherPulsar:
667+
columns = ["toas", "stoas", "toaerrs", "residuals", "freqs", "backend_flags", "telescope"]
668+
vector_columns = ["Mmat", "sunssb", "pos_t"]
669+
tensor_columns = ["planetssb"]
670+
# flags are done separately
671+
metadata = ["name", "dm", "dmx", "pdist", "pos", "phi", "theta"]
672+
# notes: currently ignores _isort/__isort and gets sorted versions
673+
674+
def __init__(self):
675+
pass
676+
677+
def __str__(self):
678+
return f"<Pulsar {self.name}: {len(self.residuals)} res, {self.Mmat.shape[1]} pars>"
679+
680+
def __repr__(self):
681+
return str(self)
682+
683+
def sort_data(self):
684+
"""Sort data by time. This function is defined so that tests will pass."""
685+
self._isort = np.argsort(self.toas, kind="mergesort")
686+
self._iisort = np.zeros(len(self._isort), dtype=int)
687+
for ii, p in enumerate(self._isort):
688+
self._iisort[p] = ii
689+
690+
@classmethod
691+
def read_feather(cls, filename):
692+
f = feather.read_table(filename)
693+
self = FeatherPulsar()
694+
695+
for array in FeatherPulsar.columns:
696+
if array in f.column_names:
697+
setattr(self, array, f[array].to_numpy())
698+
699+
for array in FeatherPulsar.vector_columns:
700+
cols = [c for c in f.column_names if c.startswith(array)]
701+
setattr(self, array, np.array([f[col].to_numpy() for col in cols]).swapaxes(0, 1).copy())
702+
703+
for array in FeatherPulsar.tensor_columns:
704+
rows = sorted(set(["_".join(c.split("_")[:-1]) for c in f.column_names if c.startswith(array)]))
705+
cols = [[c for c in f.column_names if c.startswith(row)] for row in rows]
706+
setattr(
707+
self,
708+
array,
709+
np.array([[f[col].to_numpy() for col in row] for row in cols]).swapaxes(0, 2).swapaxes(1, 2).copy(),
710+
)
711+
712+
self.flags = {}
713+
for array in [c for c in f.column_names if c.startswith("flags_")]:
714+
self.flags["_".join(array.split("_")[1:])] = f[array].to_numpy().astype("U")
715+
716+
meta = json.loads(f.schema.metadata[b"json"])
717+
for attr in FeatherPulsar.metadata:
718+
if attr in meta:
719+
setattr(self, attr, meta[attr])
720+
else:
721+
print(f"Pulsar.read_feather: cannot find {attr} in feather file {filename}.")
722+
723+
if "noisedict" in meta:
724+
setattr(self, "noisedict", meta["noisedict"])
725+
726+
self.sort_data()
727+
728+
return self
729+
730+
def to_list(a):
731+
return a.tolist() if isinstance(a, np.ndarray) else a
732+
733+
def save_feather(self, filename, noisedict=None):
734+
self._toas = self._toas.astype(float)
735+
pydict = {array: getattr(self, array) for array in FeatherPulsar.columns}
736+
737+
pydict.update(
738+
{
739+
f"{array}_{i}": getattr(self, array)[:, i]
740+
for array in FeatherPulsar.vector_columns
741+
for i in range(getattr(self, array).shape[1])
742+
}
743+
)
744+
745+
pydict.update(
746+
{
747+
f"{array}_{i}_{j}": getattr(self, array)[:, i, j]
748+
for array in FeatherPulsar.tensor_columns
749+
for i in range(getattr(self, array).shape[1])
750+
for j in range(getattr(self, array).shape[2])
751+
}
752+
)
753+
754+
pydict.update({f"flags_{flag}": self.flags[flag] for flag in self.flags})
755+
756+
meta = {}
757+
for attr in Pulsar.metadata:
758+
if hasattr(self, attr):
759+
meta[attr] = Pulsar.to_list(getattr(self, attr))
760+
else:
761+
print(f"Pulsar.save_feather: cannot find {attr} in Pulsar {self.name}.")
762+
763+
# use attribute if present
764+
noisedict = getattr(self, "noisedict", None) if noisedict is None else noisedict
765+
if noisedict:
766+
# only keep noisedict entries that are for this pulsar (requires pulsar name to be first part of the key!)
767+
meta["noisedict"] = {par: val for par, val in noisedict.items() if par.startswith(self.name)}
768+
769+
feather.write_feather(Table.from_pydict(pydict, metadata={"json": json.dumps(meta)}), filename)
770+
771+
658772
def Pulsar(*args, **kwargs):
773+
featherfile = [x for x in args if isinstance(x, str) and x.endswith(".feather")]
774+
if featherfile:
775+
return FeatherPulsar.read_feather(featherfile[0])
776+
featherfile = kwargs.get("filepath", None)
777+
if featherfile:
778+
return FeatherPulsar.read_feather(featherfile)
779+
659780
ephem = kwargs.get("ephem", None)
660781
clk = kwargs.get("clk", None)
661782
bipm_version = kwargs.get("bipm_version", None)

enterprise/signals/gp_signals.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -876,5 +876,12 @@ def solve(self, right, left_array=None, logdet=False):
876876

877877
TNT = self.Nmat.solve(T, left_array=T)
878878
return TNT - np.tensordot(self.MNF(T), self.MNMMNF(T), (0, 0))
879+
elif left_array is not None and right.ndim == left_array.ndim and right.ndim <= 2:
880+
T = right
881+
L = left_array
882+
883+
LNT = self.Nmat.solve(T, left_array=L)
884+
885+
return LNT - np.tensordot(self.MNF(L), self.MNMMNF(T), (0, 0))
879886
else:
880887
raise ValueError("Incorrect arguments given to MarginalizingNmat.solve.")

enterprise/signals/signal_base.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -691,6 +691,13 @@ def get_lnprior(self, params):
691691
def pulsars(self):
692692
return [p.psrname for p in self._signalcollections]
693693

694+
def get_hypercube_transform(self, params):
695+
# transform from unit cube to prior cube for nested sampling using PPFs
696+
# map parameter vector if needed
697+
params = params if isinstance(params, dict) else self.map_params(params)
698+
699+
return np.hstack([p.get_ppf(params=params) for p in self.params])
700+
694701
def _set_signal_dict(self):
695702
"""Set signal dictionary"""
696703

0 commit comments

Comments
 (0)