• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

dsavransky / EXOSIMS / 11294204788

11 Oct 2024 02:30PM UTC coverage: 66.032%. First build
11294204788

Pull #395

github

web-flow
Merge 79a05365c into 18ce85989
Pull Request #395: np.Inf and np.array(obj, copy = False) changed

86 of 104 new or added lines in 26 files covered. (82.69%)

9543 of 14452 relevant lines covered (66.03%)

0.66 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

77.39
/EXOSIMS/Completeness/GarrettCompleteness.py
1
# -*- coding: utf-8 -*-
2
from EXOSIMS.Completeness.BrownCompleteness import BrownCompleteness
1✔
3
import numpy as np
1✔
4
import os
1✔
5
import hashlib
1✔
6
import scipy.optimize as optimize
1✔
7
import scipy.interpolate as interpolate
1✔
8
import scipy.integrate as integrate
1✔
9
import astropy.units as u
1✔
10
import pickle
1✔
11
from EXOSIMS.util.memoize import memoize
1✔
12
from tqdm import tqdm
1✔
13
from EXOSIMS.util._numpy_compat import copy_if_needed
1✔
14

15

16
class GarrettCompleteness(BrownCompleteness):
1✔
17
    """Analytical Completeness class
18

19
    This class contains all variables and methods necessary to perform
20
    Completeness Module calculations based on Garrett and Savransky 2016
21
    in exoplanet mission simulation.
22

23
    The completeness calculations performed by this method assume that all
24
    planetary parameters are independently distributed. The probability density
25
    functions used here are either independent or marginalized from a joint
26
    probability density function.
27

28
    Args:
29
        order_of_quadrature (int):
30
            The order of quadrature used in the comp_dmag function's fixed quad
31
            integration. Higher values will give marginal improvements in the
32
            comp_calc completeness values, but are slower.
33
        **specs:
34
            :ref:`sec:inputspec`
35

36
    Attributes:
37
        updates (nx5 ndarray):
38
            Completeness values of successive observations of each star in the
39
            target list (initialized in gen_update)
40

41
    """
42

43
    def __init__(self, order_of_quadrature=15, **specs):
1✔
44

45
        # Set order of quadrature used in comp_dmag
46
        self.order_of_quadrature = int(order_of_quadrature)
1✔
47

48
        # Run BrownCompleteness init
49
        BrownCompleteness.__init__(self, **specs)
1✔
50

51
        self._outspec["order_of_quadrature"] = self.order_of_quadrature
1✔
52

53
    def completeness_setup(self):
1✔
54
        """Preform any preliminary calculations needed for this flavor of completeness
55

56
        For GarrettCompleteness this involves setting up various interpolants.
57
        See [Garrett2016]_ for details.
58
        """
59
        # get unitless values of population parameters
60
        self.amin = float(self.PlanetPopulation.arange.min().value)
1✔
61
        self.amax = float(self.PlanetPopulation.arange.max().value)
1✔
62
        self.emin = float(self.PlanetPopulation.erange.min())
1✔
63
        self.emax = float(self.PlanetPopulation.erange.max())
1✔
64
        self.pmin = float(self.PlanetPopulation.prange.min())
1✔
65
        self.pmax = float(self.PlanetPopulation.prange.max())
1✔
66
        self.Rmin = float(self.PlanetPopulation.Rprange.min().to("earthRad").value)
1✔
67
        self.Rmax = float(self.PlanetPopulation.Rprange.max().to("earthRad").value)
1✔
68
        if self.PlanetPopulation.constrainOrbits:
1✔
69
            self.rmin = self.amin
×
70
            self.rmax = self.amax
×
71
        else:
72
            self.rmin = self.amin * (1.0 - self.emax)
1✔
73
            self.rmax = self.amax * (1.0 + self.emax)
1✔
74
        self.zmin = self.pmin * self.Rmin**2
1✔
75
        self.zmax = self.pmax * self.Rmax**2
1✔
76
        # conversion factor
77
        self.x = float(u.earthRad.to("AU"))
1✔
78
        # distributions needed
79
        self.dist_sma = self.PlanetPopulation.dist_sma
1✔
80
        self.dist_eccen = self.PlanetPopulation.dist_eccen
1✔
81
        self.dist_eccen_con = self.PlanetPopulation.dist_eccen_from_sma
1✔
82
        self.dist_albedo = self.PlanetPopulation.dist_albedo
1✔
83
        self.dist_radius = self.PlanetPopulation.dist_radius
1✔
84
        # are any of a, e, p, Rp constant?
85
        self.aconst = self.amin == self.amax
1✔
86
        self.econst = self.emin == self.emax
1✔
87
        self.pconst = self.pmin == self.pmax
1✔
88
        self.Rconst = self.Rmin == self.Rmax
1✔
89
        # degenerate case where aconst, econst and e = 0
90
        assert not (
1✔
91
            all([self.aconst, self.econst, self.pconst, self.Rconst]) and self.emax == 0
92
        ), (
93
            "At least one parameter (out of semi-major axis, albedo, and radius) must "
94
            "vary when eccentricity is constant and zero."
95
        )
96
        # solve for bstar
97
        beta = np.linspace(0.0, np.pi, 1000) * u.rad
1✔
98
        Phis = self.PlanetPhysicalModel.calc_Phi(beta)
1✔
99
        # Interpolant for phase function which removes astropy Quantity
100
        self.Phi = interpolate.InterpolatedUnivariateSpline(
1✔
101
            beta.value, Phis, k=3, ext=1
102
        )
103
        self.Phiinv = interpolate.InterpolatedUnivariateSpline(
1✔
104
            Phis[::-1], beta.value[::-1], k=3, ext=1
105
        )
106
        # get numerical derivative of phase function
107
        dPhis = np.zeros(beta.shape)
1✔
108
        db = beta[1].value - beta[0].value
1✔
109
        dPhis[0:1] = (
1✔
110
            -25.0 * Phis[0:1]
111
            + 48.0 * Phis[1:2]
112
            - 36.0 * Phis[2:3]
113
            + 16.0 * Phis[3:4]
114
            - 3.0 * Phis[4:5]
115
        ) / (12.0 * db)
