Skip to content

Commit 8af6b1c

Browse files
committed
Merge branch 'main' into nextflow
2 parents df093b5 + 1c63489 commit 8af6b1c

30 files changed

+1314
-124
lines changed

.github/workflows/sphinx_deploy.yaml

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Deploy Sphinx documentation to Pages
2+
3+
on:
4+
push:
5+
branches: [main] # branch to trigger deployment
6+
7+
jobs:
8+
pages:
9+
runs-on: ubuntu-20.04
10+
environment:
11+
name: github-pages
12+
url: ${{ steps.deployment.outputs.page_url }}
13+
permissions:
14+
pages: write
15+
id-token: write
16+
steps:
17+
- id: deployment
18+
uses: sphinx-notes/pages@v3

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ instance/
7070

7171
# Sphinx documentation
7272
docs/_build/
73+
html/
7374

7475
# PyBuilder
7576
target/

.readthedocs.yaml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# .readthedocs.yaml
2+
# Read the Docs configuration file
3+
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
4+
5+
# Required
6+
version: 2
7+
8+
# Set the OS, Python version and other tools you might need
9+
build:
10+
os: ubuntu-22.04
11+
tools:
12+
python: "3.11"
13+
14+
# Build documentation in the "docs/" directory with Sphinx
15+
sphinx:
16+
configuration: docs/conf.py
17+
18+
# Optionally declare the Python requirements required to build your docs
19+
python:
20+
install:
21+
- requirements: docs/requirements.txt

binary_tools.py

