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

dsavransky / EXOSIMS / 11979115482

22 Nov 2024 07:45PM UTC coverage: 65.499% (-0.2%) from 65.737%
11979115482

Pull #403

github

web-flow
Merge 0d3dd536f into 86cc76cd9
Pull Request #403: Improved version tracking and deprecated SVN

36 of 54 new or added lines in 2 files covered. (66.67%)

43 existing lines in 5 files now uncovered.

9534 of 14556 relevant lines covered (65.5%)

0.65 hits per line

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

77.94
/EXOSIMS/Completeness/BrownCompleteness.py
1
# -*- coding: utf-8 -*-
2
import time
1✔
3
import numpy as np
1✔
4
from scipy import interpolate
1✔
5
import astropy.units as u
1✔
6
import astropy.constants as const
1✔
7
import os
1✔
8
import pickle
1✔
9
import hashlib
1✔
10
from EXOSIMS.Prototypes.Completeness import Completeness
1✔
11
from EXOSIMS.util.eccanom import eccanom
1✔
12
from EXOSIMS.util.deltaMag import deltaMag
1✔
13
from tqdm import tqdm
1✔
14

15

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

19
    This class contains all variables and methods necessary to perform
20
    Completeness Module calculations in exoplanet mission simulation.
21

22
    Args:
23
        specs:
24
            user specified values
25

26
    Attributes:
27
        Nplanets (int):
28
            Number of planets for initial completeness Monte Carlo simulation
29
        classpath (str):
30
            Path on disk to Brown Completeness
31
        filename (str):
32
            Name of file where completeness interpolant is stored
33
        updates (float nx5 ndarray):
34
            Completeness values of successive observations of each star in the
35
            target list (initialized in gen_update)
36

37
    """
38

39
    def __init__(self, Nplanets=1e8, **specs):
1✔
40

41
        # Number of planets to sample
42
        self.Nplanets = int(Nplanets)
1✔
43

44
        # Run Completeness prototype __init__
45
        Completeness.__init__(self, **specs)
1✔
46

47
        # copy Nplanets into outspec
48
        self._outspec["Nplanets"] = self.Nplanets
1✔
49

50
    def completeness_setup(self):
1✔
51
        """Preform any preliminary calculations needed for this flavor of completeness
52

53
        For BrownCompleteness this includes generating a 2D histogram of s vs. dMag for
54
        the planet population and creating interpolants over it.