116
        dPhis[-2:-1] = (
1✔
117
            25.0 * Phis[-2:-1]
118
            - 48.0 * Phis[-3:-2]
119
            + 36.0 * Phis[-4:-3]
120
            - 16.0 * Phis[-5:-4]
121
            + 3.0 * Phis[-6:-5]
122
        ) / (12.0 * db)
123
        dPhis[2:-2] = (Phis[0:-4] - 8.0 * Phis[1:-3] + 8.0 * Phis[3:-1] - Phis[4:]) / (
1✔
124
            12.0 * db
125
        )
126
        self.dPhi = interpolate.InterpolatedUnivariateSpline(
1✔
127
            beta.value, dPhis, k=3, ext=1
128
        )
129
        # solve for bstar
130
        f = lambda b: 2.0 * np.sin(b) * np.cos(b) * self.Phi(b) + np.sin(
1✔
131
            b
132
        ) ** 2 * self.dPhi(b)
133
        self.bstar = float(optimize.root(f, np.pi / 3.0).x)
1✔
134
        # helpful constants
135
        self.cdmin1 = -2.5 * np.log10(self.pmax * (self.Rmax * self.x / self.rmin) ** 2)
1✔
136
        self.cdmin2 = -2.5 * np.log10(
1✔
137
            self.pmax
138
            * (self.Rmax * self.x * np.sin(self.bstar)) ** 2
139
            * self.Phi(self.bstar)
140
        )
141
        self.cdmin3 = -2.5 * np.log10(self.pmax * (self.Rmax * self.x / self.rmax) ** 2)
1✔
142
        self.cdmax = -2.5 * np.log10(self.pmin * (self.Rmin * self.x / self.rmax) ** 2)
1✔
143
        self.val = np.sin(self.bstar) ** 2 * self.Phi(self.bstar)
1✔
144
        self.d1 = -2.5 * np.log10(self.pmax * (self.Rmax * self.x / self.rmin) ** 2)
1✔
145
        self.d2 = -2.5 * np.log10(
1✔
146
            self.pmax * (self.Rmax * self.x / self.rmin) ** 2 * self.Phi(self.bstar)
147
        )
148
        self.d3 = -2.5 * np.log10(
1✔
149
            self.pmax * (self.Rmax * self.x / self.rmax) ** 2 * self.Phi(self.bstar)
150
        )
151
        self.d4 = -2.5 * np.log10(
1✔
152
            self.pmax * (self.Rmax * self.x / self.rmax) ** 2 * self.Phi(np.pi / 2.0)
153
        )
154
        self.d5 = -2.5 * np.log10(
1✔
155
            self.pmin * (self.Rmin * self.x / self.rmax) ** 2 * self.Phi(np.pi / 2.0)
156
        )
157
        # vectorize scalar methods
158
        self.rgrand2v = np.vectorize(self.rgrand2, otypes=[np.float64])
1✔
159
        self.f_dmagsv = np.vectorize(self.f_dmags, otypes=[np.float64])
1✔
160
        self.f_sdmagv = np.vectorize(self.f_sdmag, otypes=[np.float64])
1✔
161
        self.f_dmagv = np.vectorize(self.f_dmag, otypes=[np.float64])
1✔
162
        self.f_sv = np.vectorize(self.f_s, otypes=[np.float64])
1✔
163
        self.mindmagv = np.vectorize(self.mindmag, otypes=[np.float64])
1✔
164
        self.maxdmagv = np.vectorize(self.maxdmag, otypes=[np.float64])
1✔
165
        # inverse functions for phase angle
166
        b1 = np.linspace(0.0, self.bstar, 20000)
1✔
167
        # b < bstar
168
        self.binv1 = interpolate.InterpolatedUnivariateSpline(
1✔
169
            np.sin(b1) ** 2 * self.Phi(b1), b1, k=1, ext=1
170
        )
171
        b2 = np.linspace(self.bstar, np.pi, 20000)
1✔
172
        b2val = np.sin(b2) ** 2 * self.Phi(b2)
1✔
173
        # b > bstar
174
        self.binv2 = interpolate.InterpolatedUnivariateSpline(
1✔
175
            b2val[::-1], b2[::-1], k=1, ext=1
176
        )
177
        if self.rmin != self.rmax:
1✔
178
            # get pdf of r
179
            self.vprint("Generating pdf of orbital radius")
1✔
180
            r = np.linspace(self.rmin, self.rmax, 1000)
1✔
181
            fr = np.zeros(r.shape)
1✔
182
            for i in range(len(r)):
1✔
183
                fr[i] = self.f_r(r[i])
1✔
184
            self.dist_r = interpolate.InterpolatedUnivariateSpline(r, fr, k=3, ext=1)
1✔
185
            self.vprint("Finished pdf of orbital radius")
1✔
186
        if not all([self.pconst, self.Rconst]):
1✔
187
            # get pdf of p*R**2
188
            self.vprint("Generating pdf of albedo times planetary radius squared")
1✔
189
            z = np.linspace(self.zmin, self.zmax, 1000)
1✔
190
            fz = np.zeros(z.shape)
1✔
191
            for i in range(len(z)):
1✔
192
                fz[i] = self.f_z(z[i])
1✔
193
            self.dist_z = interpolate.InterpolatedUnivariateSpline(z, fz, k=3, ext=1)
1✔
194
            self.vprint("Finished pdf of albedo times planetary radius squared")
1✔
195

196
    def target_completeness(self, TL):
1✔
197
        """Generates completeness values for target stars
198

199
        This method is called from TargetList __init__ method.
200

201
        Args:
202
            TL (TargetList module):
203
                TargetList class object
204

205
        Returns:
206
            ~numpy.ndarray(float)):
207
                int_comp: 1D numpy array of completeness values for each target star
208

209
        """
210

211
        OS = TL.OpticalSystem
1✔
212
        if TL.calc_char_int_comp:
