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 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