55
        """
56

57
        # set up "ensemble visit photometric and obscurational completeness"
58
        # interpolant for initial completeness values
59
        # bins for interpolant
60
        bins = 1000
1✔
61
        # xedges is array of separation values for interpolant
62
        if self.PlanetPopulation.constrainOrbits:
1✔
63
            self.xedges = np.linspace(
×
64
                0.0, self.PlanetPopulation.arange[1].to("AU").value, bins + 1
65
            )
66
        else:
67
            self.xedges = np.linspace(
1✔
68
                0.0, self.PlanetPopulation.rrange[1].to("AU").value, bins + 1
69
            )
70

71
        # yedges is array of delta magnitude values for interpolant
72
        self.ymin = -2.5 * np.log10(
1✔
73
            float(
74
                self.PlanetPopulation.prange[1]
75
                * (self.PlanetPopulation.Rprange[1] / self.PlanetPopulation.rrange[0])
76
                ** 2
77
            )
78
        )
79
        self.ymax = -2.5 * np.log10(
1✔
80
            float(
81
                self.PlanetPopulation.prange[0]
82
                * (self.PlanetPopulation.Rprange[0] / self.PlanetPopulation.rrange[1])
83
                ** 2
84
            )
85
            * 1e-11
86
        )
87
        self.yedges = np.linspace(self.ymin, self.ymax, bins + 1)
1✔
88
        # number of planets for each Monte Carlo simulation
89
        nplan = 1e6
1✔
90
        # number of simulations to perform (must be integer)
91
        steps = int(np.floor(self.Nplanets / nplan))
1✔
92

93
        # path to 2D completeness pdf array for interpolation
94
        Cpath = os.path.join(self.cachedir, self.filename + ".comp")
1✔
95
        Cpdf, xedges2, yedges2 = self.genC(
1✔
96
            Cpath,
97
            nplan,
98
            self.xedges,
99
            self.yedges,
100
            steps,
101
            remainder=self.Nplanets - steps * nplan,
102
        )
103

104
        xcent = 0.5 * (xedges2[1:] + xedges2[:-1])
1✔
105
        ycent = 0.5 * (yedges2[1:] + yedges2[:-1])
1✔
106
        xnew = np.hstack((0.0, xcent, self.PlanetPopulation.rrange[1].to("AU").value))
1✔
107
        ynew = np.hstack((self.ymin, ycent, self.ymax))
1✔
108
        Cpdf = np.pad(Cpdf, 1, mode="constant")
1✔
109

110
        # save interpolant to object
111
        self.Cpdf = Cpdf
1✔
112
        self.EVPOCpdf = interpolate.RectBivariateSpline(xnew, ynew, Cpdf.T)
1✔
113
        self.EVPOC = np.vectorize(self.EVPOCpdf.integral, otypes=[np.float64])
1✔
114
        self.xnew = xnew
1✔
115
        self.ynew = ynew
1✔
116

117
    def generate_cache_names(self, **specs):
1✔
118
        """Generate unique filenames for cached products"""
119

120
        # filename for completeness interpolant stored in a pickled .comp file
121
        self.filename = (
1✔
122
            self.PlanetPopulation.__class__.__name__
123
            + self.PlanetPhysicalModel.__class__.__name__
124
            + self.__class__.__name__
125
            + str(self.Nplanets)
126
            + self.PlanetPhysicalModel.whichPlanetPhaseFunction
127
        )
128

129
        # filename for dynamic completeness array in a pickled .dcomp file
130
        self.dfilename = (
1✔
131
            self.PlanetPopulation.__class__.__name__
132
            + self.PlanetPhysicalModel.__class__.__name__
133
            + specs["modules"]["OpticalSystem"]
134
            + specs["modules"]["StarCatalog"]
135
            + specs["modules"]["TargetList"]
136
        )
137
        # Remove spaces from string
138
        self.dfilename = self.dfilename.replace(" ", "")
1✔
139

140
        atts = list(self.PlanetPopulation.__dict__)
1✔
141
        self.extstr = ""
1✔
142
        for att in sorted(atts, key=str.lower):
1✔
143
            if (
1✔
144
                not (callable(getattr(self.PlanetPopulation, att)))
145
                and (att != "PlanetPhysicalModel")
146
                and (att != "cachedir")
147
                and (att != "_outspec")
148
            ):
149
                self.extstr += "%s: " % att + str(getattr(self.PlanetPopulation, att))
1✔
150
        ext = hashlib.md5(self.extstr.encode("utf-8")).hexdigest()
1✔
151
        self.filename += ext
1✔
152
        # Remove spaces from string (in the case of prototype use)
153
        self.filename = self.filename.replace(" ", "")
1✔
154

155
    def target_completeness(self, TL):
1✔
156
        """Generates completeness values for target stars using average case
157
        values
158

159
        This method is called from TargetList __init__ method.
160

161
        Args:
162
            TL (TargetList module):
163
                TargetList class object
164

165
        Returns:
166
            float ndarray:
167
                Completeness values for each target star
168

169
        """
170

171
        self.vprint("Generating int_comp values")
1✔
172
        OS = TL.OpticalSystem
1✔
173
        if TL.calc_char_int_comp:
1✔
174
            mode = list(
×
175
                filter(lambda mode: "spec" in mode["inst"]["name"], OS.observingModes)
176
            )[0]
177
        else:
178
            mode = list(filter(lambda mode: mode["detectionMode"], OS.observingModes))[
1✔
179
                0
180
            ]
181

182
        IWA = mode["IWA"]
1✔
183
        OWA = mode["OWA"]
1✔
184
        smin = np.tan(IWA) * TL.dist
1✔
185
        if np.isinf(OWA):
1✔
186
            smax = np.array([self.xedges[-1]] * len(smin)) * u.AU
×
187
        else:
188
            smax = np.tan(OWA) * TL.dist
1✔
189
            smax[smax > self.PlanetPopulation.rrange[1]] = self.PlanetPopulation.rrange[
1✔
190
                1
191
            ]
192

193
        int_comp = np.zeros(smin.shape)
1✔
194
        if self.PlanetPopulation.scaleOrbits:
1✔
195
            L = np.where(TL.L > 0, TL.L, 1e-10)  # take care of zero/negative values
×
196
            smin = smin / np.sqrt(L)
×
197
            smax = smax / np.sqrt(L)
×
198
            scaled_dMag = TL.int_dMag - 2.5 * np.log10(L)
×
199
            mask = (scaled_dMag > self.ymin) & (smin < self.PlanetPopulation.rrange[1])
×
200
            int_comp[mask] = self.EVPOC(
×
201
                smin[mask].to("AU").value,
202
                smax[mask].to("AU").value,
203
                0.0,
204
                scaled_dMag[mask],
205
            )
206
        else:
207
            mask = smin < self.PlanetPopulation.rrange[1]
1✔
208
            int_comp[mask] = self.EVPOC(
1✔
209
                smin[mask].to("AU").value,
210
                smax[mask].to("AU").value,
211
                0.0,
212
                TL.int_dMag[mask],
213
            )
214

215
        int_comp[int_comp < 1e-6] = 0.0
1✔
216
        # ensure that completeness is between 0 and 1
217
        int_comp = np.clip(int_comp, 0.0, 1.0)
1✔
218

219
        return int_comp
1✔
220

221
    def gen_update(self, TL):
1✔
222
        """Generates dynamic completeness values for multiple visits of each