1✔
213
            mode = list(
×
214
                filter(lambda mode: "spec" in mode["inst"]["name"], OS.observingModes)
215
            )[0]
216
        else:
217
            mode = list(filter(lambda mode: mode["detectionMode"], OS.observingModes))[
1✔
218
                0
219
            ]
220

221
        # To limit the amount of computation, we want to find the most common
222
        # int_dMag value (typically the one the user sets in the input json since
223
        # int_dMag is either the user input or the intCutoff_dMag).
224
        vals, counts = np.unique(TL.int_dMag, return_counts=True)
1✔
225
        self.mode_dMag = vals[np.argwhere(counts == np.max(counts))[0][0]]
1✔
226
        mode_dMag_mask = TL.int_dMag == self.mode_dMag
1✔
227

228
        # important PlanetPopulation attributes
229
        atts = list(self.PlanetPopulation.__dict__)
1✔
230
        extstr = ""
1✔
231
        for att in sorted(atts, key=str.lower):
1✔
232
            if (
1✔
233
                not callable(getattr(self.PlanetPopulation, att))
234
                and att != "PlanetPhysicalModel"
235
            ):
236
                extstr += "%s: " % att + str(getattr(self.PlanetPopulation, att)) + " "
1✔
237
        # include mode_dMag and intCutoff_dMag
238
        extstr += (
1✔
239
            "%s: " % "mode_dMag"
240
            + str(self.mode_dMag)
241
            + f"intCutoff_dMag: {TL.intCutoff_dMag}"
242
            + " "
243
        )
244
        ext = hashlib.md5(extstr.encode("utf-8")).hexdigest()
1✔
245
        self.filename += ext
1✔
246
        Cpath = os.path.join(self.cachedir, self.filename + ".acomp")
1✔
247

248
        # calculate separations based on IWA
249
        IWA = mode["IWA"]
1✔
250
        OWA = mode["OWA"]
1✔
251
        smin = (np.tan(IWA) * TL.dist).to("AU").value
1✔
252
        if np.isinf(OWA):
1✔
253
            smax = np.array([self.rmax] * len(smin))
×
254
        else:
255
            smax = (np.tan(OWA) * TL.dist).to("AU").value
1✔
256
            smax[smax > self.rmax] = self.rmax
1✔
257

258
        int_comp = np.zeros(smin.shape)
1✔
259
        # calculate dMags based on maximum dMag
260
        if self.PlanetPopulation.scaleOrbits:
1✔
261
            L = np.where(TL.L > 0, TL.L, 1e-10)  # take care of zero/negative values
×
262
            smin = smin / np.sqrt(L)
×
263
            smax = smax / np.sqrt(L)
×
264
            dMag_vals = TL.int_dMag - 2.5 * np.log10(L)
×
265
            separation_mask = smin < self.rmax
×
266
            int_comp[separation_mask] = self.comp_s(
×
267
                smin[separation_mask], smax[separation_mask], dMag_vals[separation_mask]
268
            )
269
        else:
270
            # In this case we find where the mode dMag value is also in the
271
            # separation range and use the vectorized integral since they have
272
            # the same dMag value. Where the dMag values are not the mode we
273
            # must use comp_s which is slower
274
            dMag_vals = TL.int_dMag
1✔
275
            separation_mask = smin < self.rmax
1✔
276
            dist_s = self.genComp(Cpath, TL)
1✔
277
            dist_sv = np.vectorize(dist_s.integral, otypes=[np.float64])
1✔
278
            separation_mode_mask = separation_mask & mode_dMag_mask
1✔
279
            separation_not_mode_mask = separation_mask & ~mode_dMag_mask
1✔
280
            int_comp[separation_mode_mask] = dist_sv(
1✔
281
                smin[separation_mode_mask], smax[separation_mode_mask]
282
            )
283
            int_comp[separation_not_mode_mask] = self.comp_s(
1✔
284
                smin[separation_not_mode_mask],
285
                smax[separation_not_mode_mask],
286
                dMag_vals[separation_not_mode_mask],
287
            )
288

289
        # ensure that completeness values are between 0 and 1
290
        int_comp = np.clip(int_comp, 0.0, 1.0)
1✔
291

292
        return int_comp
1✔
293

294
    def genComp(self, Cpath, TL):
1✔
295
        """Generates function to get completeness values
296

297
        Args:
298
            Cpath (str):
299
                Path to pickled dictionary containing interpolant function
300
            TL (TargetList module):
301
                TargetList class object
302

303
        Returns:
304
            dist_s (callable(s)):
305
                Marginalized to self.mode_dMag probability density function for
306
                projected separation
307

308
        """
309

310
        if os.path.exists(Cpath):
1✔
311
            # dist_s interpolant already exists for parameters
312
            self.vprint("Loading cached completeness file from %s" % Cpath)
×
313
            try:
×
314
                with open(Cpath, "rb") as ff:
×
315
                    H = pickle.load(ff)
×
316
            except UnicodeDecodeError:
×
317
                with open(Cpath, "rb") as ff:
×
318
                    H = pickle.load(ff, encoding="latin1")
×
319
            self.vprint("Completeness loaded from cache.")
×
320
            dist_s = H["dist_s"]
×
321
        else:
322
            # generate dist_s interpolant and pickle it
323
            self.vprint('Cached completeness file not found at "%s".' % Cpath)
1✔
324
            self.vprint("Generating completeness.")
1✔
325
            self.vprint(
1✔
326
                "Marginalizing joint pdf of separation and dMag up to mode_dMag"
327
            )
328
            # get pdf of s up to mode_dMag
329
            s = np.linspace(0.0, self.rmax, 1000)
1✔
330
            fs = np.zeros(s.shape)
1✔
331
            for i in range(len(s)):
1✔
332
                fs[i] = self.f_s(s[i], self.mode_dMag)
1✔
333
            dist_s = interpolate.InterpolatedUnivariateSpline(s, fs, k=3, ext=1)
1✔
334
            self.vprint("Finished marginalization")
1✔
335
            H = {"dist_s": dist_s}