+300
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
#!/usr/bin/env python
2+
3+
"""
4+
Code containing binary utilities adapted from Scintools codebase
5+
6+
__author__ = ["Andrew Cameron", "Daniel Reardon"]
7+
__maintainer__ = "Andrew Cameron"
8+
__email__ = "andrewcameron@swin.edu.au"
9+
__status__ = "Development"
10+
"""
11+
12+
# Imports
13+
import numpy as np
14+
from decimal import Decimal,InvalidOperation
15+
from scipy.optimize import fsolve
16+
import math
17+
18+
# Constants
19+
DAYPERYEAR = 365.25
20+
21+
# reads a par file into a dictionary object
22+
# this functionality is already performed to an extent by PSRDB / util.ephemeris, but
23+
# that code is moreso just to read and store the fields. This code has better control for
24+
# handling/mainpulating/calculating with those fields, and so for the moment I'll retain it.
25+
def read_par(parfile):
26+
"""
27+
Reads a par file and return a dictionary of parameter names and values
28+
"""
29+
30+
par = {}
31+
ignore = ['DMMODEL', 'DMOFF', "DM_", "CM_", 'CONSTRAIN', 'JUMP', 'NITS',
32+
'NTOA', 'CORRECT_TROPOSPHERE', 'PLANET_SHAPIRO', 'DILATEFREQ',
33+
'TIMEEPH', 'MODE', 'TZRMJD', 'TZRSITE', 'TZRFRQ', 'EPHVER',
34+
'T2CMETHOD']
35+
36+
file = open(parfile, 'r')
37+
for line in file.readlines():
38+
err = None
39+
p_type = None
40+
sline = line.split()
41+
if len(sline) == 0 or line[0] == "#" or line[0:2] == "C " or sline[0] in ignore:
42+
continue
43+
44+
param = sline[0]
45+
if param == "E":
46+
param = "ECC"
47+
48+
val = sline[1]
49+
if len(sline) == 3 and sline[2] not in ['0', '1']:
50+
err = sline[2].replace('D', 'E')
51+
elif len(sline) == 4:
52+
err = sline[3].replace('D', 'E')
53+
54+
try:
55+
val = int(val)
56+
p_type = 'd'
57+
except ValueError:
58+
try:
59+
val = float(Decimal(val.replace('D', 'E')))
60+
if 'e' in sline[1] or 'E' in sline[1].replace('D', 'E'):
61+
p_type = 'e'
62+
else:
63+
p_type = 'f'
64+
except InvalidOperation:
65+
p_type = 's'
66+
67+
par[param] = val
68+
if err:
69+
par[param+"_ERR"] = float(err)
70+
71+
if p_type:
72+
par[param+"_TYPE"] = p_type
73+
74+
file.close()
75+
76+
return par
77+
78+
def get_binphase(mjds, pars):
79+
"""
80+
Calculates binary phase for an array of barycentric MJDs and a parameter dictionary
81+
"""
82+
83+
U = get_true_anomaly(mjds, pars)
84+
OM = get_omega(pars, U)
85+
86+
# normalise U
87+
U = np.fmod(U, 2*np.pi)
88+
89+
return np.fmod(U + OM + 2*np.pi, 2*np.pi)/(2*np.pi)
90+
91+
def get_ELL1_arctan(EPS1, EPS2):
92+
"""
93+
Given the EPS1 and EPS2 parameters of the ELL1 binary model,
94+
calculate the arctan(EPS1/EPS2) value accounting for all degeneracies and ambiguities.
95+
This function has been abstracted as it is needed for calculating both OM and T0
96+
"""
97+
98+
# check for undefined tan
99+
if (EPS2 == 0):
100+
if (EPS1 > 0):
101+
AT = np.pi/2
102+
elif (EPS1 < 0):
103+
AT = -np.pi/2
104+
else:
105+
# eccentricity must be perfectly zero - omega is therefore undefined
106+
AT = 0
107+
else:
108+
AT = np.arctan(EPS1/EPS2)
109+
# check for tan degeneracy
110+
if (EPS2 < 0):
111+
AT += np.pi
112+
113+
return np.fmod(AT + 2*np.pi, 2*np.pi)
114+
115+
def get_omega(pars, U):
116+
"""
117+
Calculate the instantaneous version of omega (radians) accounting for OMDOT
118+
per Eq. 8.19 of the Handbook. May be slightly incorrect for relativistic systems
119+
"""
120+
121+
# get reference omega
122+
if 'TASC' in pars.keys():
123+
if 'EPS1' in pars.keys() and 'EPS2' in pars.keys():
124+
125+
OM = get_ELL1_arctan(pars['EPS1'], pars['EPS2'])
126+
# ensure OM within range 0..2pi
127+
OM = np.fmod(OM + 2*np.pi, 2*np.pi)
128+
129+
else:
130+
OM = 0
131+
else:
132+
if 'OM' in pars.keys():
133+
OM = pars['OM'] * np.pi/180
134+
else:
135+
OM = 0
136+
137+
# get change in omega
138+
if 'OMDOT' in pars.keys():
139+
# convert from deg/yr to rad/day
140+
OMDOT = pars['OMDOT'] * (np.pi/180) / DAYPERYEAR
141+
else:
142+
OMDOT = 0
143+
144+
# calculate current omega
145+
OMB = get_OMB(pars)
146+
OM = OM + OMDOT*U/(OMB)
147+
148+
return OM
149+
150+
def get_OMB(pars):
151+
"""
152+
Return a simple, constant value of OMB (rad / days)
153+
"""
154+
155+
if 'PB' in pars.keys():
156+
OMB = 2*np.pi/pars['PB']
157+
158+
elif 'FB0' in pars.keys():
159+
OMB = 2*np.pi*pars['FB0'] * 86400
160+
161+
return OMB
162+
163+
def get_ecc(pars):
164+
"""
165+
Calculate eccentricity depending on binary model
166+
"""
167+
168+
if 'TASC' in pars.keys():
169+
if 'EPS1' in pars.keys() and 'EPS2' in pars.keys():
170+
ECC = np.sqrt(pars['EPS1']**2 + pars['EPS2']**2)
171+
else:
172+
ECC = 0
173+
else:
174+
if 'ECC' in pars.keys():
175+
ECC = pars['ECC']
176+
else:
177+
ECC = 0
178+
179+
return ECC
180+
181+
def get_T0(pars):
182+
"""
183+
Calculate T0 depending on binary model
184+
"""
185+
186+
if 'TASC' in pars.keys():
187+
if 'EPS1' in pars.keys() and 'EPS2' in pars.keys():
188+
OMB = get_OMB(pars)
189+
T0 = pars['TASC'] + get_ELL1_arctan(pars['EPS1'], pars['EPS2'])/OMB # MJD - No PBDOT correction required as referenced to zero epoch
190+
else:
191+
T0 = pars['TASC']
192+
else:
193+
T0 = pars['T0'] # MJD
194+
195+
return T0
196+
197+
def get_mean_anomaly(mjds, pars):
198+
"""
199+
Calculates mean anomalies for an array of barycentric MJDs and a parameter dictionary
200+
"""
201+
202+
# handle conversion of T0/TASC
203+
T0 = get_T0(pars)
204+
205+
# determine which type of orbital period encoding we're dealing with
206+
if 'PB' in pars.keys():
207+
208+
PB = pars['PB']
209+
210+
# normal approach
211+
if 'PBDOT' in pars.keys():
212+
PBDOT = pars['PBDOT']
213+
else:
214+
PBDOT = 0
215+
216+
if np.abs(PBDOT) > 1e-6: # adjusted from Daniels' setting
217+
# correct tempo-format
218+
PBDOT *= 10**-12
219+
220+
OMB = get_OMB(pars)
221+
M = OMB*((mjds - T0) - 0.5*(PBDOT/PB) * (mjds - T0)**2)
222+
223+
elif 'FB0' in pars.keys():
224+
225+
M = np.zeros(len(mjds))
226+
i = 0
227+
228+
# produce integrated Taylor series of FB terms
229+
while ('FB' + ('%s' % i) in pars.keys()):
230+
M = M + pars['FB' + ('%s' % i)] * ((mjds - T0)**(i+1))/math.factorial(i + 1)
231+
i += 1
232+
233+
M = M * 2*np.pi * 86400
234+
235+
M = M.squeeze()
236+
return M
237+
238+
def get_eccentric_anomaly(mjds, pars):
239+
"""
240+
Calculates eccentric anomalies for an array of barycentric MJDs and a parameter dictionary
241+
"""
242+
243+
# first obtain mean anomaly
244+
M = get_mean_anomaly(mjds, pars)
245+
246+
# handle conversion of EPS/ECC
247+
ECC = get_ecc(pars)
248+
249+
# eccentric anomaly
250+
if ECC < 1e-4:
251+
print('Assuming circular orbit for true anomaly calculation')
252+
E = M
253+
else:
254+
M = np.asarray(M, dtype=np.float64)
255+
E = fsolve(lambda E: E - ECC*np.sin(E) - M, M)
256+
E = np.asarray(E, dtype=np.float128)
257+
258+
return E
259+
260+
def get_true_anomaly(mjds, pars):
261+
"""
262+
Calculates true anomalies for an array of barycentric MJDs and a parameter dictionary
263+
"""
264+
265+
# first obtain eccentric anomaly
266+
E = get_eccentric_anomaly(mjds, pars)
267+
268+
# handle conversion of EPS/ECC
269+
ECC = get_ecc(pars)
270+
271+
# true anomaly
272+
U = 2*np.arctan2(np.sqrt(1 + ECC) * np.sin(E/2), np.sqrt(1 - ECC) * np.cos(E/2))
273+
274+
if hasattr(U, "__len__"):
275+
U[np.argwhere(U < 0)] = U[np.argwhere(U < 0)] + 2*np.pi
276+
#U = U.squeeze()
277+
elif U < 0:
278+
U += 2*np.pi
279+
280+
# final change - need to have U count the number of orbits - rescale to match M and E
281+
E_fac = np.floor_divide(E, 2*np.pi)
282+
U += E_fac * 2*np.pi
283+
284+
return U
285+
286+
def is_binary(pars):
287+
"""
288+
Determine if a set of parameters adequately describes a binary pulsar
289+
"""
290+
291+
retval = False
292+
293+
bflag = 'BINARY' in pars.keys()
294+
orbflag = 'PB' in pars.keys() or 'FB0' in pars.keys()
295+
timeflag = 'TASC' in pars.keys() or 'T0' in pars.keys()
296+
297+
if (bflag and orbflag and timeflag):
298+
retval = True
299+
300+
return retval

0 commit comments

Comments
 (0)