223
        star in the target list
224

225
        Args:
226
            TL (TargetList):
227
                TargetList class object
228

229
        """
230

231
        OS = TL.OpticalSystem
1✔
232
        PPop = TL.PlanetPopulation
1✔
233

234
        # get name for stored dynamic completeness updates array
235
        # inner and outer working angles for detection mode
236
        mode = list(filter(lambda mode: mode["detectionMode"], OS.observingModes))[0]
1✔
237
        IWA = mode["IWA"]
1✔
238
        OWA = mode["OWA"]
1✔
239
        extstr = (
1✔
240
            self.extstr
241
            + "IWA: "
242
            + str(IWA)
243
            + " OWA: "
244
            + str(OWA)
245
            + " nStars: "
246
            + str(TL.nStars)
247
        )
248
        ext = hashlib.md5(extstr.encode("utf-8")).hexdigest()
1✔
249
        self.dfilename += ext
1✔
250
        self.dfilename += ".dcomp"
1✔
251

252
        path = os.path.join(self.cachedir, self.dfilename)
1✔
253
        # if the 2D completeness update array exists as a .dcomp file load it
254
        if os.path.exists(path):
1✔
255
            self.vprint('Loading cached dynamic completeness array from "%s".' % path)
1✔
256
            try:
1✔
257
                with open(path, "rb") as ff:
1✔
258
                    self.updates = pickle.load(ff)
1✔
259
            except UnicodeDecodeError:
×
260
                with open(path, "rb") as ff:
×
261
                    self.updates = pickle.load(ff, encoding="latin1")
×
262
            self.vprint("Dynamic completeness array loaded from cache.")
1✔
263
        else:
264
            # run Monte Carlo simulation and pickle the resulting array
265
            self.vprint('Cached dynamic completeness array not found at "%s".' % path)
1✔
266
            # dynamic completeness values: rows are stars, columns are number of visits
267
            self.updates = np.zeros((TL.nStars, 5))
1✔
268
            # number of planets to simulate
269
            nplan = int(2e4)
1✔
270
            # sample quantities which do not change in time
271
            a, e, p, Rp = PPop.gen_plan_params(nplan)
1✔
272
            a = a.to("AU").value
1✔
273
            # sample angles
274
            I, O, w = PPop.gen_angles(nplan)
1✔
275
            I = I.to("rad").value
1✔
276
            O = O.to("rad").value
1✔
277
            w = w.to("rad").value
1✔
278
            Mp = PPop.gen_mass(nplan)  # M_earth
1✔
279
            rmax = a * (1.0 + e)  # AU
1✔
280
            # sample quantity which will be updated
281
            M = np.random.uniform(high=2.0 * np.pi, size=nplan)
1✔
282
            newM = np.zeros((nplan,))
1✔
283
            # population values
284
            smin = (np.tan(IWA) * TL.dist).to("AU").value
1✔
285
            if np.isfinite(OWA):
1✔
286
                smax = (np.tan(OWA) * TL.dist).to("AU").value
1✔
287
            else:
288
                smax = np.array(
×
289
                    [np.max(PPop.arange.to("AU").value) * (1.0 + np.max(PPop.erange))]
290
                    * TL.nStars
291
                )
292
            # fill dynamic completeness values
293
            for sInd in tqdm(
1✔
294
                range(TL.nStars), desc="Calculating dynamic completeness for each star"
295
            ):
296
                mu = (const.G * (Mp + TL.MsTrue[sInd])).to("AU3/day2").value
1✔
297
                n = np.sqrt(mu / a**3)  # in 1/day
1✔
298
                # normalization time equation from Brown 2015
299
                dt = (
1✔
300
                    58.0
301
                    * (TL.L[sInd] / 0.83) ** (3.0 / 4.0)
302
                    * (TL.MsTrue[sInd] / (0.91 * u.M_sun)) ** (1.0 / 2.0)
303
                )  # days
304
                # remove rmax < smin
305
                pInds = np.where(rmax > smin[sInd])[0]
1✔
306
                # calculate for 5 successive observations
307
                for num in range(5):
1✔
308
                    if num == 0:
1✔
309
                        self.updates[sInd, num] = TL.int_comp[sInd]
1✔
310
                    if not pInds.any():
1✔
311
                        break
1✔
312
                    # find Eccentric anomaly
UNCOV
313
                    if num == 0:
×
UNCOV
314
                        E = eccanom(M[pInds], e[pInds])
×
UNCOV
315
                        newM[pInds] = M[pInds]
×
316
                    else:
UNCOV
317
                        E = eccanom(newM[pInds], e[pInds])
×
318

UNCOV
319
                    r1 = a[pInds] * (np.cos(E) - e[pInds])
×
UNCOV
320
                    r1 = np.hstack(
×
321
                        (
322
                            r1.reshape(len(r1), 1),
323
                            r1.reshape(len(r1), 1),
324
                            r1.reshape(len(r1), 1),
325
                        )
326
                    )
UNCOV
327
                    r2 = a[pInds] * np.sin(E) * np.sqrt(1.0 - e[pInds] ** 2)
×
UNCOV
328
                    r2 = np.hstack(
×
329
                        (
330
                            r2.reshape(len(r2), 1),
331
                            r2.reshape(len(r2), 1),
332
                            r2.reshape(len(r2), 1),
333
                        )
334
                    )
335

UNCOV
336
                    a1 = np.cos(O[pInds]) * np.cos(w[pInds]) - np.sin(
×
337
                        O[pInds]
338
                    ) * np.sin(w[pInds]) * np.cos(I[pInds])
UNCOV
339
                    a2 = np.sin(O[pInds]) * np.cos(w[pInds]) + np.cos(
×
340
                        O[pInds]
341
                    ) * np.sin(w[pInds]) * np.cos(I[pInds])
UNCOV
342
                    a3 = np.sin(w[pInds]) * np.sin(I[pInds])
×
UNCOV
343
                    A = np.hstack(
×
344
                        (
345
                            a1.reshape(len(a1), 1),
346
                            a2.reshape(len(a2), 1),
347
                            a3.reshape(len(a3), 1),
348
                        )
349
                    )
350

UNCOV
351
                    b1 = -np.cos(O[pInds]) * np.sin(w[pInds]) - np.sin(
×
352
                        O[pInds]
353
                    ) * np.cos(w[pInds]) * np.cos(I[pInds])
UNCOV
354
                    b2 = -np.sin(O[pInds]) * np.sin(w[pInds]) + np.cos(
×
355
                        O[pInds]
356
                    ) * np.cos(w[pInds]) * np.cos(I[pInds])
UNCOV
357
                    b3 = np.cos(w[pInds]) * np.sin(I[pInds])
×
UNCOV
358
                    B = np.hstack(
×
359
                        (
360
                            b1.reshape(len(b1), 1),
361
                            b2.reshape(len(b2), 1),
362
                            b3.reshape(len(b3), 1),
363
                        )
364
                    )
365

366
                    # planet position, planet-star distance, apparent separation
UNCOV
367
                    r = A * r1 + B * r2  # position vector (AU)
×
UNCOV
368
                    d = np.linalg.norm(r, axis=1)  # planet-star distance
×
UNCOV
369
                    s = np.linalg.norm(r[:, 0:2], axis=1)  # apparent separation
×
UNCOV
370
                    beta = np.arccos(r[:, 2] / d)  # phase angle
×
UNCOV
371
                    Phi = self.PlanetPhysicalModel.calc_Phi(
×
372
                        beta * u.rad
373
                    )  # phase function
UNCOV
374
                    dMag = deltaMag(
×
375
                        p[pInds], Rp[pInds], d * u.AU, Phi
376
                    )  # difference in magnitude
377

UNCOV
378
                    toremoves = np.where((s > smin[sInd]) & (s < smax[sInd]))[0]
×
UNCOV
379
                    toremovedmag = np.where(dMag < max(TL.intCutoff_dMag))[0]
×
UNCOV
380
                    toremove = np.intersect1d(toremoves, toremovedmag)
×
381

UNCOV
382
                    pInds = np.delete(pInds, toremove)
×
383

UNCOV
384
                    if num == 0:
×
UNCOV
385
                        self.updates[sInd, num] = TL.int_comp[sInd]
×
386
                    else:
UNCOV
387
                        self.updates[sInd, num] = float(len(toremove)) / nplan
×
388

389
                    # update M
UNCOV
390
                    newM[pInds] = (
×
391
                        (newM[pInds] + n[pInds] * dt) / (2 * np.pi) % 1 * 2.0 * np.pi
392
                    )
393
            # ensure that completeness values are between 0 and 1
394
            self.updates = np.clip(self.updates, 0.0, 1.0)
1✔
395
            # store dynamic completeness array as .dcomp file
396
            with open(path, "wb") as ff:
1✔
397
                pickle.dump(self.updates, ff)
1✔
398
            self.vprint("Dynamic completeness calculations finished")
1✔
399
            self.vprint("Dynamic completeness array stored in %r" % path)
1✔
400

401
    def completeness_update(self, TL, sInds, visits, dt):
1✔
402
        """Updates completeness value for stars previously observed by selecting