1✔
336
            with open(Cpath, "wb") as ff:
1✔
337
                pickle.dump(H, ff)
1✔
338
            self.vprint("Completeness data stored in %s" % Cpath)
1✔
339

340
        return dist_s
1✔
341

342
    def comp_s(self, smin, smax, dMag):
1✔
343
        """Calculates completeness by first integrating over dMag and then
344
        projected separation.
345

346
        Args:
347
            smin (ndarray):
348
                Values of minimum projected separation (AU) from instrument
349
            smax (ndarray):
350
                Value of maximum projected separation (AU) from instrument
351
            dMag (ndarray):
352
                Planet delta magnitude
353

354
        Returns:
355
            comp (ndarray):
356
                Completeness values
357

358
        """
359
        # cast to arrays
360
        smin = np.array(smin, ndmin=1, copy=copy_if_needed)
1✔
361
        smax = np.array(smax, ndmin=1, copy=copy_if_needed)
1✔
362
        dMag = np.array(dMag, ndmin=1, copy=copy_if_needed)
1✔
363

364
        comp = np.zeros(smin.shape)
1✔
365
        for i in tqdm(
1✔
366
            range(len(smin)),
367
            desc="Integrating pdf over dMag and separation for completeness",
368
        ):
369
            comp[i] = integrate.fixed_quad(
1✔
370
                self.f_sv, smin[i], smax[i], args=(dMag[i],), n=50
371
            )[0]
372
        # ensure completeness values are between 0 and 1
373
        comp = np.clip(comp, 0.0, 1.0)
1✔
374

375
        return comp
1✔
376

377
    @memoize
1✔
378
    def f_s(self, s, max_dMag):
1✔
379
        """Calculates probability density of projected separation marginalized
380
        up to max_dMag
381

382
        Args:
383
            s (float):
384
                Value of projected separation
385
            max_dMag (float):
386
                Maximum planet delta magnitude
387

388
        Returns:
389
            f (float):
390
                Probability density
391

392
        """
393

394
        if (s == 0.0) or (s == self.rmax):
1✔
395
            f = 0.0
1✔
396
        else:
397
            d1 = self.mindmag(s)
1✔
398
            d2 = self.maxdmag(s)
1✔
399
            if d2 > max_dMag:
1✔
400
                d2 = max_dMag
1✔
401
            if d1 > d2:
1✔
402
                f = 0.0
1✔
403
            else:
404
                f = integrate.fixed_quad(self.f_dmagsv, d1, d2, args=(s,), n=50)[0]
1✔
405

406
        return f
1✔
407

408
    @memoize
1✔
409
    def f_dmags(self, dmag, s):
1✔
410
        """Calculates the joint probability density of dMag and projected
411
        separation
412

413
        Args:
414
            dmag (float):
415
                Planet delta magnitude
416
            s (float):
417
                Value of projected separation (AU)
418

419
        Returns:
420
            f (float):
421
                Value of joint probability density
422

423
        """
424

425
        if (dmag < self.mindmag(s)) or (dmag > self.maxdmag(s)) or (s == 0.0):
1✔
426
            f = 0.0
×
427
        else:
428
            if self.rmin == self.rmax:
1✔
429
                b1 = np.arcsin(s / self.amax)
×
430
                b2 = np.pi - b1
×
431
                z1 = 10.0 ** (-0.4 * dmag) * (self.amax / self.x) ** 2 / self.Phi(b1)
×
432
                z2 = 10.0 ** (-0.4 * dmag) * (self.amax / self.x) ** 2 / self.Phi(b2)
×
433
                f = 0.0
×
434
                if (z1 > self.zmin) and (z1 < self.zmax):
×
435
                    f += (
×
436
                        np.sin(b1)
437
                        / 2.0
438
                        * self.dist_z(z1)
439
                        * z1
440
                        * np.log(10.0)
441
                        / (2.5 * self.amax * np.cos(b1))
442
                    )
443
                if (z2 > self.zmin) and (z2 < self.zmax):
×
444
                    f += (
×
445
                        np.sin(b2)
446
                        / 2.0
447
                        * self.dist_z(z2)
448
                        * z2
449
                        * np.log(10.0)
450
                        / (-2.5 * self.amax * np.cos(b2))
451
                    )
452
            else:
453
                ztest = (s / self.x) ** 2 * 10.0 ** (-0.4 * dmag) / self.val
1✔
454
                if self.PlanetPopulation.pfromRp:
1✔
455
                    f = 0.0
1✔
456
                    minR = self.PlanetPopulation.Rbs[:-1]
1✔
457
                    maxR = self.PlanetPopulation.Rbs[1:]
1✔
458
                    for i in range(len(minR)):
1✔
459
                        ptest = self.PlanetPopulation.get_p_from_Rp(
1✔
460
                            minR[i] * u.earthRad
461
                        )
462
                        Rtest = np.sqrt(ztest / ptest)
1✔
463
                        if Rtest > minR[i]:
1✔
464
                            if Rtest > self.Rmin:
1✔
465
                                Rl = Rtest
1✔
466
                            else:
467
                                Rl = self.Rmin
1✔
468
                        else:
469
                            if self.Rmin > minR[i]:
1✔
470
                                Rl = self.Rmin
×
471
                            else:
472
                                Rl = minR[i]
1✔
473
                        if self.Rmax > maxR[i]:
1✔
474
                            Ru = maxR[i]
1✔
475
                        else:
476
                            Ru = self.Rmax
1✔
477
                        if Rl < Ru:
1✔
478
                            f += integrate.fixed_quad(
1✔
479
                                self.f_dmagsRp, Rl, Ru, args=(dmag, s), n=200
480
                            )[0]
481
                elif ztest >= self.zmax:
1✔
482
                    f = 0.0
×
483
                elif self.pconst & self.Rconst:
1✔
484
                    f = self.f_dmagsz(self.zmin, dmag, s)
1✔
485
                else:
486
                    if ztest < self.zmin:
