pizzetti 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pizzetti/__init__.py +12 -0
- pizzetti/_exact.py +231 -0
- pizzetti/_general.py +161 -0
- pizzetti/_orbit.py +140 -0
- pizzetti/_series.py +208 -0
- pizzetti/laws.py +58 -0
- pizzetti/occult.py +225 -0
- pizzetti/transitmodel.py +199 -0
- pizzetti-0.1.0.dist-info/METADATA +161 -0
- pizzetti-0.1.0.dist-info/RECORD +13 -0
- pizzetti-0.1.0.dist-info/WHEEL +5 -0
- pizzetti-0.1.0.dist-info/licenses/LICENSE +674 -0
- pizzetti-0.1.0.dist-info/top_level.txt +1 -0
pizzetti/__init__.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""pizzetti: fast limb-darkened transit light curves.
|
|
2
|
+
|
|
3
|
+
A drop-in replacement for batman's ``TransitModel`` / ``TransitParams``. While
|
|
4
|
+
the planet is entirely inside the stellar disk, the flux is evaluated with an
|
|
5
|
+
elementary series that has a rigorous error bound; near and across the limb
|
|
6
|
+
an exact (quadratic family) or quadrature (other power laws) solution is used.
|
|
7
|
+
"""
|
|
8
|
+
from .occult import SERIES_TOL, occult, series_cut
|
|
9
|
+
from .transitmodel import TransitModel, TransitParams
|
|
10
|
+
|
|
11
|
+
__version__ = "0.1.0"
|
|
12
|
+
__all__ = ["TransitModel", "TransitParams", "occult", "series_cut", "SERIES_TOL", "__version__"]
|
pizzetti/_exact.py
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Exact Mandel & Agol (2002) flux for quadratic limb darkening, used for the
|
|
2
|
+
samples that the interior series does not cover (near and across the stellar
|
|
3
|
+
limb).
|
|
4
|
+
|
|
5
|
+
Vectorised, branch-free evaluation with Bulirsch's general complete elliptic
|
|
6
|
+
integral (Bulirsch 1969) at a fixed number of steps, and elliptic operands in
|
|
7
|
+
closed form from (z, p) as in Agol, Luger & Foreman-Mackey (2020, eq. 41).
|
|
8
|
+
Lanes that are not converged after the fixed steps are recomputed with a
|
|
9
|
+
scalar early-exit iteration. Adapted from the Pandora code (Hippke & Heller
|
|
10
|
+
2022, GPL-3).
|
|
11
|
+
|
|
12
|
+
Conventions: z = projected centre distance, k = radius ratio (both in stellar
|
|
13
|
+
radii); quadratic law I(mu) = 1 - u1 (1 - mu) - u2 (1 - mu)^2. The flux is
|
|
14
|
+
F = 1 - (c1 lex + c2 ldx + u2 edx) omega,
|
|
15
|
+
c1 = 1 - u1 - 2 u2, c2 = u1 + 2 u2, omega = 1 / (1 - u1/3 - u2/6),
|
|
16
|
+
with the LD-independent terms lex (blocked area / pi), ldx (lambda_d of
|
|
17
|
+
Mandel & Agol plus 2/3 Theta(k - z)) and edx (eta_d).
|
|
18
|
+
"""
|
|
19
|
+
from math import pi
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from numba import njit
|
|
23
|
+
|
|
24
|
+
# fixed Bulirsch steps of the vectorised pass; non-converged lanes fall back
|
|
25
|
+
ELL_BATCH_ITERATIONS = 5
|
|
26
|
+
# floor of the complementary modulus (kc = 0 only at internal tangency)
|
|
27
|
+
KC_MIN = 1e-150
|
|
28
|
+
# smallest |z - k| (z == k exactly is moved by one ulp)
|
|
29
|
+
Z_K_MIN = 1e-150
|
|
30
|
+
|
|
31
|
+
# fdlibm e_asin / e_acos rational kernel
|
|
32
|
+
_PS0 = 1.66666666666666657415e-01
|
|
33
|
+
_PS1 = -3.25565818622400915405e-01
|
|
34
|
+
_PS2 = 2.01212532134862925881e-01
|
|
35
|
+
_PS3 = -4.00555345006794114027e-02
|
|
36
|
+
_PS4 = 7.91534994289814532176e-04
|
|
37
|
+
_PS5 = 3.47933107596021167570e-05
|
|
38
|
+
_QS1 = -2.40339491173441421878e+00
|
|
39
|
+
_QS2 = 2.02094576023350569471e+00
|
|
40
|
+
_QS3 = -6.88283971605453293030e-01
|
|
41
|
+
_QS4 = 7.70381505559019352791e-02
|
|
42
|
+
_HALF_PI = 1.5707963267948966
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@njit(cache=True, fastmath=True, error_model="numpy", inline="always")
|
|
46
|
+
def acos_poly(x):
|
|
47
|
+
"""arccos(x) for -1 <= x <= 1, branch-free (fdlibm with selects), so that
|
|
48
|
+
loops calling it vectorise; <= 1 ulp from libm."""
|
|
49
|
+
ax = abs(x)
|
|
50
|
+
small = ax <= 0.5
|
|
51
|
+
z = x * x if small else 0.5 * (1.0 - ax)
|
|
52
|
+
s = np.sqrt(z)
|
|
53
|
+
p = z * (_PS0 + z * (_PS1 + z * (_PS2 + z * (_PS3 + z * (_PS4 + z * _PS5)))))
|
|
54
|
+
q = 1.0 + z * (_QS1 + z * (_QS2 + z * (_QS3 + z * _QS4)))
|
|
55
|
+
r = p / q
|
|
56
|
+
a_small = _HALF_PI - (x + x * r)
|
|
57
|
+
t = 2.0 * (s + s * r)
|
|
58
|
+
a_big = t if x > 0 else pi - t
|
|
59
|
+
return a_small if small else a_big
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
@njit(cache=True, fastmath=True, error_model="numpy", inline="always")
|
|
63
|
+
def acos_acc(x, om, op):
|
|
64
|
+
"""arccos(x) from x and accurately computed om = 1 - x, op = 1 + x
|
|
65
|
+
(both >= 0), branch-free: no loss of accuracy near x = +-1, where
|
|
66
|
+
arccos(x) = 2 asin(sqrt((1 - |x|) / 2)) is evaluated from the given
|
|
67
|
+
(1 - |x|). Arguments are clamped to [-1, 1]."""
|
|
68
|
+
x = max(min(x, 1.0), -1.0)
|
|
69
|
+
om = max(om, 0.0)
|
|
70
|
+
op = max(op, 0.0)
|
|
71
|
+
ax = abs(x)
|
|
72
|
+
small = ax <= 0.5
|
|
73
|
+
zz = x * x if small else 0.5 * min(om if x > 0 else op, 1.0)
|
|
74
|
+
s = np.sqrt(zz)
|
|
75
|
+
p = zz * (_PS0 + zz * (_PS1 + zz * (_PS2 + zz * (_PS3 + zz * (_PS4 + zz * _PS5)))))
|
|
76
|
+
q = 1.0 + zz * (_QS1 + zz * (_QS2 + zz * (_QS3 + zz * _QS4)))
|
|
77
|
+
r = p / q
|
|
78
|
+
a_small = _HALF_PI - (x + x * r)
|
|
79
|
+
t = 2.0 * (s + s * r)
|
|
80
|
+
a_big = t if x > 0 else pi - t
|
|
81
|
+
return a_small if small else a_big
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@njit(cache=True, fastmath=True, error_model="numpy", inline="always")
|
|
85
|
+
def limb_terms(z, k):
|
|
86
|
+
"""lex, edx on the limb (Case III) with the arccos arguments and the
|
|
87
|
+
kite area in factored form (accurate at the contact points)."""
|
|
88
|
+
INV_PI = 1 / pi
|
|
89
|
+
k2 = k * k
|
|
90
|
+
z2 = z * z
|
|
91
|
+
zs = max(z, 1e-300)
|
|
92
|
+
zk = max(z * k, 1e-300)
|
|
93
|
+
# factors ordered so that cancelling sums are exact (Sterbenz)
|
|
94
|
+
f1 = (z - 1.0) + k # z + k - 1 >= 0 on the limb
|
|
95
|
+
f2 = (1.0 - z) + k # 1 + k - z
|
|
96
|
+
f3 = (1.0 - k) + z # 1 + z - k
|
|
97
|
+
f4 = 1.0 + z + k
|
|
98
|
+
f5 = f2
|
|
99
|
+
f6 = f1
|
|
100
|
+
# kap1 = acos((1 - k^2 + z^2) / (2 z)), 1 - x = f1 f2 / (2z), 1 + x = f3 f4 / (2z)
|
|
101
|
+
kap1 = acos_acc((1 - k2 + z2) / (2 * zs), f1 * f2 / (2 * zs), f3 * f4 / (2 * zs))
|
|
102
|
+
# kap0 = acos((k^2 + z^2 - 1) / (2 k z)), 1 - x = f5 f3 / (2kz), 1 + x = f6 f4 / (2kz)
|
|
103
|
+
kap0 = acos_acc((k2 + z2 - 1) / (2 * zk), f5 * f3 / (2 * zk), f6 * f4 / (2 * zk))
|
|
104
|
+
kite = np.sqrt(max(f1 * f2 * f3 * f4, 0.0))
|
|
105
|
+
lex = (k2 * kap0 + kap1 - 0.5 * kite) * INV_PI
|
|
106
|
+
edx = 0.5 * INV_PI * (kap1 + k2 * (k2 + 2 * z2) * kap0 - (1 + 5 * k2 + z2) / 4 * kite)
|
|
107
|
+
return lex, edx
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
@njit(cache=True, fastmath=True)
|
|
111
|
+
def ell_combo_kc(kc, p, A, B, C, Cp):
|
|
112
|
+
"""A K + B E + C Pi from one Bulirsch iteration with early exit.
|
|
113
|
+
kc: complementary modulus, p = sqrt(n + 1), Cp = C / p."""
|
|
114
|
+
a1 = A + B
|
|
115
|
+
b1 = A + B * kc * kc
|
|
116
|
+
a2 = C
|
|
117
|
+
b2 = Cp
|
|
118
|
+
e = kc
|
|
119
|
+
m0 = 1.0
|
|
120
|
+
for _ in range(1000):
|
|
121
|
+
f1 = a1
|
|
122
|
+
a1 = a1 + b1 / m0
|
|
123
|
+
b1 = 2 * (b1 + f1 * kc)
|
|
124
|
+
inv_p = 1 / p
|
|
125
|
+
f2 = a2
|
|
126
|
+
a2 = a2 + b2 * inv_p
|
|
127
|
+
g = e * inv_p
|
|
128
|
+
b2 = 2 * (b2 + f2 * g)
|
|
129
|
+
p = g + p
|
|
130
|
+
g = m0
|
|
131
|
+
m0 = kc + m0
|
|
132
|
+
if abs(g - kc) > 1e-8 * g:
|
|
133
|
+
kc = 2 * np.sqrt(e)
|
|
134
|
+
e = kc * m0
|
|
135
|
+
else:
|
|
136
|
+
return 0.5 * pi * ((b1 + a1 * m0) / (2 * m0 * m0) + (b2 + a2 * m0) / (m0 * (m0 + p)))
|
|
137
|
+
return np.nan
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@njit(cache=True, fastmath=True)
|
|
141
|
+
def exact_point(z, k, c1, c2, u2, omega):
|
|
142
|
+
"""Exact flux at one on-star z (0 <= z < 1 + k, z != k, not fully
|
|
143
|
+
occulted), scalar."""
|
|
144
|
+
INV_PI = 1 / pi
|
|
145
|
+
k2 = k * k
|
|
146
|
+
z2 = z * z
|
|
147
|
+
s = z + k
|
|
148
|
+
d = z - k
|
|
149
|
+
x1 = d * d
|
|
150
|
+
x2 = s * s
|
|
151
|
+
x3 = k2 - z2
|
|
152
|
+
sg = 1.0 if d > 0 else -1.0
|
|
153
|
+
if z < 1 - k:
|
|
154
|
+
lex = k2
|
|
155
|
+
edx = k2 / 2 * (k2 + 2 * z2)
|
|
156
|
+
num = ((1.0 - z) - k) * (1 + s)
|
|
157
|
+
den = 1 - x1
|
|
158
|
+
A = 1 - 5 * z2 + k2 + x3 * x3
|
|
159
|
+
p = s / abs(d)
|
|
160
|
+
Cp = 3 * sg
|
|
161
|
+
else:
|
|
162
|
+
lex, edx = limb_terms(z, k)
|
|
163
|
+
num = ((z - 1.0) + k) * (s + 1)
|
|
164
|
+
den = 4 * z * k
|
|
165
|
+
A = (1 - x2) * (2 * x2 + x1 - 3) - 3 * x3 * (x2 - 2)
|
|
166
|
+
p = 1 / abs(d)
|
|
167
|
+
Cp = 3 * s * sg
|
|
168
|
+
sden = np.sqrt(den)
|
|
169
|
+
kc = max(np.sqrt(max(num, 0.0)) / sden, KC_MIN)
|
|
170
|
+
ldx = 2 / 9 * INV_PI / sden * ell_combo_kc(kc, p, A, den * (z2 + 7 * k2 - 4), 3 * s / d, Cp)
|
|
171
|
+
if z < k:
|
|
172
|
+
ldx = ldx + 2 / 3
|
|
173
|
+
return 1 - (c1 * lex + c2 * ldx + u2 * edx) * omega
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
@njit(cache=True, fastmath=True, error_model="numpy")
|
|
177
|
+
def exact_batch(zb, m, k, c1, c2, u2, omega, flux, dev):
|
|
178
|
+
"""Exact flux for zb[:m] (on-star, z != k, not fully occulted), branch-free
|
|
179
|
+
so that LLVM vectorises the loop. dev[j] > 0 marks lanes that need the
|
|
180
|
+
scalar exact_point()."""
|
|
181
|
+
INV_PI = 1 / pi
|
|
182
|
+
k2 = k * k
|
|
183
|
+
for j in range(m):
|
|
184
|
+
z = zb[j]
|
|
185
|
+
z2 = z * z
|
|
186
|
+
s = z + k
|
|
187
|
+
d = z - k
|
|
188
|
+
x1 = d * d
|
|
189
|
+
x2 = s * s
|
|
190
|
+
x3 = k2 - z2
|
|
191
|
+
inner = z < 1 - k
|
|
192
|
+
num = ((1.0 - z) - k) * (1 + s) if inner else ((z - 1.0) + k) * (s + 1)
|
|
193
|
+
den = 1 - x1 if inner else 4 * z * k
|
|
194
|
+
inv_sden = 1 / np.sqrt(den)
|
|
195
|
+
kc = max(np.sqrt(max(num, 0.0)) * inv_sden, KC_MIN)
|
|
196
|
+
sg = 1.0 if d > 0 else -1.0
|
|
197
|
+
inv_ad = 1 / abs(d)
|
|
198
|
+
p = s * inv_ad if inner else inv_ad
|
|
199
|
+
a2 = 3 * s * sg * inv_ad
|
|
200
|
+
b2 = 3 * sg if inner else 3 * s * sg
|
|
201
|
+
A = (1 - 5 * z2 + k2 + x3 * x3) if inner else ((1 - x2) * (2 * x2 + x1 - 3) - 3 * x3 * (x2 - 2))
|
|
202
|
+
B = den * (z2 + 7 * k2 - 4)
|
|
203
|
+
a1 = A + B
|
|
204
|
+
b1 = A + B * kc * kc
|
|
205
|
+
e = kc
|
|
206
|
+
m0 = 1.0
|
|
207
|
+
dd = 0.0
|
|
208
|
+
for _ in range(ELL_BATCH_ITERATIONS):
|
|
209
|
+
r = 1 / (m0 * p)
|
|
210
|
+
f1 = a1
|
|
211
|
+
a1 = a1 + b1 * (p * r)
|
|
212
|
+
b1 = 2 * (b1 + f1 * kc)
|
|
213
|
+
inv_p = m0 * r
|
|
214
|
+
f2 = a2
|
|
215
|
+
a2 = a2 + b2 * inv_p
|
|
216
|
+
g = e * inv_p
|
|
217
|
+
b2 = 2 * (b2 + f2 * g)
|
|
218
|
+
p = g + p
|
|
219
|
+
g = m0
|
|
220
|
+
m0 = kc + m0
|
|
221
|
+
dd = abs(g - kc) - 1e-8 * g
|
|
222
|
+
kc = 2 * np.sqrt(e)
|
|
223
|
+
e = kc * m0
|
|
224
|
+
mp = m0 + p
|
|
225
|
+
ell = 0.25 * pi * ((b1 + a1 * m0) * mp + 2 * m0 * (b2 + a2 * m0)) / (m0 * m0 * mp)
|
|
226
|
+
ldx = (2 / 9 * INV_PI) * inv_sden * ell + (2 / 3 if z < k else 0.0)
|
|
227
|
+
lex_l, edx_l = limb_terms(z, k)
|
|
228
|
+
lex = k2 if inner else lex_l
|
|
229
|
+
edx = k2 / 2 * (k2 + 2 * z2) if inner else edx_l
|
|
230
|
+
flux[j] = 1 - (c1 * lex + c2 * ldx + u2 * edx) * omega
|
|
231
|
+
dev[j] = dd
|
pizzetti/_general.py
ADDED
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"""Flux for intensity laws I(mu) = sum_i c_i mu^(alpha_i) where the occultor
|
|
2
|
+
touches or crosses the stellar limb, by Green's theorem.
|
|
3
|
+
|
|
4
|
+
With the radial primitive F_i(r) = int_0^r (1 - s^2)^g s ds
|
|
5
|
+
= (1 - (1 - r^2)^(g+1)) / (2 (g + 1)), g = alpha_i / 2, the field
|
|
6
|
+
(F(r)/r^2) (x, y) has divergence (1 - r^2)^g and is smooth at r = 0. The
|
|
7
|
+
blocked flux is the outward flux through the boundary of the occulted part
|
|
8
|
+
of the stellar disk:
|
|
9
|
+
* stellar-limb arc inside the occultor (r = 1): F(1) * 2 theta_s,
|
|
10
|
+
cos theta_s = (1 + z^2 - p^2) / (2 z);
|
|
11
|
+
* occultor arc inside the star, points (z + p cos t, p sin t),
|
|
12
|
+
cos t < c = (1 - z^2 - p^2) / (2 z p):
|
|
13
|
+
2 p int_{t0}^{pi} (F(r)/r^2) (p + z cos t) dt, t0 = arccos(c).
|
|
14
|
+
The arc integral uses composite Gauss-Legendre quadrature on segments that
|
|
15
|
+
grow geometrically away from the nearest singularity of (1 - r^2)^(g+1)
|
|
16
|
+
(at t = -t0, or at +-i sqrt(y_min/(z p)) for an interior disk); the first
|
|
17
|
+
segment uses t = t0 + L u^2 to remove the (t - t0)^(g+1) endpoint behaviour.
|
|
18
|
+
"""
|
|
19
|
+
from math import pi
|
|
20
|
+
|
|
21
|
+
import numpy as np
|
|
22
|
+
from numba import njit
|
|
23
|
+
|
|
24
|
+
#: Gauss-Legendre nodes per segment of the limb arc integral (geometric
|
|
25
|
+
#: segments towards the nearest singularity).
|
|
26
|
+
N_NODES = 12
|
|
27
|
+
_X, _W = np.polynomial.legendre.leggauss(N_NODES)
|
|
28
|
+
GL_U = 0.5 * (_X + 1.0)
|
|
29
|
+
GL_W = 0.5 * _W
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@njit(cache=True, fastmath=False)
|
|
33
|
+
def _h(x, y, g):
|
|
34
|
+
"""(1 - y^(g+1)) / (2 (g+1) x) with x = r^2 and y = 1 - r^2 (both given
|
|
35
|
+
accurately), stable for x -> 0 and y -> 0."""
|
|
36
|
+
if x < 1e-8:
|
|
37
|
+
return 0.5 * (1.0 - 0.5 * g * x)
|
|
38
|
+
if y <= 0.0:
|
|
39
|
+
return 0.5 / ((g + 1.0) * x)
|
|
40
|
+
if x < 0.5:
|
|
41
|
+
return -np.expm1((g + 1.0) * np.log1p(-x)) / (2.0 * (g + 1.0) * x)
|
|
42
|
+
return (1.0 - np.exp((g + 1.0) * np.log(y))) / (2.0 * (g + 1.0) * x)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@njit(cache=True, fastmath=False)
|
|
46
|
+
def _acos_acc(x, om, op):
|
|
47
|
+
"""arccos(x) from accurate 1 - x (om) and 1 + x (op)."""
|
|
48
|
+
if x > 0.5:
|
|
49
|
+
return 2.0 * np.arcsin(np.sqrt(max(om, 0.0) / 2.0))
|
|
50
|
+
if x < -0.5:
|
|
51
|
+
return pi - 2.0 * np.arcsin(np.sqrt(max(op, 0.0) / 2.0))
|
|
52
|
+
return np.arccos(x)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@njit(cache=True, fastmath=False)
|
|
56
|
+
def blocked_limb(z, p, gams, cs, gu, gw):
|
|
57
|
+
"""sum_i c_i * (blocked flux of (1 - r^2)^{g_i}) for an occultor that
|
|
58
|
+
touches or crosses the limb (or covers the star)."""
|
|
59
|
+
nt = gams.shape[0]
|
|
60
|
+
tot = 0.0
|
|
61
|
+
need_log = False
|
|
62
|
+
for i in range(nt):
|
|
63
|
+
gi = gams[i]
|
|
64
|
+
if not (gi == 0.0 or gi == 1.0 or gi == 0.5 or gi == 0.25 or gi == 0.75):
|
|
65
|
+
need_log = True
|
|
66
|
+
# stellar-limb arc inside the occultor. cos(theta_s) = (1 + z^2 - p^2)/(2z),
|
|
67
|
+
# 1 - cos = (z-1+p)(1-z+p)/(2z), 1 + cos = (1-p+z)(1+z+p)/(2z); the
|
|
68
|
+
# cancelling sums are ordered so that they are exact (Sterbenz), and the
|
|
69
|
+
# case decisions use them (not the rounded cosine).
|
|
70
|
+
if z > 0.0:
|
|
71
|
+
om = ((z - 1.0) + p) * ((1.0 - z) + p) / (2.0 * z)
|
|
72
|
+
op = ((1.0 - p) + z) * (1.0 + z + p) / (2.0 * z)
|
|
73
|
+
if om <= 0.0:
|
|
74
|
+
th = 0.0
|
|
75
|
+
elif op <= 0.0:
|
|
76
|
+
th = pi
|
|
77
|
+
else:
|
|
78
|
+
th = _acos_acc(1.0 - om if om < op else op - 1.0, om, op)
|
|
79
|
+
else:
|
|
80
|
+
th = pi if p >= 1.0 else 0.0
|
|
81
|
+
if th > 0.0:
|
|
82
|
+
for i in range(nt):
|
|
83
|
+
tot += cs[i] * 2.0 * th * 0.5 / (gams[i] + 1.0)
|
|
84
|
+
# occultor arc inside the star: cos t < c = (1 - z^2 - p^2) / (2 z p),
|
|
85
|
+
# 1 - c = (z-1+p)(z+p+1)/(2zp), 1 + c = (1-z+p)(1-p+z)/(2zp)
|
|
86
|
+
if z > 0.0 and p > 0.0:
|
|
87
|
+
omc = ((z - 1.0) + p) * (z + p + 1.0) / (2.0 * z * p)
|
|
88
|
+
opc = ((1.0 - z) + p) * ((1.0 - p) + z) / (2.0 * z * p)
|
|
89
|
+
arc = opc > 0.0
|
|
90
|
+
else:
|
|
91
|
+
omc = 0.0 if p < 1.0 else 2.0
|
|
92
|
+
opc = 2.0 if p < 1.0 else 0.0
|
|
93
|
+
arc = p < 1.0
|
|
94
|
+
if arc:
|
|
95
|
+
if omc <= 0.0:
|
|
96
|
+
t0 = 0.0
|
|
97
|
+
else:
|
|
98
|
+
t0 = _acos_acc(1.0 - omc if omc < opc else opc - 1.0, omc, opc)
|
|
99
|
+
zp = z * p
|
|
100
|
+
omz2p2 = ((1.0 - z) - p) * (1.0 + z + p) + 2.0 * zp # 1 - z^2 - p^2
|
|
101
|
+
# length scale of the nearest singularity of (1 - r^2)^(g+1) in t:
|
|
102
|
+
# the second zero at -t0 (limb), or +-i sqrt(y_min / (z p)) (interior)
|
|
103
|
+
if t0 > 0.0:
|
|
104
|
+
scale = t0
|
|
105
|
+
else:
|
|
106
|
+
ymin = ((1.0 - z) - p) * (1.0 + z + p)
|
|
107
|
+
scale = np.sqrt(max(ymin, 0.0) / max(zp, 1e-300)) if zp > 0.0 else pi
|
|
108
|
+
scale = min(max(scale, 1e-12), pi - t0)
|
|
109
|
+
a = t0
|
|
110
|
+
b = t0 + scale
|
|
111
|
+
first = True
|
|
112
|
+
for _ in range(64):
|
|
113
|
+
L = b - a
|
|
114
|
+
for n in range(gu.shape[0]):
|
|
115
|
+
u = gu[n]
|
|
116
|
+
if first: # t = t0 + L u^2: removes the (t - t0)^(g+1) endpoint
|
|
117
|
+
t = a + L * u * u
|
|
118
|
+
jac = 2.0 * L * u * gw[n]
|
|
119
|
+
else:
|
|
120
|
+
t = a + L * u
|
|
121
|
+
jac = L * gw[n]
|
|
122
|
+
ct = np.cos(t)
|
|
123
|
+
# 1 - r^2 = 2 z p (cos t0 - cos t); cos t0 = 1 - omc from its accurate form
|
|
124
|
+
if t0 > 0.0:
|
|
125
|
+
y = 2.0 * zp * ((1.0 - ct) - omc)
|
|
126
|
+
else:
|
|
127
|
+
y = omz2p2 - 2.0 * zp * ct
|
|
128
|
+
y = min(max(y, 0.0), 1.0)
|
|
129
|
+
r2 = max(1.0 - y, 0.0) if y > 0.5 else min(max(z * z + p * p + 2.0 * zp * ct, 0.0), 1.0)
|
|
130
|
+
geo = 2.0 * p * (p + z * ct) * jac
|
|
131
|
+
sy = np.sqrt(y)
|
|
132
|
+
qy = np.sqrt(sy)
|
|
133
|
+
ly = np.log(y) if (y > 0.0 and need_log) else -745.0
|
|
134
|
+
l1x = np.log1p(-r2) if r2 < 0.05 else 0.0
|
|
135
|
+
for i in range(nt):
|
|
136
|
+
gi = gams[i]
|
|
137
|
+
if gi == 0.0:
|
|
138
|
+
hv = 0.5
|
|
139
|
+
elif gi == 1.0:
|
|
140
|
+
hv = 0.25 * (1.0 + y)
|
|
141
|
+
elif r2 < 1e-8:
|
|
142
|
+
hv = 0.5 * (1.0 - 0.5 * gi * r2)
|
|
143
|
+
elif r2 < 0.05:
|
|
144
|
+
hv = -np.expm1((gi + 1.0) * l1x) / (2.0 * (gi + 1.0) * r2)
|
|
145
|
+
elif gi == 0.5:
|
|
146
|
+
hv = (1.0 - y * sy) / (3.0 * r2)
|
|
147
|
+
elif gi == 0.25:
|
|
148
|
+
hv = (1.0 - y * qy) / (2.5 * r2)
|
|
149
|
+
elif gi == 0.75:
|
|
150
|
+
hv = (1.0 - y * sy * qy) / (3.5 * r2)
|
|
151
|
+
else:
|
|
152
|
+
hv = (1.0 - np.exp((gi + 1.0) * ly)) / (2.0 * (gi + 1.0) * r2)
|
|
153
|
+
tot += cs[i] * hv * geo
|
|
154
|
+
if b >= pi:
|
|
155
|
+
break
|
|
156
|
+
first = False
|
|
157
|
+
a = b
|
|
158
|
+
b = min(t0 + 3.0 * (b - t0), pi)
|
|
159
|
+
if pi - b < 0.25 * (b - a):
|
|
160
|
+
b = pi
|
|
161
|
+
return tot
|
pizzetti/_orbit.py
ADDED
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
"""Sky-projected separation and true anomaly (same conventions as batman's _rsky)."""
|
|
2
|
+
from math import pi
|
|
3
|
+
|
|
4
|
+
import numpy as np
|
|
5
|
+
from numba import njit
|
|
6
|
+
|
|
7
|
+
#: separation returned when the planet is behind the star (primary) or in
|
|
8
|
+
#: front of it (secondary), as in batman
|
|
9
|
+
BIGD = 100.0
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# fdlibm __kernel_sin / __kernel_cos coefficients on [-pi/4, pi/4]
|
|
13
|
+
_S1 = -1.66666666666666324348e-01
|
|
14
|
+
_S2 = 8.33333333332248946124e-03
|
|
15
|
+
_S3 = -1.98412698298579493134e-04
|
|
16
|
+
_S4 = 2.75573137070700676789e-06
|
|
17
|
+
_S5 = -2.50507602534068634195e-08
|
|
18
|
+
_S6 = 1.58969099521155010221e-10
|
|
19
|
+
_C1 = 4.16666666666666019037e-02
|
|
20
|
+
_C2 = -1.38888888888741095749e-03
|
|
21
|
+
_C3 = 2.48015872894767294178e-05
|
|
22
|
+
_C4 = -2.75573143513906633035e-07
|
|
23
|
+
_C5 = 2.08757232129817482790e-09
|
|
24
|
+
_C6 = -1.13596475577881948265e-11
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@njit(cache=True, fastmath=True, inline="always")
|
|
28
|
+
def sin_turns(u):
|
|
29
|
+
"""sin(2 pi u) for a phase u in turns, branch-free polynomial (vectorises);
|
|
30
|
+
error ~1e-16 independent of |u| (the phase is reduced in turns)."""
|
|
31
|
+
f = u - np.floor(u + 0.5)
|
|
32
|
+
q = np.floor(4.0 * f + 0.5)
|
|
33
|
+
r = 2.0 * pi * (f - 0.25 * q)
|
|
34
|
+
z = r * r
|
|
35
|
+
s = r + r * z * (_S1 + z * (_S2 + z * (_S3 + z * (_S4 + z * (_S5 + z * _S6)))))
|
|
36
|
+
c = 1.0 - 0.5 * z + z * z * (_C1 + z * (_C2 + z * (_C3 + z * (_C4 + z * (_C5 + z * _C6)))))
|
|
37
|
+
k = np.int64(q) & 3
|
|
38
|
+
v = c if (k & 1) == 1 else s
|
|
39
|
+
return -v if k >= 2 else v
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@njit(cache=True, fastmath=True)
|
|
43
|
+
def _rsky_circular(t, tp, per, a, si, w_turns, transittype, d):
|
|
44
|
+
"""Circular orbit: d = a sqrt(1 - sin^2(f + w) sin^2 i), f = 2 pi (t - tp) / per."""
|
|
45
|
+
inv_per = 1.0 / per
|
|
46
|
+
for i in range(t.shape[0]):
|
|
47
|
+
s = sin_turns((t[i] - tp) * inv_per + w_turns)
|
|
48
|
+
ss = s * si
|
|
49
|
+
dd = a * np.sqrt(max(1.0 - ss * ss, 0.0))
|
|
50
|
+
bad = (ss <= 0.0) if transittype == 1 else (ss >= 0.0)
|
|
51
|
+
d[i] = BIGD if bad else dd
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@njit(cache=True)
|
|
55
|
+
def eccentric_anomaly(M, e):
|
|
56
|
+
"""Solve Kepler's equation E - e sin E = M (Newton, robust start)."""
|
|
57
|
+
Mr = M - 2.0 * pi * np.floor(M / (2.0 * pi))
|
|
58
|
+
E = Mr + e * np.sin(Mr) if e < 0.8 else pi
|
|
59
|
+
for _ in range(100):
|
|
60
|
+
f = E - e * np.sin(E) - Mr
|
|
61
|
+
dE = f / (1.0 - e * np.cos(E))
|
|
62
|
+
E -= dE
|
|
63
|
+
if abs(dE) < 1e-15 * max(1.0, abs(E)):
|
|
64
|
+
break
|
|
65
|
+
return E + (M - Mr)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@njit(cache=True)
|
|
69
|
+
def _true_anomaly(t, tc, per, ecc, omega):
|
|
70
|
+
f0 = pi / 2.0 - omega
|
|
71
|
+
if ecc < 1.0e-5:
|
|
72
|
+
E = f0
|
|
73
|
+
M = f0
|
|
74
|
+
else:
|
|
75
|
+
E = 2.0 * np.arctan(np.sqrt((1.0 - ecc) / (1.0 + ecc)) * np.tan(f0 / 2.0))
|
|
76
|
+
M = E - ecc * np.sin(E)
|
|
77
|
+
tp = tc - per * M / 2.0 / pi
|
|
78
|
+
n = t.shape[0]
|
|
79
|
+
f = np.empty(n)
|
|
80
|
+
if ecc < 1.0e-5:
|
|
81
|
+
for i in range(n):
|
|
82
|
+
x = (t[i] - tp) / per
|
|
83
|
+
f[i] = (x - np.trunc(x)) * 2.0 * pi
|
|
84
|
+
else:
|
|
85
|
+
sq = np.sqrt((1.0 + ecc) / (1.0 - ecc))
|
|
86
|
+
nm = 2.0 * pi / per
|
|
87
|
+
for i in range(n):
|
|
88
|
+
E = eccentric_anomaly(nm * (t[i] - tp), ecc)
|
|
89
|
+
f[i] = 2.0 * np.arctan(sq * np.tan(E / 2.0))
|
|
90
|
+
return f
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
@njit(cache=True)
|
|
94
|
+
def rsky(t, tc, per, a, inc, ecc, omega, transittype):
|
|
95
|
+
"""Separation of centres in stellar radii; inc and omega in radians;
|
|
96
|
+
transittype 1 (primary) or 2 (secondary)."""
|
|
97
|
+
n = t.shape[0]
|
|
98
|
+
d = np.empty(n)
|
|
99
|
+
si = np.sin(inc)
|
|
100
|
+
if ecc < 1.0e-5:
|
|
101
|
+
# time of periastron as in batman (true = mean anomaly for e = 0)
|
|
102
|
+
tp = tc - per * (pi / 2.0 - omega) / 2.0 / pi
|
|
103
|
+
_rsky_circular(t, tp, per, a, si, omega / (2.0 * pi), transittype, d)
|
|
104
|
+
return d
|
|
105
|
+
f = _true_anomaly(t, tc, per, ecc, omega)
|
|
106
|
+
for i in range(n):
|
|
107
|
+
s = np.sin(f[i] + omega)
|
|
108
|
+
if transittype == 1 and s * si <= 0.0:
|
|
109
|
+
d[i] = BIGD
|
|
110
|
+
elif transittype == 2 and s * si >= 0.0:
|
|
111
|
+
d[i] = BIGD
|
|
112
|
+
else:
|
|
113
|
+
d[i] = a * (1.0 - ecc * ecc) / (1.0 + ecc * np.cos(f[i])) * np.sqrt(1.0 - s * s * si * si)
|
|
114
|
+
return d
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
@njit(cache=True)
|
|
118
|
+
def true_anomaly(t, tc, per, ecc, omega):
|
|
119
|
+
return _true_anomaly(t, tc, per, ecc, omega)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
@njit(cache=True)
|
|
123
|
+
def eclipse(d, p, fp):
|
|
124
|
+
"""Secondary eclipse with a uniform planet disk (batman's _eclipse)."""
|
|
125
|
+
if abs(p - 0.5) < 1.0e-3:
|
|
126
|
+
p = 0.5
|
|
127
|
+
n = d.shape[0]
|
|
128
|
+
out = np.empty(n)
|
|
129
|
+
for i in range(n):
|
|
130
|
+
x = d[i]
|
|
131
|
+
if x >= 1.0 + p:
|
|
132
|
+
out[i] = 1.0 + fp
|
|
133
|
+
elif x < 1.0 - p:
|
|
134
|
+
out[i] = 1.0
|
|
135
|
+
else:
|
|
136
|
+
kap1 = np.arccos(min((1.0 - p * p + x * x) / 2.0 / x, 1.0))
|
|
137
|
+
kap0 = np.arccos(min((p * p + x * x - 1.0) / 2.0 / p / x, 1.0))
|
|
138
|
+
at = (p * p * kap0 + kap1 - 0.5 * np.sqrt(max(4.0 * x * x - (1.0 + x * x - p * p) ** 2, 0.0))) / pi
|
|
139
|
+
out[i] = 1.0 + fp * (1.0 - at / p / p)
|
|
140
|
+
return out
|