403
        the appropriate value from the updates array
404

405
        Args:
406
            TL (TargetList module):
407
                TargetList class object
408
            sInds (integer array):
409
                Indices of stars to update
410
            visits (integer array):
411
                Number of visits for each star
412
            dt (astropy Quantity array):
413
                Time since previous observation
414

415
        Returns:
416
            float ndarray:
417
                Completeness values for each star
418

419
        """
420
        # if visited more than five times, return 5th stored dynamic
421
        # completeness value
422
        visits[visits > 4] = 4
1✔
423
        dcomp = self.updates[sInds, visits]
1✔
424

425
        return dcomp
1✔
426

427
    def genC(self, Cpath, nplan, xedges, yedges, steps, remainder=0):
1✔
428
        """Gets completeness interpolant for initial completeness
429

430
        This function either loads a completeness .comp file based on specified
431
        Planet Population module or performs Monte Carlo simulations to get
432
        the 2D completeness values needed for interpolation.
433

434
        Args:
435
            Cpath (string):
436
                path to 2D completeness value array
437
            nplan (float):
438
                number of planets used in each simulation
439
            xedges (float ndarray):
440
                x edge of 2d histogram (separation)
441
            yedges (float ndarray):
442
                y edge of 2d histogram (dMag)
443
            steps (integer):