×
487
                        f = integrate.fixed_quad(
×
488
                            self.f_dmagsz, self.zmin, self.zmax, args=(dmag, s), n=200
489
                        )[0]
490
                    else:
491
                        f = integrate.fixed_quad(
×
492
                            self.f_dmagsz, ztest, self.zmax, args=(dmag, s), n=200
493
                        )[0]
494
        return f
1✔
495

496
    def f_dmagsz(self, z, dmag, s):
1✔
497
        """Calculates the joint probability density of albedo times planetary
498
        radius squared, dMag, and projected separation
499

500
        Args:
501
            z (ndarray):
502
                Values of albedo times planetary radius squared
503
            dmag (float):
504
                Planet delta magnitude
505
            s (float):
506
                Value of projected separation
507

508
        Returns:
509
            f (ndarray):
510
                Values of joint probability density
511

512
        """
513
        if not isinstance(z, np.ndarray):
1✔
514
            z = np.array(z, ndmin=1, copy=copy_if_needed)
1✔
515

516
        vals = (s / self.x) ** 2 * 10.0 ** (-0.4 * dmag) / z
1✔
517

518
        f = np.zeros(z.shape)
1✔
519
        fa = f[vals < self.val]
1✔
520
        za = z[vals < self.val]
1✔
521
        valsa = vals[vals < self.val]
1✔
522
        b1 = self.binv1(valsa)
1✔
523
        b2 = self.binv2(valsa)
1✔
524
        r1 = s / np.sin(b1)
1✔
525
        r2 = s / np.sin(b2)
1✔
526
        good1 = (r1 > self.rmin) & (r1 < self.rmax)
1✔
527
        good2 = (r2 > self.rmin) & (r2 < self.rmax)
1✔
528
        if self.pconst & self.Rconst:
1✔
529
            fa[good1] = (
1✔
530
                np.sin(b1[good1])
531
                / 2.0
532
                * self.dist_r(r1[good1])
533
                / np.abs(self.Jac(b1[good1]))
534
            )
535
            fa[good2] += (
1✔
536
                np.sin(b2[good2])
537
                / 2.0
538
                * self.dist_r(r2[good2])
539
                / np.abs(self.Jac(b2[good2]))
540
            )
541
        else:
542
            fa[good1] = (
×
543
                self.dist_z(za[good1])
544
                * np.sin(b1[good1])
545
                / 2.0
546
                * self.dist_r(r1[good1])
547
                / np.abs(self.Jac(b1[good1]))
548
            )
549
            fa[good2] += (
×
550
                self.dist_z(za[good2])
551
                * np.sin(b2[good2])
552
                / 2.0
553
                * self.dist_r(r2[good2])
554
                / np.abs(self.Jac(b2[good2]))
555
            )
556

557
        f[vals < self.val] = fa
1✔
558

559
        return f
1✔
560

561
    def f_dmagsRp(self, Rp, dmag, s):
1✔
562
        """Calculates the joint probability density of planetary radius,
563
        dMag, and projected separation
564

565
        Args:
566
            Rp (ndarray):
567
                Values of planetary radius
568
            dmag (float):
569
                Planet delta magnitude
570
            s (float):
571
                Value of projected separation
572

573
        Returns:
574
            f (ndarray):
575
                Values of joint probability density
576

577
        """
578
        if not isinstance(Rp, np.ndarray):
1✔
NEW
579
            Rp = np.array(Rp, ndmin=1, copy=copy_if_needed)
×
580

581
        vals = (
1✔
582
            (s / self.x) ** 2
583
            * 10.0 ** (-0.4 * dmag)
584
            / self.PlanetPopulation.get_p_from_Rp(Rp * u.earthRad)
585
            / Rp**2
586
        )
587

588
        f = np.zeros(Rp.shape)
1✔
589
        fa = f[vals < self.val]
1✔
590
        Rpa = Rp[vals < self.val]
1✔
591
        valsa = vals[vals < self.val]
1✔
592
        b1 = self.binv1(valsa)
1✔
593
        b2 = self.binv2(valsa)
1✔
594
        r1 = s / np.sin(b1)
1✔
595
        r2 = s / np.sin(b2)
1✔
596
        good1 = (r1 > self.rmin) & (r1 < self.rmax)
1✔
597
        good2 = (r2 > self.rmin) & (r2 < self.rmax)
1✔
598
        if self.pconst & self.Rconst:
1✔
599
            fa[good1] = (
×
600
                np.sin(b1[good1])
601
                / 2.0
602
                * self.dist_r(r1[good1])
603
                / np.abs(self.Jac(b1[good1]))
604
            )
605
            fa[good2] += (
×
606
                np.sin(b2[good2])
607
                / 2.0
608
                * self.dist_r(r2[good2])
609
                / np.abs(self.Jac(b2[good2]))
610
            )
611
        else:
612
            fa[good1] = (
1✔
613
                self.dist_radius(Rpa[good1])
614
                * np.sin(b1[good1])
615
                / 2.0
616
                * self.dist_r(r1[good1])
617
                / np.abs(self.Jac(b1[good1]))
618
            )
619
            fa[good2] += (
1✔
620
                self.dist_radius(Rpa[good2])
621
                * np.sin(b2[good2])
622
                / 2.0
623
                * self.dist_r(r2[good2])
624
                / np.abs(self.Jac(b2[good2]))
625
            )
626

627
        f[vals < self.val] = fa
1✔
628

629
        return f
1✔
630

631
    def mindmag(self, s):
1✔
632
        """Calculates the minimum value of dMag for projected separation
633

634
        Args:
635
            s (float):
636
                Projected separations (AU)
637

638
        Returns:
639
            mindmag (float):
640
                Minimum planet delta magnitude
641
        """
642
        if s == 0.0:
1✔
643
            mindmag = self.cdmin1
×
644
        elif s < self.rmin * np.sin(self.bstar):
1✔
645
            mindmag = self.cdmin1 - 2.5 * np.log10(self.Phi(np.arcsin(s / self.rmin)))
1✔
646
        elif s < self.rmax * np.sin(self.bstar):