444
                number of nplan simulations to perform
445
            remainder (integer):
446
                residual number of planets to simulate
447

448
        Returns:
449
            float ndarray:
450
                2D numpy ndarray containing completeness probability density values
451

452
        """
453

454
        # if the 2D completeness pdf array exists as a .comp file load it
455
        if os.path.exists(Cpath):
1✔
456
            self.vprint('Loading cached completeness file from "%s".' % Cpath)
1✔
457
            try:
1✔
458
                with open(Cpath, "rb") as ff:
1✔
459
                    H = pickle.load(ff)
1✔
460
            except UnicodeDecodeError:
×
461
                with open(Cpath, "rb") as ff:
×
462
                    H = pickle.load(ff, encoding="latin1")
×
463
            self.vprint("Completeness loaded from cache.")
1✔
464
        else:
465
            # run Monte Carlo simulation and pickle the resulting array
466
            self.vprint('Cached completeness file not found at "%s".' % Cpath)
1✔
467

468
            t0, t1 = None, None  # keep track of per-iteration time
1✔
469
            for i in tqdm(range(steps), desc="Creating 2d completeness pdf"):
1✔
470
                t0, t1 = t1, time.time()
×
471
                if t0 is None:
×
472
                    delta_t_msg = ""  # no message
×
473
                else:
474
                    delta_t_msg = "[%.3f s/iteration]" % (t1 - t0)  # noqa: F841
×
475
                # get completeness histogram
476
                h, xedges, yedges = self.hist(nplan, xedges, yedges)
×
477
                if i == 0:
×
478
                    H = h
×
479
                else:
480
                    H += h
×
481
            if not remainder == 0:
1✔
482
                h, xedges, yedges = self.hist(remainder, xedges, yedges)
1✔
483
                if steps > 0:  # if H exists already
1✔
484
                    H += h
×
485
                else:  # if H does not exist
486
                    H = h
1✔
487

488
            H = H / (self.Nplanets * (xedges[1] - xedges[0]) * (yedges[1] - yedges[0]))
1✔
489

490
            # store 2D completeness pdf array as .comp file
491
            with open(Cpath, "wb") as ff:
1✔
492
                pickle.dump(H, ff)
1✔
493
            self.vprint("Monte Carlo completeness calculations finished")
1✔
494
            self.vprint("2D completeness array stored in %r" % Cpath)
1✔
495

496
        return H, xedges, yedges
1✔
497

498
    def hist(self, nplan, xedges, yedges):
1✔
499
        """Returns completeness histogram for Monte Carlo simulation