1✔
647
            mindmag = self.cdmin2 + 5.0 * np.log10(s)
1✔
648
        elif s <= self.rmax:
1✔
649
            mindmag = self.cdmin3 - 2.5 * np.log10(self.Phi(np.arcsin(s / self.rmax)))
1✔
650
        else:
651
            mindmag = np.inf
×
652

653
        return mindmag
1✔
654

655
    def maxdmag(self, s):
1✔
656
        """Calculates the maximum value of dMag for projected separation
657

658
        Args:
659
            s (float):
660
                Projected separation (AU)
661

662
        Returns:
663
            maxdmag (float):
664
                Maximum planet delta magnitude
665

666
        """
667

668
        if s == 0.0:
1✔
669
            maxdmag = self.cdmax - 2.5 * np.log10(self.Phi(np.pi))
×
670
        elif s < self.rmax:
1✔
671
            maxdmag = self.cdmax - 2.5 * np.log10(
1✔
672
                np.abs(self.Phi(np.pi - np.arcsin(s / self.rmax)))
673
            )
674
        else:
675
            maxdmag = self.cdmax - 2.5 * np.log10(self.Phi(np.pi / 2.0))
×
676

677
        return maxdmag
1✔
678

679
    def Jac(self, b):
1✔
680
        """Calculates determinant of the Jacobian transformation matrix to get
681
        the joint probability density of dMag and s
682

683
        Args:
684
            b (ndarray):
685
                Phase angles
686

687
        Returns:
688
            f (ndarray):
689
                Determinant of Jacobian transformation matrix
690

691
        """
692

693
        f = -2.5 / (self.Phi(b) * np.log(10.0)) * self.dPhi(b) * np.sin(
1✔
694
            b
695
        ) - 5.0 / np.log(10.0) * np.cos(b)
696

697
        return f
1✔
698

699
    def rgrand1(self, e, a, r):
1✔
700
        """Calculates first integrand for determinining probability density of
701
        orbital radius
702

703
        Args:
704
            e (ndarray):
705
                Values of eccentricity
706
            a (float):
707
                Values of semi-major axis in AU
708
            r (float):
709
                Values of orbital radius in AU
710

711
        Returns:
712
            f (ndarray):
713
                Values of first integrand
714

715
        """
716
        if self.PlanetPopulation.constrainOrbits:
1✔
717
            f = 1.0 / (np.sqrt((a * e) ** 2 - (a - r) ** 2)) * self.dist_eccen_con(e, a)
×
718
        else:
719
            f = 1.0 / (np.sqrt((a * e) ** 2 - (a - r) ** 2)) * self.dist_eccen(e)
1✔
720

721
        return f
1✔
722

723
    def rgrand2(self, a, r):
1✔
724
        """Calculates second integrand for determining probability density of
725
        orbital radius
726

727
        Args:
728
            a (float):
729
                Value of semi-major axis in AU
730
            r (float):
731
                Value of orbital radius in AU
732

733
        Returns:
734
            f (float):
735
                Value of second integrand
736

737
        """
738
        emin1 = np.abs(1.0 - r / a)
1✔
739
        emin1 *= 1.0 + 1e-3
1✔
740
        if emin1 < self.emin:
1✔
741
            emin1 = self.emin
×
742

743
        if emin1 >= self.emax:
1✔
744
            f = 0.0
1✔
745
        else:
746
            if self.PlanetPopulation.constrainOrbits:
1✔
747
                if a <= 0.5 * (self.amin + self.amax):
×
748
                    elim = 1.0 - self.amin / a
×
749
                else:
750
                    elim = self.amax / a - 1.0
×
751
                if emin1 > elim:
×
752
                    f = 0.0
×
753
                else:
754
                    f = (
×
755
                        self.dist_sma(a)
756
                        / a
757
                        * integrate.fixed_quad(
758
                            self.rgrand1, emin1, elim, args=(a, r), n=60
759
                        )[0]
760
                    )
761
            else:
762
                f = (
1✔
763
                    self.dist_sma(a)
764
                    / a
765
                    * integrate.fixed_quad(
766
                        self.rgrand1, emin1, self.emax, args=(a, r), n=60
767
                    )[0]
768
                )
769

770
        return f
1✔
771

772
    def rgrandac(self, e, a, r):
1✔
773
        """Calculates integrand for determining probability density of orbital
774
        radius when semi-major axis is constant
775

776
        Args:
777
            e (ndarray):
778
                Values of eccentricity
779
            a (float):
780
                Value of semi-major axis in AU
781
            r (float):
782
                Value of orbital radius in AU
783

784
        Returns:
785
            f (ndarray):
786
                Value of integrand
787

788
        """
789
        if self.PlanetPopulation.constrainOrbits:
×
790
            f = (
×
791
                r
792
                / (np.pi * a * np.sqrt((a * e) ** 2 - (a - r) ** 2))
793
                * self.dist_eccen_con(e, a)
794
            )
795
        else:
796
            f = (
×
797
                r
798
                / (np.pi * a * np.sqrt((a * e) ** 2 - (a - r) ** 2))
799
                * self.dist_eccen(e)
800
            )
801

802
        return f
×
803

804
    def rgrandec(self, a, e, r):
1✔
805
        """Calculates integrand for determining probability density of orbital
806
        radius when eccentricity is constant
807

808
        Args:
809
            a (ndarray):
810
                Values of semi-major axis in AU
811
            e (float):
812
                Value of eccentricity
813
            r (float):
814
                Value of orbital radius in AU
815

816
        Returns:
817
            f (float):
818
                Value of integrand
819
        """
820

821
        f = r / (np.pi * a * np.sqrt((a * e) ** 2 - (a - r) ** 2)) * self.dist_sma(a)
×
822

823
        return f
×
824

825
    def f_r(self, r):
1✔
826
        """Calculates the probability density of orbital radius
827

828
        Args:
829
            r (float):
830
                Value of semi-major axis in AU
831

832
        Returns:
833
            f (float):
834
                Value of probability density
835

836
        """
837
        # takes scalar input
838
        if (r == self.rmin) or (r == self.rmax):
1✔
839
            f = 0.0
1✔
840
        else:
841
            if self.aconst & self.econst:
1✔
842
                if self.emin == 0.0:
×
843
                    f = self.dist_sma(r)
×
844
                else:
845
                    if r > self.amin * (1.0 - self.emin):
×
846
                        f = r / (
×
847
                            np.pi
848
                            * self.amin
849
                            * np.sqrt(
850
                                (self.amin * self.emin) ** 2 - (self.amin - r) ** 2
851
                            )
852
                        )
853
                    else:
854
                        f = 0.0
×
855
            elif self.aconst:
1✔
856
                etest1 = 1.0 - r / self.amin
×
857
                etest2 = r / self.amin - 1.0
×
858
                if self.emax < etest1:
×
859
                    f = 0.0
×
860
                else:
861
                    if r < self.amin:
×
862
                        if self.emin > etest1:
×
863
                            low = self.emin
×
864
                        else:
865
                            low = etest1
×
866
                    else:
867
                        if self.emin > etest2:
×
868
                            low = self.emin
×
869
                        else:
870
                            low = etest2
×
871
                    f = integrate.fixed_quad(
×
872
                        self.rgrandac, low, self.emax, args=(self.amin, r), n=60
873
                    )[0]
874
            elif self.econst:
1✔
875
                if self.emin == 0.0:
×
876
                    f = self.dist_sma(r)
×
877
                else:
878
                    atest1 = r / (1.0 - self.emin)
×
879
                    atest2 = r / (1.0 + self.emin)
×
880
                    if self.amax < atest1:
×
881
                        high = self.amax
×
882
                    else:
883
                        high = atest1
×
884
                    if self.amin < atest2:
×
885
                        low = atest2
×
886
                    else:
887
                        low = self.amin
×
888
                    f = integrate.fixed_quad(
×
889
                        self.rgrandec, low, high, args=(self.emin, r), n=60
890
                    )[0]
891
            else:
892
                if self.PlanetPopulation.constrainOrbits:
1✔
893
                    a1 = 0.5 * (self.amin + r)
×
894
                    a2 = 0.5 * (self.amax + r)
×
895
                else:
896
                    a1 = r / (1.0 + self.emax)
1✔
897
                    a2 = r / (1.0 - self.emax)
1✔
898
                    if a1 < self.amin:
1✔
899
                        a1 = self.amin
1✔
900
                    if a2 > self.amax:
1✔
901
                        a2 = self.amax
1✔
902
                f = (
1✔
903
                    r
904
                    / np.pi
905
                    * integrate.fixed_quad(self.rgrand2v, a1, a2, args=(r,), n=60)[0]
906
                )
907

908
        return f
1✔
909

910
    def Rgrand(self, R, z):
1✔
911
        """Calculates integrand for determining probability density of albedo
912
        times planetary radius squared
913

914
        Args:
915
            R (ndarray):
916
                Values of planetary radius
917
            z (float):
918
                Value of albedo times planetary radius squared
919

920
        Returns:
921
            f (ndarray):
922
                Values of integrand
923

924
        """
925

926
        f = self.dist_albedo(z / R**2) * self.dist_radius(R) / R**2
1✔
927

928
        return f
1✔
929

930
    def f_z(self, z):
1✔
931
        """Calculates probability density of albedo times planetary radius
932
        squared
933

934
        Args:
935
            z (float):
936
                Value of albedo times planetary radius squared
937

938
        Returns:
939
            f (float):
940
                Probability density
941

942
        """
943

944
        # takes scalar input
945
        if (z < self.pmin * self.Rmin**2) or (z > self.pmax * self.Rmax**2):
1✔
946
            f = 0.0
×
947
        else:
948
            if self.pconst & self.Rconst:
1✔
949
                f = 1.0
×
950
            elif self.pconst:
1✔
951
                f = (
×
952
                    1.0
953
                    / (2.0 * np.sqrt(self.pmin * z))
954
                    * self.dist_radius(np.sqrt(z / self.pmin))
955
                )
956
            elif self.Rconst:
1✔
957
                f = 1.0 / self.Rmin**2 * self.dist_albedo(z / self.Rmin**2)
×
958
            else:
959
                R1 = np.sqrt(z / self.pmax)
1✔
960
                R2 = np.sqrt(z / self.pmin)
1✔
961
                if R1 < self.Rmin:
1✔
962
                    R1 = self.Rmin
1✔
963
                if R2 > self.Rmax:
1✔
964
                    R2 = self.Rmax
1✔
965
                if R1 > R2:
1✔
966
                    f = 0.0
×
967
                else:
968
                    f = integrate.fixed_quad(self.Rgrand, R1, R2, args=(z,), n=200)[0]
1✔
969

970
        return f
1✔
971

972
    def s_bound(self, dmag, smax):
1✔
973
        """Calculates the bounding value of projected separation for dMag
974

975
        Args:
976
            dmag (float):
977
                Planet delta magnitude
978
            smax (float):
979
                maximum projected separation (AU)
980

981
        Returns:
982
            sb (float):
983
                boundary value of projected separation (AU)
984
        """
985

986
        if dmag < self.d1:
1✔
987
            s = 0.0
×
988
        elif (dmag > self.d1) and (dmag <= self.d2):
1✔
989
            s = self.rmin * np.sin(
×
990
                self.Phiinv(
991
                    self.rmin**2
992
                    * 10.0 ** (-0.4 * dmag)
993
                    / (self.pmax * (self.Rmax * self.x) ** 2)
994
                )
995
            )
996
        elif (dmag > self.d2) and (dmag <= self.d3):
1✔
997
            s = np.sin(self.bstar) * np.sqrt(
1✔
998
                self.pmax
999
                * (self.Rmax * self.x) ** 2
1000
                * self.Phi(self.bstar)
1001
                / 10.0 ** (-0.4 * dmag)
1002
            )
1003
        elif (dmag > self.d3) and (dmag <= self.d4):