500

501
        This function uses the inherited Planet Population module.
502

503
        Args:
504
            nplan (float):
505
                number of planets used
506
            xedges (float ndarray):
507
                x edge of 2d histogram (separation)
508
            yedges (float ndarray):
509
                y edge of 2d histogram (dMag)
510

511
        Returns:
512
            float ndarray:
513
                2D numpy ndarray containing completeness frequencies
514

515
        """
516

517
        s, dMag = self.genplans(nplan)
1✔
518
        # get histogram
519
        h, yedges, xedges = np.histogram2d(
1✔
520
            dMag,
521
            s.to("AU").value,
522
            bins=1000,
523
            range=[[yedges.min(), yedges.max()], [xedges.min(), xedges.max()]],
524
        )
525

526
        return h, xedges, yedges
1✔
527

528
    def genplans(self, nplan):
1✔
529
        """Generates planet data needed for Monte Carlo simulation
530

531
        Args:
532
            nplan (integer):
533
                Number of planets
534

535
        Returns:
536
            tuple:
537
            s (astropy Quantity array):
538
                Planet apparent separations in units of AU
539
            dMag (ndarray):
540
                Difference in brightness
541

542
        """
543

544
        PPop = self.PlanetPopulation
1✔
545

546
        nplan = int(nplan)
1✔
547

548
        # sample uniform distribution of mean anomaly
549
        M = np.random.uniform(high=2.0 * np.pi, size=nplan)
1✔
550
        # sample quantities
551
        a, e, p, Rp = PPop.gen_plan_params(nplan)
1✔
552
        # check if circular orbits
553
        if np.sum(PPop.erange) == 0:
1✔
554
            r = a
×
555
            e = 0.0
×
556
            E = M
×
557
        else:
558
            E = eccanom(M, e)
1✔
559
            # orbital radius
560
            r = a * (1.0 - e * np.cos(E))
1✔
561

562
        beta = np.arccos(1.0 - 2.0 * np.random.uniform(size=nplan)) * u.rad
1✔
563
        s = r * np.sin(beta)
1✔
564
        # phase function
565
        Phi = self.PlanetPhysicalModel.calc_Phi(beta)
1✔
566
        # calculate dMag
567
        dMag = deltaMag(p, Rp, r, Phi)
1✔
568

569
        return s, dMag
1✔
570

571
    def comp_per_intTime(
1✔
572
        self, intTimes, TL, sInds, fZ, fEZ, WA, mode, C_b=None, C_sp=None, TK=None
573
    ):
574
        """Calculates completeness for integration time
575

576
        Args:
577
            intTimes (astropy Quantity array):
578
                Integration times
579
            TL (:ref:`TargetList`):
580
                TargetList object
581
            sInds (integer ndarray):
582
                Integer indices of the stars of interest
583
            fZ (astropy Quantity array):
584
                Surface brightness of local zodiacal light in units of 1/arcsec2
585
            fEZ (astropy Quantity array):
586
                Surface brightness of exo-zodiacal light in units of 1/arcsec2
587
            WA (astropy Quantity):
588
                Working angle of the planet of interest in units of arcsec
589
            mode (dict):
590
                Selected observing mode
591
            C_b (astropy Quantity array):
592
                Background noise electron count rate in units of 1/s (optional)
593
            C_sp (astropy Quantity array):
594
                Residual speckle spatial structure (systematic error) in units of 1/s
595
                (optional)
596
            TK (:ref:`TimeKeeping`):
597
                TimeKeeping object (optional)
598

599
        Returns:
600
            flat ndarray:
601
                Completeness values
602

603
        """
604
        intTimes, sInds, fZ, fEZ, WA, smin, smax, dMag = self.comps_input_reshape(
1✔
605
            intTimes, TL, sInds, fZ, fEZ, WA, mode, C_b=C_b, C_sp=C_sp, TK=TK
606
        )
607

608
        comp = self.comp_calc(smin, smax, dMag)
1✔
609
        mask = smin > self.PlanetPopulation.rrange[1].to("AU").value
1✔
610
        comp[mask] = 0.0
1✔
611
        # ensure completeness values are between 0 and 1
612
        comp = np.clip(comp, 0.0, 1.0)
1✔
613

614
        return comp
1✔
615

616
    def comp_calc(self, smin, smax, dMag):
1✔
617
        """Calculates completeness for given minimum and maximum separations
618
        and dMag
619

620
        Note: this method assumes scaling orbits when scaleOrbits == True has
621
        already occurred for smin, smax, dMag inputs
622

623
        Args:
624
            smin (float ndarray):
625
                Minimum separation(s) in AU
626
            smax (float ndarray):
627
                Maximum separation(s) in AU
628
            dMag (float ndarray):
629
                Difference in brightness magnitude
630

631
        Returns:
632
            float ndarray:
633
                Completeness values
634

635
        """
636

637
        comp = self.EVPOC(smin, smax, 0.0, dMag)
1✔
638
        # remove small values
639
        comp[comp < 1e-6] = 0.0
1✔
640

641
        return comp
1✔
642

643
    def dcomp_dt(
1✔
644
        self, intTimes, TL, sInds, fZ, fEZ, WA, mode, C_b=None, C_sp=None, TK=None
645
    ):
646
        """Calculates derivative of completeness with respect to integration time
647

648
        Args:
649
            intTimes (astropy Quantity array):
650
                Integration times
651
            TL (TargetList module):
652
                TargetList class object
653
            sInds (integer ndarray):
654
                Integer indices of the stars of interest
655
            fZ (astropy Quantity array):
656
                Surface brightness of local zodiacal light in units of 1/arcsec2
657
            fEZ (astropy Quantity array):
658
                Surface brightness of exo-zodiacal light in units of 1/arcsec2
659
            WA (astropy Quantity):
660
                Working angle of the planet of interest in units of arcsec
661
            mode (dict):
662
                Selected observing mode
663
            C_b (astropy Quantity array):
664
                Background noise electron count rate in units of 1/s (optional)
665
            C_sp (astropy Quantity array):
666
                Residual speckle spatial structure (systematic error) in units of 1/s
667
                (optional)
668
            TK (:ref:`TimeKeeping`):
669
                TimeKeeping object (optional)
670

671
        Returns:
672
            astropy Quantity array:
673
                Derivative of completeness with respect to integration time
674
                (units 1/time)
675

676
        """
677
        intTimes, sInds, fZ, fEZ, WA, smin, smax, dMag = self.comps_input_reshape(
1✔
678
            intTimes, TL, sInds, fZ, fEZ, WA, mode, C_b=C_b, C_sp=C_sp, TK=TK
679
        )
680

681
        ddMag = TL.OpticalSystem.ddMag_dt(
1✔
682
            intTimes, TL, sInds, fZ, fEZ, WA, mode, TK=TK
683
        ).reshape((len(intTimes),))
684
        dcomp = self.calc_fdmag(dMag, smin, smax)
1✔
685
        mask = smin > self.PlanetPopulation.rrange[1].to("AU").value
1✔
686
        dcomp[mask] = 0.0
1✔
687

688
        return dcomp * ddMag
1✔
689

690
    def comps_input_reshape(
1✔
691
        self, intTimes, TL, sInds, fZ, fEZ, WA, mode, C_b=None, C_sp=None, TK=None
692
    ):
693
        """
694
        Reshapes inputs for comp_per_intTime and dcomp_dt as necessary
695

696
        Args:
697
            intTimes (astropy Quantity array):
698
                Integration times
699
            TL (TargetList module):
700
                TargetList class object
701
            sInds (integer ndarray):
702
                Integer indices of the stars of interest
703
            fZ (astropy Quantity array):
704
                Surface bright ness of local zodiacal light in units of 1/arcsec2
705
            fEZ (astropy Quantity array):
706
                Surface brightness of exo-zodiacal light in units of 1/arcsec2
707
            WA (astropy Quantity):
708
                Working angle of the planet of interest in units of arcsec
709
            mode (dict):
710
                Selected observing mode
711
            C_b (astropy Quantity array):
712
                Background noise electron count rate in units of 1/s (optional)
713
            C_sp (astropy Quantity array):
714
                Residual speckle spatial structure (systematic error) in units of
715
                1/s (optional)
716
            TK (:ref:`TimeKeeping`):
717
                TimeKeeping object (optional)
718

719
        Returns:
720
            tuple:
721
            intTimes (astropy Quantity array):
722
                Integration times
723
            sInds (integer ndarray):
724
                Integer indices of the stars of interest
725
            fZ (astropy Quantity array):
726
                Surface brightness of local zodiacal light in units of 1/arcsec2
727
            fEZ (astropy Quantity array):
728
                Surface brightness of exo-zodiacal light in units of 1/arcsec2
729
            WA (astropy Quantity):
730
                Working angle of the planet of interest in units of arcsec
731
            smin (ndarray):
732
                Minimum projected separations in AU
733
            smax (ndarray):
734
                Maximum projected separations in AU
735
            dMag (ndarray):
736
                Difference in brightness magnitude
737
        """
738

739
        # cast inputs to arrays and check
740
        intTimes = np.array(intTimes.value, ndmin=1) * intTimes.unit
1✔
741
        sInds = np.array(sInds, ndmin=1)
1✔
742
        fZ = np.array(fZ.value, ndmin=1) * fZ.unit
1✔
743
        fEZ = np.array(fEZ.value, ndmin=1) * fEZ.unit
1✔
744
        WA = np.array(WA.value, ndmin=1) * WA.unit
1✔
745
        assert len(intTimes) in [
1✔
746
            1,
747
            len(sInds),
748
        ], "intTimes must be constant or have same length as sInds"
749
        assert len(fZ) in [
1✔
750
            1,
751
            len(sInds),
752
        ], "fZ must be constant or have same length as sInds"
753
        assert len(fEZ) in [
1✔
754
            1,
755
            len(sInds),
756
        ], "fEZ must be constant or have same length as sInds"
757
        assert len(WA) in [
1✔
758
            1,
759
            len(sInds),
760
        ], "WA must be constant or have same length as sInds"
761
        # make constants arrays of same length as sInds if len(sInds) != 1
762
        if len(sInds) != 1:
1✔
763
            if len(intTimes) == 1:
1✔
764
                intTimes = np.repeat(intTimes.value, len(sInds)) * intTimes.unit
1✔
765
            if len(fZ) == 1:
1✔
766
                fZ = np.repeat(fZ.value, len(sInds)) * fZ.unit
1✔
767
            if len(fEZ) == 1:
1✔
768
                fEZ = np.repeat(fEZ.value, len(sInds)) * fEZ.unit
1✔
769
            if len(WA) == 1:
1✔
770
                WA = np.repeat(WA.value, len(sInds)) * WA.unit
×
771

772
        dMag = TL.OpticalSystem.calc_dMag_per_intTime(
1✔
773
            intTimes, TL, sInds, fZ, fEZ, WA, mode, C_b=C_b, C_sp=C_sp, TK=TK
774
        ).reshape((len(intTimes),))
775
        # calculate separations based on IWA and OWA
776
        IWA = mode["IWA"]
1✔
777
        OWA = mode["OWA"]
1✔
778
        smin = (np.tan(IWA) * TL.dist[sInds]).to("AU").value
1✔
779
        if np.isinf(OWA):
1✔
780
            smax = np.array(
×
781
                [self.PlanetPopulation.rrange[1].to("AU").value] * len(smin)
782
            )
783
        else:
784
            smax = (np.tan(OWA) * TL.dist[sInds]).to("AU").value
1✔
785
            smax[smax > self.PlanetPopulation.rrange[1].to("AU").value] = (
1✔
786
                self.PlanetPopulation.rrange[1].to("AU").value
787
            )
788
        smin[smin > smax] = smax[smin > smax]
1✔
789

790
        # take care of scaleOrbits == True
791
        if self.PlanetPopulation.scaleOrbits:
1✔
792
            L = np.where(
1✔
793
                TL.L[sInds] > 0, TL.L[sInds], 1e-10
794
            )  # take care of zero/negative values
795
            smin = smin / np.sqrt(L)
1✔
796
            smax = smax / np.sqrt(L)
1✔
797
            dMag -= 2.5 * np.log10(L)
1✔
798

799
        return intTimes, sInds, fZ, fEZ, WA, smin, smax, dMag
1✔
800

801
    def calc_fdmag(self, dMag, smin, smax):
1✔
802
        """Calculates probability density of dMag by integrating over projected
803
        separation
804

805
        Args:
806
            dMag (float ndarray):
807
                Planet delta magnitude(s)
808
            smin (float ndarray):
809
                Value of minimum projected separation (AU) from instrument
810
            smax (float ndarray):
811
                Value of maximum projected separation (AU) from instrument
812

813
        Returns:
814
            float:
815
                Value of probability density
816

817
        """
818

819
        f = np.zeros(len(smin))
1✔
820
        for k, dm in enumerate(dMag):
1✔
821
            f[k] = interpolate.InterpolatedUnivariateSpline(
1✔
822
                self.xnew, self.EVPOCpdf(self.xnew, dm), ext=1
823
            ).integral(smin[k], smax[k])
824

825
        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