1✔
1004
            s = self.rmax * np.sin(
1✔
1005
                self.Phiinv(
1006
                    self.rmax**2
1007
                    * 10.0 ** (-0.4 * dmag)
1008
                    / (self.pmax * (self.Rmax * self.x) ** 2)
1009
                )
1010
            )
1011
        elif (dmag > self.d4) and (dmag <= self.d5):
1✔
1012
            s = smax
1✔
1013
        else:
1014
            s = self.rmax * np.sin(
1✔
1015
                np.pi
1016
                - self.Phiinv(
1017
                    10.0 ** (-0.4 * dmag)
1018
                    * self.rmax**2
1019
                    / (self.pmin * (self.Rmin * self.x) ** 2)
1020
                )
1021
            )
1022

1023
        return s
1✔
1024

1025
    def f_sdmag(self, s, dmag):
1✔
1026
        """Calculates the joint probability density of projected separation and
1027
        dMag by flipping the order of f_dmags
1028

1029
        Args:
1030
            s (float):
1031
                Value of projected separation (AU)
1032
            dmag (float):
1033
                Planet delta magnitude
1034

1035
        Returns:
1036
            f (float):
1037
                Value of joint probability density
1038

1039
        """
1040
        return self.f_dmags(dmag, s)
1✔
1041

1042
    @memoize
1✔
1043
    def f_dmag(self, dmag, smin, smax):
1✔
1044
        """Calculates probability density of dMag by integrating over projected
1045
        separation
1046

1047
        Args:
1048
            dmag (float):
1049
                Planet delta magnitude
1050
            smin (float):
1051
                Value of minimum projected separation (AU) from instrument
1052
            smax (float):
1053
                Value of maximum projected separation (AU) from instrument
1054

1055
        Returns:
1056
            f (float):
1057
                Value of probability density
1058

1059
        """
1060
        if dmag < self.mindmag(smin):
1✔
1061
            f = 0.0
×
1062
        else:
1063
            su = self.s_bound(dmag, smax)
1✔
1064
            if su > smax:
1✔
1065
                su = smax
1✔
1066
            if su < smin:
1✔
1067
                f = 0.0
×
1068
            else:
1069
                f = integrate.fixed_quad(self.f_sdmagv, smin, su, args=(dmag,), n=50)[0]
1✔
1070

1071
        return f
1✔
1072

1073
    def comp_dmag(self, smin, smax, max_dMag):
1✔
1074
        """Calculates completeness by first integrating over projected
1075
        separation and then dMag.
1076

1077
        Args:
1078
            smin (ndarray):
1079
                Values of minimum projected separation (AU) from instrument
1080
            smax (ndarray):
1081
                Value of maximum projected separation (AU) from instrument
1082
            max_dMag (float ndarray):
1083
                Maximum planet delta magnitude
1084

1085
        Returns:
1086
            comp (ndarray):
1087
                Completeness values
1088

1089
        """
1090
        # cast to arrays
1091
        smin = np.array(smin, ndmin=1, copy=copy_if_needed)
1✔
1092
        smax = np.array(smax, ndmin=1, copy=copy_if_needed)
1✔
1093
        max_dMag = np.array(max_dMag, ndmin=1, copy=copy_if_needed)
1✔
1094
        dmax = -2.5 * np.log10(
1✔
1095
            float(
1096
                self.PlanetPopulation.prange[0]
1097
                * (self.PlanetPopulation.Rprange[0] / self.PlanetPopulation.rrange[1])
1098
                ** 2
1099
            )
1100
            * 1e-11
1101
        )
1102
        max_dMag[max_dMag > dmax] = dmax
1✔
1103

1104
        comp = np.zeros(smin.shape)
1✔
1105
        for i in tqdm(
1✔
1106
            range(len(smin)),
1107
            desc=(
1108
                "Calculating completeness values by integrating with order of "
1109
                "quadrature {}".format(self.order_of_quadrature)
1110
            ),
1111
        ):
1112
            d1 = self.mindmag(smin[i])
1✔
1113
            if d1 > max_dMag[i]:
1✔
1114
                comp[i] = 0.0
×
1115
            else:
1116
                comp[i] = integrate.fixed_quad(
1✔
1117
                    self.f_dmagv,
1118
                    d1,
1119
                    max_dMag[i],
1120
                    args=(smin[i], min(smax[i], np.finfo(np.float32).max)),
1121
                    n=self.order_of_quadrature,
1122
                )[0]
1123

1124
        # ensure completeness values are between 0 and 1
1125
        comp = np.clip(comp, 0.0, 1.0)
1✔
1126

1127
        return comp
1✔
1128

1129
    def comp_calc(self, smin, smax, dMag):
1✔
1130
        """Calculates completeness for given minimum and maximum separations
1131
        and dMag
1132

1133
        Note: this method assumes scaling orbits when scaleOrbits == True has
1134
        already occurred for smin, smax, dMag inputs
1135

1136
        Args:
1137
            smin (float ndarray):
1138
                Minimum separation(s) in AU
1139
            smax (float ndarray):
1140
                Maximum separation(s) in AU
1141
            dMag (float ndarray):
1142
                Difference in brightness magnitude
1143

1144
        Returns:
1145
            comp (float ndarray):
1146
                Completeness value(s)
1147

1148
        """
1149

1150
        comp = self.comp_dmag(smin, smax, dMag)
1✔
1151

1152
        return comp
1✔
1153

1154
    def calc_fdmag(self, dMag, smin, smax):
1✔
1155
        """Calculates probability density of dMag by integrating over projected
1156
        separation
1157

1158
        Args:
1159
            dMag (float ndarray):
1160
                Planet delta magnitude(s)
1161
            smin (float ndarray):
1162
                Value of minimum projected separation (AU) from instrument
1163
            smax (float ndarray):
1164
                Value of maximum projected separation (AU) from instrument
1165

1166
        Returns:
1167
            f (float):
1168
                Value of probability density
1169

1170
        """
1171

1172
        f = self.f_dmagv(dMag, smin, smax)
1✔
1173

1174
        return f
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc